Skip to content

Support PREWHERE and trivial count for Memory tables - #116248

Open
alexey-milovidov wants to merge 19 commits into
masterfrom
memory-prewhere
Open

Support PREWHERE and trivial count for Memory tables#116248
alexey-milovidov wants to merge 19 commits into
masterfrom
memory-prewhere

Conversation

@alexey-milovidov

@alexey-milovidov alexey-milovidov commented Aug 25, 2026

Copy link
Copy Markdown
Member

Related: ClickHouse/ClickBench#1590

Changelog category (leave one):

  • Performance Improvement

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

Support PREWHERE (including the automatic move of WHERE conditions by optimize_move_to_prewhere) for Memory tables: only the columns of the conditions are read at first, and the remaining columns are read only for the blocks where some rows pass, and only for the passing rows. This is especially beneficial for tables with SETTINGS compress = true, because for a selective condition most columns are never decompressed. Additionally, SELECT count() FROM table on a Memory table is now served from metadata, and system.columns shows real per-column sizes for Memory tables.

The motivation is benchmarking a compressed in-memory table on ClickBench (ClickHouse/ClickBench#1590), where the last seven queries (CounterID = 62) lose the primary index of MergeTree and previously had to decompress every referenced column of every block.

Implementation:

  • StorageMemory::supportsPrewhere is now true. MemorySource applies the pushed-down row-level security filter and PREWHERE inside the reading source: it materializes only the filter-input columns, executes the filter steps, skips a block entirely when no row passes, and reads the remaining columns only for the surviving rows (IColumn::filter with the combined mask). The block layout is kept in exact correspondence with the output header, which SourceStepWithFilter::applyPrewhereActions builds by running the same actions on the sample block.
  • StorageMemory::getColumnSizes reports real per-column in-memory sizes (compressed sizes when compress = true). This is what enables the plan-level WHERE -> PREWHERE optimization (it declines on storages with no column sizes) and lets MergeTreeWhereOptimizer order conditions by the actual cost of reading their columns.
  • StorageMemory::supportsTrivialCountOptimization is now true, guarded against tables that are filled during query execution (materialized CTEs, GLOBAL subquery temporary tables) and against pinned snapshots (atomic CREATE MATERIALIZED VIEW ... POPULATE), where totalRows must not be observed at planning time.
  • MemorySource reports the read progress explicitly: the automatic accounting of ISource uses the returned chunk, which holds only the rows that passed the filter, and nothing at all for a block the filter eliminated completely. read_rows, SelectedRows, max_rows_to_read and the read quotas see the number of scanned rows, the same as before and the same as what ReadFromMergeTree reports for its PREWHERE.

Two bugs of other code that this change makes reachable are fixed here as well:

  • InterpreterSelectQuery read the MergeTree parts for the condition selectivity estimator with an assert_cast of storage_snapshot->data, which is a plain static_cast in a release build. MergeTreeData::SnapshotData and StorageMemory::SnapshotData are the only two types of storage snapshot data and they alias: the row count of the latter sits at the offset of the parts pointer of the former. It was unreachable, because StorageMemory was the only storage with its own snapshot data and it did not allow moving conditions to PREWHERE. Making Memory support PREWHERE turned it into a segmentation fault on the WHERE -> PREWHERE move with enable_analyzer = 0.
  • StorageMerge::supportsTrivialCountOptimization only asked the source tables the same question, while the row policy of a source table is applied later, when createChildrenPlans builds the child read plan, and is not reflected in the source table's totalRows. SELECT count() from the Merge table therefore counted the rows the policy hides. This is reproducible on master with a File source table; for a source table of the MergeTree family it is masked by apply_patch_parts, which is enabled by default and makes MergeTreeData::supportsTrivialCountOptimization decline for the snapshot-less check StorageMerge performs.

Benchmark (ClickBench queries, 10M-row hits subset in a Memory table with compress = true, 96-core aarch64, hot runs, new binary with the optimizations toggled off/on via optimize_move_to_prewhere / optimize_trivial_count_query):

Query off on speedup
Q0 SELECT COUNT(*) 0.003 0.001 3.0x
Q23 SELECT * ... URL LIKE '%google%' ORDER BY ... LIMIT 10 0.100 0.078 1.3x
Q36 WHERE CounterID = 62 AND EventDate ... 0.032 0.022 1.5x
Q38 0.025 0.010 2.5x
Q40 0.019 0.009 2.1x
Q41 0.018 0.008 2.3x
Q42 0.015 0.008 1.9x
SELECT * point lookup by WatchID 0.094 0.044 2.1x

The remaining queries are unchanged within noise. The effect grows with table size and filter selectivity: on the full 100M-row dataset the eliminated blocks dominate.


Workflow [PR]
Sync PR [sync-upstream/pr/116248]

alexey-milovidov and others added 4 commits August 25, 2026 05:07
PREWHERE (and the pushed-down row-level security filter) is applied inside
MemorySource: only the columns of the conditions are read at first, and the
remaining columns are read only for the blocks where some rows pass and only
for the passing rows. For a table with SETTINGS compress = true a selective
condition skips decompression of all other columns for the blocks it
eliminates.

StorageMemory::getColumnSizes reports real per-column in-memory sizes
(compressed sizes when compress = true), which both enables the automatic
WHERE -> PREWHERE move in the query plan optimization and lets it order
conditions by the actual cost of reading their columns.

SELECT count() FROM table is served from metadata (totalRows is exact,
maintained under the write mutex).

Motivated by benchmarking a compressed Memory table:
ClickHouse/ClickBench#1590

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s and docs

A materialized CTE and a GLOBAL subquery temporary table are filled during
query execution, after the planner would have observed totalRows (as zero),
so the trivial count optimization must not apply to them.

Also update the in-code Memory engine documentation and add functional and
performance tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ents

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@clickhouse-gh

clickhouse-gh Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Workflow [PR], commit [3e77002]

Summary:

job_name test_name status info comment
Upgrade check (amd_release) FAIL
Error message in clickhouse-server.log (see upgrade_error_messages.txt) FAIL cidb

AI Review

Summary

This PR adds PREWHERE, metadata-backed trivial count(), and per-column size reporting for Memory tables. The StorageMerge row-policy fix and the read-progress accounting look correct in the current head, but the new IN (subquery) hardening for source-local PREWHERE still runs after CreatingSetsStep has already claimed the subquery plan, so I cannot approve it as-is.

Findings
  • ⚠️ Major: [src/Processors/QueryPlan/ReadFromMemoryStorageStep.cpp:430-433] makeSourceFilter() tries to prepare IN-subquery sets during pipeline construction, after QueryPlan::optimize() has already expanded DelayedCreatingSetsStep and moved each FutureSetFromSubquery::source into a CreatingSetsStep child (src/Interpreters/PreparedSets.cpp:430, src/Processors/QueryPlan/Optimizations/addPlansForSets.cpp:15-33). That is exactly the timing src/Interpreters/PreparedSets.h:156-159 and src/Storages/VirtualColumnUtils.h:32-39 say is too late. As a result, non-key PREWHERE ... IN (SELECT ...) and pushed row-policy filters on Memory still depend on the pipeline-level CreatingSetsStep, so the downstream short-circuit race this change is trying to eliminate remains reachable.
    Suggested fix: move the in-place set build to a pre-addStepsToBuildSets() hook analogous to SourceStepWithFilter::applyFilters(), and keep excluding GLOBAL IN sets there.
Tests
  • ⚠️ 05057_memory_prewhere_in_subquery covers ordinary execution, but it does not exercise the early-close path that motivated this hardening. Please add a focused regression where a downstream branch short-circuits CreatingSetsStep while a non-key PREWHERE ... IN (SELECT ...) or row-policy filter still has to run on Memory.
Final Verdict
  • Status: ⚠️ Request changes
  • Minimum required actions: move the Memory in-place set preparation to a phase before addStepsToBuildSets() consumes the subquery plan.
  • Minimum required actions: add a regression that proves the early-close / short-circuit shape is actually fixed.

LLVM Coverage Report

Measured on commit 3e77002.

Metric Baseline Current Δ
Lines 88.90% 88.90% +0.00%
Functions 91.80% 91.80% +0.00%
Branches 81.30% 81.30% +0.00%

Changed lines: Changed C/C++ lines covered: 315/328 (96.04%) · Uncovered code

Full report · Diff report

@clickhouse-gh clickhouse-gh Bot added the pr-performance Pull request with some performance improvements label Aug 25, 2026
@clickhouse-gh

clickhouse-gh Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

📊 Cloud Performance Report

✅ AI verdict: no_change — no significant changes across 40 queries analysed

This PR adds PREWHERE support, trivial-count-from-metadata, and real per-column sizes for Memory tables (plus a StorageMerge row-policy fix); ClickBench's benchmark tables use the MergeTree engine, so the server's MergeTree read/aggregation hot path is untouched. The one flagged query, ClickBench Q31 (GROUP BY SearchEngineID, ClientIP), was deterministically kept as not_sure inside master's current variance band, and its history is intrinsically noisy, so the +12.6% here reads as run-to-run variance rather than a real PR effect. No regression is attributable to this change. The new memory_prewhere performance test is where the intended improvement should show up.

clickbench

⚠️ 1 inconclusive

Flagged queries (1 of 43)
Query Verdict Baseline median (ms) PR median (ms) Change q-value Hint
⚠️ 31 not_sure 266 300 +12.6% <0.0001 aggregation: Q31 groups by SearchEngineID, ClientIP with WHERE SearchPhrase <> '', no PREWHERE; the deterministic gate already read this +12.6% as within master's noisy variance band. Its 12.6% delta is run-to-run noise, not a PREWHERE effect.

Change = percent below ×2; the ratio of medians (×N faster/slower) beyond, where percent understates the scale. q-value = BH-FDR adjusted p; smaller is stronger evidence. MIRAI flags a query when q < fdr_q (default 0.10) — the value the verdict is based on.

tpch_adapted_1_official

🟢 No significant changes

Debug info
  • StressHouse run: 5354f7bf-c01b-4e7c-afa3-ff3cdf11db98
  • MIRAI run: 26191bb6-e4a0-4043-a86a-bb87547beb78
  • PR check IDs:
    • clickbench_146409_1788993230
    • clickbench_146415_1788993230
    • clickbench_146421_1788993230
    • tpch_adapted_1_official_146433_1788993230
    • tpch_adapted_1_official_146466_1788993230
    • tpch_adapted_1_official_146481_1788993232

alexey-milovidov and others added 4 commits August 27, 2026 11:53
Virtual columns (e.g. `_table`) are materialized outside the reading
source, so the in-source filter cannot read them: `SELECT * FROM t
PREWHERE _table = 't'` failed with `NOT_FOUND_COLUMN_IN_BLOCK`.
Declare `supportedPrewhereColumns`, so both the analyzer and the
plan-level WHERE -> PREWHERE optimization reject such conditions with
`ILLEGAL_PREWHERE`, as asserted by `03094_virtual_column_table_name`.

https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=116248&sha=ebb9ae35257ee1588af487bfadfbb6002ac9c7d6&name_0=PR&name_1=Fast%20test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ake)

Generated by running the tests; the values match manual computation and
the outputs observed in the CI report
https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=116248&sha=ebb9ae35257ee1588af487bfadfbb6002ac9c7d6&name_0=PR&name_1=Fast%20test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- `03610_disjunctions_pushdown_optimization` already pins
  `optimize_move_to_prewhere` and `query_plan_optimize_prewhere` to 1,
  so the pushed-down disjunctions over `Memory` tables now become
  PREWHERE; update the expected plan accordingly.
- `03777_join_precalculate_keys` and
  `03707_analyzer_convert_outer_any_to_inner` assert on join plans, so
  pin `optimize_move_to_prewhere = 0` to keep the asserted plans
  independent of the (harness-randomized) PREWHERE move.
- `03562_short_circuit_for_and_or` asserts on `read_rows` of count
  subqueries to prove short circuit; pin
  `optimize_trivial_count_query = 0`, because serving the count of a
  `Memory` table from metadata would defeat that signal.

https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=116248&sha=ebb9ae35257ee1588af487bfadfbb6002ac9c7d6&name_0=PR&name_1=Fast%20test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread src/Storages/StorageMemory.cpp
Comment thread src/Storages/StorageMemory.cpp Outdated
@clickhouse-gh

clickhouse-gh Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Build profile diff (arm_release)

Comparing 3e7700201 with master f6679eba0 (stripped binary size, per-symbol sizes and ThinLTO time; compile times per translation unit against the most recent warmup build that recompiled it).

✅ No significant changes.

Binary sizes

programs/clickhouse-stripped: smaller than the master baseline by the known offset between the two builds, so the difference is not shown. A delta that differs from the offset by more than 50% of it is shown, in either direction.

The official master build is compiled with -g and a pull request build is not, and XRay counts debug instructions towards its instrumentation threshold, so master instruments thousands of functions more and its binary is ~0.4% larger no matter what the pull request does.

Object file sizes

9 object files changed (+72.21 KiB total), 0 added.

Object file Master PR Δ
src/CMakeFiles/dbms.dir/Processors/QueryPlan/ReadFromMemoryStorageStep.cpp.o 204.84 KiB 254.82 KiB +49.98 KiB (+24.40%)
src/CMakeFiles/dbms.dir/Storages/StorageMemory.cpp.o 601.66 KiB 622.64 KiB +20.98 KiB (+3.49%)

716 more object files are built by the master warmup baseline only (it builds every object-file target, a pull request build only clickhouse-bundle) and not compared.

Compile time of recompiled translation units

16 translation units recompiled, 106 s compile time in total, 16 of them have a recent master baseline.

Job report

…ocks

`SELECT count()` on a `Memory` table is served from the row counter, while ordinary
reads use the set of blocks captured in `getStorageSnapshot`. The counters were
separate atomics updated around `data.set`, so a concurrent reader could observe a
row count that corresponded to no state the table ever had.

Move the row and byte counters into the `MultiVersion` object that holds the blocks,
as the pre-existing `TODO` in `getStorageSnapshot` suggested. They are now published
atomically together with the blocks they describe, `totalRows` is the exact row count
of a committed state, and `SnapshotData::rows` is exact rather than approximate.

Also restrict `supportedPrewhereColumns` to the stored columns without a `DEFAULT`
expression (the same restriction as `StorageFile`): such a column is absent from the
blocks written before `ALTER TABLE ... ADD COLUMN`, and the in-source filter reads it
as the default value of its type instead of evaluating the expression. `ALIAS` and
`EPHEMERAL` columns are excluded as well, because they are never stored.

Add `05052_memory_prewhere_added_column` and `05053_memory_trivial_count_concurrent`.
Comment on lines +391 to +420
MemorySourceFilterPtr ReadFromMemoryStorageStep::makeSourceFilter(const NamesAndTypesList & physical_columns) const
{
if (!query_info.row_level_filter && !query_info.prewhere_info)
return nullptr;

auto result = std::make_shared<MemorySourceFilter>();
ExpressionActionsSettings actions_settings(context);

/// The row-level security filter runs first, so PREWHERE expressions are never evaluated
/// on the rows the policy hides.
if (query_info.row_level_filter)
{
const auto & row_level_filter = *query_info.row_level_filter;
result->steps.push_back({
.actions = std::make_shared<ExpressionActions>(row_level_filter.actions.clone(), actions_settings),
.filter_column_name = row_level_filter.column_name,
.remove_filter_column = row_level_filter.do_remove_column,
.replace_filter_to_constant = false,
});
}

if (query_info.prewhere_info)
{
const auto & prewhere_info = *query_info.prewhere_info;
result->steps.push_back({
.actions = std::make_shared<ExpressionActions>(prewhere_info.prewhere_actions.clone(), actions_settings),
.filter_column_name = prewhere_info.prewhere_column_name,
.remove_filter_column = prewhere_info.remove_prewhere_column,
.replace_filter_to_constant = !prewhere_info.remove_prewhere_column && prewhere_info.need_filter,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new source-local filter path executes row_level_filter / PREWHERE actions inside MemorySource::generateFiltered(), but it never prepares the IN (subquery) sets those DAGs can carry. VirtualColumnUtils / PreparedSets require that to happen in applyFilters(), and ReadFromMergeTree also has a follow-up updatePrewhereInfo() hook for conditions that are moved into PREWHERE after applyFilters() already ran.

As written, valid queries such as SELECT ... FROM memory_table PREWHERE k IN (SELECT ...) can still fail with LOGICAL_ERROR: Not-ready Set is passed, and the same problem applies to pushed row-policy filters using IN. I think this step needs the same set-preparation handling as ReadFromMergeTree, plus a focused regression test for explicit PREWHERE ... IN (SELECT ...) and a row policy with IN.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕵 Done in 961435020213: makeSourceFilter now calls VirtualColumnUtils::buildSetsForDAGExcludingGlobalIn on query_info.row_level_filter->actions and query_info.prewhere_info->prewhere_actions before it turns them into ExpressionActions. That is a single point covering every path into the source (explicit PREWHERE, a condition moved there by optimizePrewhere after applyFilters already ran, a pushed row policy, and the delay_read_for_global_sub_queries reader), so it does not depend on query_plan_optimize_primary_key being enabled, unlike splitting the work between applyFilters and updatePrewhereInfo as ReadFromMergeTree does. Sets of GLOBAL IN are excluded for the same reason as in ReadFromMergeTree::updatePrewhereInfo: ReadFromRemote has to attach an external table to them first.

One correction to the finding: I could not reproduce an actual failure. On a binary built without this change, PREWHERE k IN (SELECT ...), NOT IN, the optimizer-moved variant, use_index_for_in_with_subqueries = 0, a self-referencing subquery, Merge over Memory, and a row policy USING k IN (SELECT ...) all return correct results, because the pipeline-level CreatingSetsStep does gate the source through DelayedPortsProcessor in those plans. So this is the same hardening ReadFromMergeTree carries against the short-circuit race, not a currently reachable Not-ready Set is passed.

The regression test is 05057_memory_prewhere_in_subquery, covering explicit PREWHERE ... IN (SELECT ...), NOT IN, the optimizer-moved form, and a row policy with IN combined with both PREWHERE and trivial count().

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still runs too late to make the IN sets ready on the source-local path.

src/Interpreters/PreparedSets.h:156-159 and src/Storages/VirtualColumnUtils.h:32-39 both require the in-place build to happen from SourceStepWithFilter::applyFilters(): once the plan reaches pipeline construction, CreatingSetsStep may already own the subquery. That is exactly what happens here. QueryPlan::optimize() expands DelayedCreatingSetsStep before pipeline build (src/Processors/QueryPlan/Optimizations/optimizeTree.cpp:875-896), addPlansForSets() asks each FutureSetFromSubquery for a plan (src/Processors/QueryPlan/Optimizations/addPlansForSets.cpp:15-33), and FutureSetFromSubquery::build() does auto plan = std::move(source) (src/Interpreters/PreparedSets.cpp:430).

So by the time ReadFromMemoryStorageStep::makeSourceFilter() reaches buildSetsForDAGExcludingGlobalIn here, buildOrderedSetInplace / buildSetInplace no longer have a source plan to execute. Non-key PREWHERE ... IN (SELECT ...) and pushed row-policy filters therefore still fall back to the pipeline-level CreatingSetsStep, so the downstream short-circuit race this hardening is trying to close remains reachable.

…er test

`ReadFromMemoryStorageStep` evaluates the row-level security filter and
`PREWHERE` inside `MemorySource`, so a condition such as
`PREWHERE k IN (SELECT ...)` carries a `FutureSet` that has to be ready by
the time the source runs. The pipeline-level `CreatingSetsStep` normally
fills it in, but `DelayedPortsProcessor` can be short-circuited by a
downstream processor that closes its inputs early, which is why
`ReadFromMergeTree` builds those sets in place for its storage-level
`PREWHERE`. Do the same in `makeSourceFilter`, excluding the sets of
`GLOBAL IN`, to which `ReadFromRemote` still has to attach an external
table. Added `05057_memory_prewhere_in_subquery` covering explicit and
optimizer-moved `PREWHERE ... IN (SELECT ...)` and a row policy with `IN`.

`05052_memory_prewhere_added_column` failed with the old analyzer: it
substitutes an `ALIAS` column expression into `PREWHERE` before the storage
sees it, so `PREWHERE a = 4` is not rejected there. Pin `enable_analyzer`
for that assertion. Renumbered the two tests that collided with master.

https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=116248&sha=de59dc98eb76385347efe69e31ca5bf9b02bdf47&name_0=PR&name_1=Stateless%20tests%20%28amd_llvm_coverage%2C%20old%20analyzer%2C%20s3%20storage%2C%20DBReplicated%2C%20parallel%2C%202%2F3%29
#116248
Comment thread src/Processors/QueryPlan/ReadFromMemoryStorageStep.cpp
Comment thread src/Storages/StorageMemory.cpp
@clickhouse-gh clickhouse-gh Bot added the comp-simple-engines Lightweight single-node table engines: Log/StripeLog (append-only logs), Buffer (async batching),... label Sep 4, 2026
alexey-milovidov and others added 7 commits September 7, 2026 17:45
…a source table has a row policy

`StorageMerge::supportsTrivialCountOptimization` only asked the source tables
the same question, and `StorageMerge::totalRows` sums their `totalRows`. The
row policy of a source table is applied later, while `createChildrenPlans`
builds the child read plan (`RowPolicyData`), and it is not reflected in the
source table's `totalRows`, so `SELECT count()` from the `Merge` table returned
the count including the rows the policy hides.

This is reproducible on `master` with a source table of a storage that
advertises the trivial count unconditionally, e.g. `File`:

```
CREATE TABLE file_child (x UInt64) ENGINE = File(TSV);
INSERT INTO file_child SELECT number FROM numbers(10);
CREATE TABLE merge_over_file (x UInt64) ENGINE = Merge(currentDatabase(), '^file_child$');
CREATE ROW POLICY pol ON file_child USING x < 3 TO ALL;
SELECT count() FROM file_child;      -- 3
SELECT count() FROM merge_over_file; -- 10, must be 3
```

For a source table of the `MergeTree` family the gap is masked by
`apply_patch_parts`, which is enabled by default and makes
`MergeTreeData::supportsTrivialCountOptimization` decline for the snapshot-less
check `StorageMerge` performs. Making `Memory` support the trivial count opens
the gap for `Memory` source tables as well, so close it in `StorageMerge`:
decline when any source table has a row policy that is not always true. The row
policy of the `Merge` table itself is already checked by the caller.
…in the source

`ISource` derives the read progress from the returned chunk, which for the
in-source filter holds only the rows that passed, and nothing at all for a
block the filter eliminates completely. That under-reported `read_rows` and
`SelectedRows` and weakened `max_rows_to_read` and read quotas for a selective
scan of a `Memory` table.

Report the progress explicitly in `MemorySource::generateFiltered` for every
scanned block, including the blocks where no row passes: the number of rows
scanned, and the size of the columns actually materialized from the block. This
suppresses the automatic accounting of `ISource` (it only kicks in when the
generator reported nothing), so the rows are not counted twice, and it makes
the number of rows the same as before the in-source filter existed - and the
same as what `ReadFromMergeTree` reports for its `PREWHERE`.
…nalyzer

`InterpreterSelectQuery` read the `MergeTree` parts for the condition
selectivity estimator with an `assert_cast` of `storage_snapshot->data`, which
in a release build is a plain `static_cast`. `MergeTreeData::SnapshotData` and
`StorageMemory::SnapshotData` are the only two types of storage snapshot data,
and they alias: the `size_t rows` of the latter sits at the offset of the
`RangesInDataPartsPtr parts` of the former. Until now this was unreachable,
because `StorageMemory` was the only storage with its own snapshot data and it
did not allow moving conditions to `PREWHERE`; every other storage that does
leaves `storage_snapshot->data` empty.

Making `Memory` support `PREWHERE` opened this path, and a table with one row
made the cast produce `parts = 0x1`, which passed the null check and was
dereferenced:

    Address: 0x1. Access: <not available>. Address not mapped to object.
    ...
    src/Interpreters/InterpreterSelectQuery.cpp:908: DB::InterpreterSelectQuery::InterpreterSelectQuery(...)::$_0::operator()(bool) const

It fired on the existing test `04927_date_preimage_result_correctness`, whose
`Memory` table is queried with a `WHERE` and `enable_analyzer = 0`. The new
tests of this pull request all used an explicit `PREWHERE` with the old
analyzer, and the crash needs a `WHERE` and no `PREWHERE`.

Check the type of the snapshot data instead, the same way
`ReadFromMergeTree::createProjectionQueryPlan` does. The parts are only used by
the `MergeTree` condition selectivity estimator, so leaving them empty for
other storages is the correct behaviour, not a fallback.

Report: https://s3.amazonaws.com/clickhouse-test-reports/praktika.html?PR=116248&sha=961435020213aeb0283b85946de22b833445e67b&name_0=PR&name_1=Stateless%20tests%20%28arm_binary%2C%20parallel%29
… the read progress

`05136_merge_trivial_count_row_policy`: `SELECT count()` from a `Merge` table
whose source table has a row policy - for a `Memory` source table, and for a
`File` source table, where the wrong result is reproducible on `master`.

`05137_memory_prewhere_old_analyzer`: the WHERE -> PREWHERE move for a `Memory`
table with `enable_analyzer = 0`, which used to be a segmentation fault. The
new tests of this pull request used an explicit `PREWHERE` with the old
analyzer, and the crash needs a `WHERE` and no `PREWHERE`.

`05138_memory_prewhere_read_rows`: `read_rows` of a selective `PREWHERE` over a
`Memory` table is the number of scanned rows, both when one row passes and when
no row passes at all.

Also fold the row policy of the target of a matched `Alias` table into the
comment of `StorageMerge::supportsTrivialCountOptimization`: it needs no check
of its own, because `StorageAlias` declines the trivial count for the
snapshot-less check that `StorageMerge` performs.
# Conflicts:
#	tests/queries/0_stateless/03707_analyzer_convert_outer_any_to_inner.sql
`05138_memory_prewhere_read_rows` looked up the queries in `system.query_log`
with `query LIKE '%FROM t_memory_read_rows %'`, and the third query of the test
ends with `FROM t_memory_read_rows;`, so the trailing space excluded it and only
two of the three expected `read_rows` values were returned. Dropped the trailing
space from the pattern.

`04330_join_disjunctions_pushdown_using_type_mismatch` asserts on the plan of a
join over two `Memory` tables. Now that `Memory` supports `PREWHERE`, the filters
that the disjunction push-down places above the reads are moved into `PREWHERE`
and print as `Prewhere filter column:` instead of `Filter column:`, and both
`optimize_move_to_prewhere` and `query_plan_optimize_prewhere` are randomized by
the test harness, so the assertion was not deterministic either way. Pinned both
to 0 in the two `EXPLAIN` queries, which keeps the reference of the test as it is
on master.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp-simple-engines Lightweight single-node table engines: Log/StripeLog (append-only logs), Buffer (async batching),... pr-performance Pull request with some performance improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant