Fix sort order violation for TTL GROUP BY with SET on a sorting key column - #108550
Fix sort order violation for TTL GROUP BY with SET on a sorting key column#108550groeneai wants to merge 69 commits into
Conversation
Pre-PR validation gate (click to expand)
Session id: cron:clickhouse-worker-slot-4:20260625-200200 |
|
cc @CurtizJ @KochetovNicolai — could you review this? It fixes a sort-order violation (silent primary-index corruption in release, LOGICAL_ERROR abort in debug) when a |
|
Workflow [PR], commit [98b5743] Summary: ❌
AI ReviewSummaryThis PR broadens the PR Metadata
Exact replacement: Final VerdictNo new blocking or major code issues found in the current PR head. Tighten the PR text so it matches the final non-deterministic LLVM Coverage ReportMeasured on commit 98b5743.
Changed lines: Changed C/C++ lines covered: 848/895 (94.75%) · Uncovered code |
|
Fixed in e779631. The gate now maps each sorting-key dependency to its storage column via One more place needed the same treatment: the re-sort step built Validated both directions on a debug build: |
CI finish ledger — e779631Every failed check below has an owner (a fixing PR).
Not PR-caused: this PR only changes Session id: cron:our-pr-ci-monitor:20260626-033000 |
|
@groeneai This fixes the MergeTask path at |
|
@cv4g Good catch on MutateTask ( Reproduced on this branch (with the merge fix already in) using a single level-0 part so the mutation, not a background merge, applies the TTL: CREATE TABLE t (k Float64, ts DateTime, v Float64)
ENGINE = MergeTree ORDER BY (k, toStartOfDay(ts))
TTL ts + toIntervalDay(1) GROUP BY k, toStartOfDay(ts)
SET ts = max(ts) + interval 100 years, k = max(v)
SETTINGS min_bytes_for_full_part_storage = 128, materialize_ttl_recalculate_only = 0;
SYSTEM STOP TTL MERGES t;
INSERT INTO t VALUES (1.0, '2000-06-09 10:00', 96827), (1.0, '2000-06-10 10:00', 41302);
ALTER TABLE t MATERIALIZE TTL SETTINGS mutations_sync = 2;
-- before bcd95db: physical read order is k = 96827, 41302 (descending) -> corrupt index
-- after bcd95db: k = 41302, 96827 (ascending) -> sortedThe fix extracts the gate and the recompute-and-resort into Multiple |
|
Addressed the secondary-index finding in f977bca (full analysis in the inline thread). Net scope of this PR now: the sort-order re-sort covers both the merge path (MergeTask) and the mutation path (MutateTask / Pre-PR validation gate (click to expand)
Session id: cron:clickhouse-worker-slot-8:20260630-124500 |
CI finish ledger — f977bcaCI fully finished (Finish Workflow: success). This PR fixes the TTL
Session id: cron:our-pr-ci-monitor:20260630-173000 |
|
Re: the |
A MergeTree table with `TTL ... GROUP BY ... SET <col> = <agg>` where the SET clause rewrites a column the table's sorting key depends on (directly, or via an expression such as toStartOfDay(ts)) produced an out-of-order data part during the TTL merge. TTLAggregationAlgorithm::finalizeAggregates emits aggregated groups in the input (already-sorted) order, and the merge writer trusts the stream order: it reads the primary-key columns by name (including the materialized sort-key expression column, which still holds its pre-SET value) and writes them without re-sorting. When the SET changes a sort-key column the produced part is therefore not sorted by the sorting key. In a debug build the merge's CheckSortedTransform aborts the server with "Sort order of blocks violated"; in a release build the part is written silently with a primary index inconsistent with the data, which can return wrong results for primary-key-filtered queries. Fix: after the TTL step in the merge pipeline, when a GROUP BY TTL assigns a sorting-key dependency column, recompute the sort-key expression columns from the post-SET values (overwriting the now-stale materialized ones) and re-sort by the sorting key. The step is gated so it is a no-op for every other merge. This preserves the existing tested behaviour where SET targets a sort-key column but the output happens to stay ordered (03545_number_of_rows_in_ttltransform). Closes ClickHouse#108514 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n sorting keys The re-sort gate added for the TTL GROUP BY SET sort-order fix compared the sorting-key dependency names against the SET target column name directly. A sorting-key dependency can be a subcolumn (ORDER BY t.a requires t.a), while a SET target is always a physical storage column (SET t = ...). The names never matched for a subcolumn sorting key, so the gate did not fire and the part was built from the stale pre-SET subcolumn value. Map each sorting-key dependency to its storage column with getColumnNameInStorage before comparing, the same way extractMergingAndGatheringColumns does. Also hide the stale materialized sort-key columns from the subcolumn extractor in the re-sort step so the subcolumn is recomputed from the post-SET physical column instead of reusing the stale value. Adds a Tuple subcolumn regression to 04327_ttl_group_by_set_sort_key_resort. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n path The same sort-order violation the merge path fixes is also reachable through ALTER TABLE ... MATERIALIZE TTL. A MATERIALIZE TTL mutation with a GROUP BY TTL runs MutateAllPartColumnsTask, which materializes the sorting-key expression before the TTLTransform and writes through MergedBlockOutputStream (rewrite_primary_key=true). When the GROUP BY ... SET rewrites a column the sorting key depends on, the aggregation emits groups in input order, so the stream is no longer ordered by the sorting key and the materialized sort-key expression is stale. The mutation writer rebuilds the primary index from that stream, so it gets an index inconsistent with the data; there is no CheckSortedTransform after the TTL step in the mutation pipeline, so a debug build does not even catch it and a release build silently writes a corrupt part. Extract the gate (groupByTTLAssignsSortKeyColumn) and the recompute-and-resort logic into TTLResortUtils, shared by both paths. After the TTL step in MutateAllPartColumnsTask, when the gate fires, recompute the sorting-key expression columns from the post-SET values and re-sort by the sorting key, the same way the merge path does. Gated to a no-op for every other mutation. MutateSomePartColumnsTask is not affected: a GROUP BY TTL forces the full rewrite path, and that task writes through MergedColumnOnlyOutputStream with rewrite_primary_key=false, which reuses the source part index. Extends the regression test 04327_ttl_group_by_set_sort_key_resort with MATERIALIZE TTL cases for a plain Float64 sort key and a Tuple subcolumn sort key, asserting the produced part is physically ordered by the sorting key. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A `MATERIALIZE TTL` mutation with a `GROUP BY` TTL records the rewritten physical columns as `TTL_TARGET` (for example `t`), while a skip index can depend on a subcolumn of one of them (for example `INDEX idx t.a ...`). The rebuild decision in MutationsInterpreter compared the index/projection required columns against the changed columns literally, so `t.a` did not match the changed physical column `t`: the index was not added to materialized_indices and on a full Wide part it was hardlinked from the source part unchanged, keeping its pre-SET minmax values. Queries using that index then prune against stale values (missing rows) or, since the index was never registered in the new part's checksums, stop pruning entirely. Map each required column to its storage column (the same way the sorting-key gate does) before comparing, so subcolumn indices and projections of a rewritten column are rebuilt. Rebuilding the index exposed a second problem in the mutation pipeline. The primary-key and skip-index expressions were materialized before the TTL step, but a `GROUP BY` TTL aggregates the stream: the pre-step skip-index expression columns are computed from pre-aggregation, pre-SET rows, and the aggregation can drop them entirely, leaving a zero-row column that aborts the TTL transform. Mirror the merge path: when a `GROUP BY` TTL runs, materialize only the primary-key expression before the step (the aggregation groups by sort-key columns and needs them present) and compute the skip-index expressions after it. When the SET also rewrites a sort-key column, re-sort first so the rebuilt primary index matches the written row order, then recompute the skip-index expressions from the post-SET stream. Extends 04327_ttl_group_by_set_sort_key_resort with a MATERIALIZE TTL case for a skip index on a subcolumn of a TTL-rewritten column that is not in the sorting key (so the primary index cannot mask a stale skip index): the rebuilt index must return the same rows with and without skip indexes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
f977bca to
a0d5af7
Compare
A TTL GROUP BY SET can rewrite a column that a MATERIALIZED sort-key column is computed from (e.g. d Date MATERIALIZED toDate(ts), ORDER BY d, ... GROUP BY d SET ts = ...). The aggregation updates ts but leaves the stored d on its pre-SET value, so the part is written with stale sort-key data and pruning/filtering on d uses the old value. groupByTTLAssignsSortKeyColumn previously only detected a SET target that is itself a sort-key dependency. Extend detection to also flag a SET target that is a source of a MATERIALIZED sort-key column (analyzed the same way the UPDATE mutation path builds column_to_affected_materialized), and recompute those columns from their default expression before recomputing the sorting key and re-sorting. Applied to both the merge (MergeTask) and mutation (MutateTask, MATERIALIZE TTL) paths via shared TTLResortUtils helpers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Follow-up commit f2aaf8e addresses the bot's MATERIALIZED sort-key finding. Pre-PR validation gate (click to expand)
Session id: cron:clickhouse-worker-slot-0:20260711-122100 |
…GROUP BY SET Extend the MATERIALIZED-sort-key recompute for TTL GROUP BY SET in two ways: 1. Transitive closure: a SET on a base column can invalidate a MATERIALIZED sort-key column several hops away (x -> y MATERIALIZED toDate(x) -> z MATERIALIZED toYYYYMM(y), ORDER BY z). The affected-column detection now takes the fixpoint over the materialized-dependency graph and recomputes the intermediate hops (y before z), instead of a one-hop check that left z stale. 2. Subcolumn sources: buildRecomputeMaterializedColumnsDAG now prepends a subcolumn-extraction DAG (as AddingDefaultsTransform does) so a default expression reading a subcolumn (d MATERIALIZED toDate(tup.ts)) can be recomputed; previously it failed with NOT_FOUND_COLUMN_IN_BLOCK. Both paths (merge and MATERIALIZE TTL) go through the shared helper. Regression cases added to 04327 for the transitive chain and the subcolumn source. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Addressed both clickhouse-gh[bot] findings on TTLResortUtils.cpp in 3071816. Pre-PR validation gate (click to expand)
Session id: cron:clickhouse-worker-slot-1:20260711-133400 |
CI finish ledger - 7c031d1Every failure below has an owner: a fixing PR (ours or external), or a full-effort fix task
Session id: cron:our-pr-ci-monitor:20260819-000000 |
… values The `t_future_ttl_after_modify` arm saved a reference copy with `ENGINE = Memory AS SELECT * FROM t_future_ttl_after_modify`, but `SELECT *` does not expand to MATERIALIZED columns, so the saved table held only `ts` and `x`. The following assertion reads `s.m` and therefore failed to analyze: Code: 47. DB::Exception: Identifier 's.m' cannot be resolved from table with name s. Maybe you meant: ['s.x']. (UNKNOWN_IDENTIFIER) Naming `x, m` in the projection materialises the column into the snapshot. The arm's intent is unchanged: it still compares the stored `m` before and after `MATERIALIZE TTL` to prove a future GROUP BY TTL that never fires does not recompute a non-deterministic MATERIALIZED default. Verified the assertion is a live oracle rather than merely well-formed: with the `hasNonDeterministic()` guard in `getGroupByTTLSetAffectedMaterializedColumns` removed, the arm reports 0 (2 joined rows, 0 matching); with the guard present it reports 1 (2 of 2 matching). 20/20 passes with CI's settings randomization enabled, and 4 concurrent copies pass, so the file keeps working without a no-parallel tag.
CI finish ledger - b2b5033Every failure below has an owner: a fixing PR (mine or external), or a full-effort fix task
169 of 170 checks are green (152 success, 17 skipped). No other failures. Session id: cron:our-pr-ci-monitor:20260819-073000 |
CI finish ledger - 3614f3cEvery failure below has an owner: a fixing PR (ours or external), or a full-effort fix task
Both build failures are one omission, not a wrong setting name. The new block at All three jobs concluded Session id: cron:our-pr-ci-monitor:20260819-190000 |
… bound TTLAggregationAlgorithm.cpp reads ttl_group_by_unsorted_max_bytes_before_external_group_by, but the file only included Core/Settings.h. Neither the MergeTreeSettingsUInt64 alias nor the MergeTreeSettings subscript operator was visible, so the extern declaration did not parse and the subscript at the call site was rejected. Both are generated by MergeTreeSettings.h, which the sibling TTLResortUtils.cpp already includes for the same reason. Adding it in the .cpp keeps Settings.h and MergeTreeSettings.h out of headers, which the settings style check requires. The clang-tidy init-variables report on the same declaration was downstream of the unparsed type and goes away with the include. The query level max_bytes_before_external_group_by and max_bytes_ratio_before_external_group_by externs are dropped: the per-table setting replaced both, leaving no consumer in this translation unit. The style check rejects the wording "new analyzer", since the analyzer has been enabled by default since 24.3. Drop the adjective in the TTLResortUtils.cpp comment.
|
Fixed the CI break from
Verified both directions: removing the include reproduces the two Also dropped the now-unused |
…History ttl_group_by_unsorted_max_bytes_before_external_group_by was added to MergeTreeSettings without a corresponding history entry, so 03999_stateless_settings_history reported PLEASE ADD and the compatibility mechanism could not restore the pre-26.8 value for it. The recorded new_value equals the compiled default (268435456), which is what the test's value-drift arm compares against. Verified both directions on a debug build: with the entry the test output is empty (matches the reference); removing only this line and rebuilding reproduces the PLEASE ADD line verbatim.
|
Pushed
Control isolating the mechanism to the thresholds (ordinary query,
So making the new setting effective means enabling two-level conversion for this aggregator, which changes output order - the property this PR exists to protect - and the adjacent comment states an exactness guarantee. That is your design call on your lines, so I would rather ask than guess: enable two-level for the unsorted path, or drop the spill assertion and keep the setting as a documented no-op for now? Happy to implement whichever you pick. Note the two |
CI finish ledger - f14aa64Every failure below has an owner: a fixing PR (ours or external), or a full-effort fix task
The Coverage at this head is therefore 4 Session id: cron:our-pr-ci-monitor:20260819-233000 |
The `ttl_group_by_unsorted_max_bytes_before_external_group_by` bound was dead code: `Aggregator` can spill only a two-level hash table, and `TTLAggregationAlgorithm` passed both two-level thresholds as 0, so the table never converted and no spill site was reachable. Worse, if a spill had ever happened, `finalizeAggregates` used `convertToChunks` on the in-memory state only, so the spilled data would have been silently dropped. Now the two-level conversion threshold is tied to the external bound (and the bound applies only to the unsorted path - the sorted path flushes per key run and never outgrows one group), and `finalizeAggregates` merges the spilled generations back bucket by bucket via `Aggregator::mergeBlocks`, flushing the in-memory remainder to disk first, so the merge-back memory high-water mark is one bucket rather than the whole part - the same scheme the external aggregation of a query uses. Also fix the `t_unsorted_group_by_spill` scenario in `04327_ttl_group_by_set_sort_key_resort`: its DDL declared `GROUP BY b` under `ORDER BY a`, which `TTLDescription::getTTLFromAST` rejects with `BAD_TTL_EXPRESSION` before anything runs. The valid shape that still forces the later TTL onto the unsorted path is an earlier `SET a = max(b)` rewriting the key itself; the spill assertion is now genuinely satisfiable (verified locally: 200000 distinct keys at a 1-byte bound produce 25 spill generations and exactly preserve count, key set and sums; the written part passes CHECK TABLE and is sorted). CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=108550&sha=f14aa64c51964620cd0cd6f2d473fa6c99081514&name_0=PR&name_1=Fast%20test PR: ClickHouse#108550 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🕵 Answering the
Verified locally on this head: |
|
Verified both halves at Bucket-wise merge-back preserves rows. The decisive check is spilled-vs-not on the same input: The bound is live, not just the spill sites. 400000 keys with a 200-byte payload: merge peak memory goes 738.27 MiB at Scoping the bound to the unsorted path holds. Measured rather than assumed, since it is the load-bearing assumption: with the bound at 1, the sorted path emits 0 spill generations even at 400000 distinct keys. Holding the row count and output width fixed and varying only the key count 1000 to 20000, peak memory goes 4.08 MiB to 8.19 MiB, i.e. it tracks the output rows and not the hash table, which is what flushing per key run predicts. All five tests on the branch pass against this head ( Two gaps in the PR description, which I will fix:
|
CI finish ledger - 7c406d6Every failure below has an owner: a fixing PR (mine or external), or a full-effort fix task
Detail on that row:
Session id: cron:our-pr-ci-monitor:20260823-050000 |
…oup-by-set-sort-key-resort
… limit The `CH Inc sync` job reported `04327_ttl_group_by_set_sort_key_resort` as `FAIL` with `Test runs too long` in `Stateless tests (amd_asan_ubsan, flaky check, s3 storage, meta in keeper)`: 181.58 s and 180.44 s against the 180 s limit. The file had grown to 19 independent scenarios, each doing its own `CREATE`/`INSERT`/`OPTIMIZE FINAL`/`DROP`, and with metadata in Keeper and data on S3 the per-DDL round trips dominate the run time (the whole file takes about 8 s locally, so that configuration inflates it roughly 22x). Split into three files with nothing dropped and no scenario weakened: - `04327_ttl_group_by_set_sort_key_resort` keeps the plain sorting-key cases (`Float64`, `String`, `LowCardinality`, subcolumn, nested-wrapper coercion, the `MATERIALIZE TTL` mutation path, the skip-index rebuild, and the non-sort-key control); - `05044_ttl_group_by_set_materialized_resort` holds the cases where the sorting key is a `MATERIALIZED` column that must be recomputed after the `SET` (direct, mutation, transitive chain, tuple chain, tuple subcolumn source, and the `EPHEMERAL`-sourced column); - `05045_ttl_group_by_set_future_and_folded` holds the non-firing future-TTL cases, the const-folded recompute cases, and the unsorted `GROUP BY` spill case. The 48 reference lines are partitioned 25/13/10, matching the local split of the run time (about 39/21/30 percent), so the slowest part should land near 70 s in that configuration. Verified locally on an aarch64 release build: all three files pass, as do `04511_ttl_multi_group_by_set_rewrites_key`, `04661_ttl_group_by_set_resort_external_sort_spill`, `04691_ttl_multi_group_by_set_materialized_recompute`, `04692_ttl_multi_group_by_set_chained_fire`, `03545_number_of_rows_in_ttltransform` and `04044_mutation_ephemeral_materialized`.
|
🕵 Pushed
Verified locally on an aarch64 release build: the three split files pass, as do The AI review's remaining Major is the reverse-direction |
The master merge 589b55e rolled the version to 26.9, so the records for `ttl_resort_max_bytes_before_external_sort` and `ttl_group_by_unsorted_max_bytes_before_external_group_by` were left in the 26.8 block of a release that never had them. The `settings_changes_history` style check rejects that, and `compatibility` needs the same thing: both settings first appear in 26.9, so `compatibility = '26.8'` has to hand out the old value 0. The reason text goes from pre-26.8 to pre-26.9 for the same reason. Recorded values are unchanged, so the newest recorded value per setting is still 268435456 and `03999_stateless_settings_history`, which compares the compiled default against `argMax(new_value, (version, index))`, is unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Pushed Values are unchanged (268435456), so the newest recorded value per setting is the same and |
…oup-by-set-sort-key-resort
… new master test `master` added `05044_authentication_method_grants_execute_as_source_rights` in the same merge, so the split files move to `05046`/`05047`.
|
🕵 Pushed
Verified locally (aarch64 release, incremental build clean): all seven PR tests plus the neighbours pass — No unresolved review threads on the PR. The one standing AI-review finding (the reverse-direction |
…SET path A MATERIALIZED column may be defined over an ALIAS column, e.g. `a Int32 ALIAS x + 100, m Int32 MATERIALIZED a + 1`. The four places that analyze such a default here fed it to TreeRewriter against getAllPhysical() plus the EPHEMERAL columns, which by construction excludes ALIAS columns, so `a` could not be resolved and a firing `TTL ... GROUP BY ... SET` failed the merge with UNKNOWN_IDENTIFIER. The mutation path stops with the same error wrapped in Code 341 and then retries forever. Neither needs the alias-backed column to have anything to do with the SET: the dependency map is built over every MATERIALIZED column in the table. An ALIAS is computed on read and never stored, so resolving the name would not help either - the recompute stage would demand a column no part holds. The reference has to be replaced by the expression it stands for, cast to the alias type, which is what MaterializedColumnDependencies::findNode already does for the UPDATE mutation path (04869_materialized_over_alias_column_mutation). Measured on 04691 with the pre-fix and post-fix binaries: the new F3 arm stops the merge with `Missing columns: 'a'` before the fix and reproduces the reference after it. A master binary merges the same table and returns a stale `m`, which the recompute now corrects (109 -> m = 110). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI finish ledger - 98b5743Every failure below has an owner: a fixing PR (ours or external), or a full-effort fix task
The causal link to #115343 was verified rather than assumed: its diff edits this exact test function at Nothing else is red at this commit: Session id: cron:our-pr-ci-monitor:20260828-070000 |
Closes: #108514
Related: #110098
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Fixed wrong results and a
Sort order of blocks violatedlogical error forMergeTreetables that useTTL ... GROUP BY ... SET. Assigning a column the sorting key depends on no longer writes a part whose primary index disagrees with the data; everyMATERIALIZEDcolumn that reads an assigned column is now recomputed instead of keeping its pre-SETvalue (so skip indices and projections over it are no longer built from stale data); and a table with severalGROUP BYTTLs no longer loses or fragments groups when an earlierSETrewrites a later TTL'sGROUP BYkey or expiry input. Both new paths are bounded by newMergeTreesettings, each spilling to disk past its threshold instead of holding the whole part in memory:ttl_resort_max_bytes_before_external_sort(default 256 MiB) for the repair sort, andttl_group_by_unsorted_max_bytes_before_external_group_by(default 256 MiB) for the hash aggregation a laterGROUP BYTTL performs when an earlierSETrewrote its grouping key.Description
Found via the AST fuzzer / stress tests (
Stress test (arm_debug),AST fuzzer (amd_debug, targeted, old_compatibility)), recurring under STID3413-350b. Example CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=100173&sha=5cfd0f5aeabe1f357125fd015ebbaaf90717b30e&name_0=PR&name_1=Stress%20test%20%28arm_debug%29Reproducer (master, any build with assertions aborts; release writes a corrupt part):
Root cause.
TTLAggregationAlgorithm::finalizeAggregatesemits aggregated groups in the input (already-sorted) order. The merge writer trusts the stream order: it reads the primary-key columns by name (including the materialized sort-key expression column such astoStartOfDay(ts), which still holds its pre-SETvalue) and writes them without re-sorting. When theSETclause rewrites a sort-key column, the produced part is therefore no longer ordered by the sorting key. A debug build catches this with the merge'sCheckSortedTransformand aborts; a release build silently writes a part whose primary index does not match the data, which can return wrong results for primary-key-filtered queries.Fix. After the TTL step, when a
GROUP BYTTL assigns a column the sorting key depends on, recompute the sort-key expression columns from the post-SETvalues (overwriting the now-stale materialized ones) and re-sort by the sorting key. This is done in both the merge pipeline (MergeTask) and the mutation pipeline (MutateTask, e.g.ALTER TABLE ... MATERIALIZE TTL), where the rebuilt primary index and the skip-index expressions must see the same order. Every repair is gated on theSETtargets of theGROUP BYTTLs that can actually fire in the part (getFiringGroupByTTLSetTargets), so it is a no-op for every other merge, and a not-yet-expiredGROUP BY ... SETon the sorting key does not cost a whole-part re-sort.The re-sort of a whole part must not be unbounded in a background merge, so it is bounded by the new
MergeTreesettingttl_resort_max_bytes_before_external_sort(default 256 MiB,0disables spilling). Background contexts inheritmax_bytes_before_external_sort = 0, which would disable spilling entirely;buildTTLResortSortingSettingsapplies the table-level threshold and zeroes the query-memory gate derived frommax_bytes_ratio_before_external_sort, so the threshold is the real bound. The temporary-data scope comes from the global context (getSharedTempDataOnDisk), which background merges and mutations have.Two further correctness gaps of
TTL ... GROUP BY ... SET, both pre-existing on master, are fixed here as well, because the first shares theMATERIALIZEDdependency analysis this change introduces and the second would otherwise be worsened by the repair sort — the shape stops failing with the loudSort order of blocks violatedand starts silently producing wrong aggregates instead:Stale
MATERIALIZEDcolumns.TTLAggregationAlgorithmcarries every column that is not aGROUP BYkey or aSETtarget forward asany(...), so aMATERIALIZEDcolumn keeps its pre-SETvalue even though its source was rewritten (d Date MATERIALIZED toDate(ts)with... SET ts = ...). The stored value, and any skip index or projection rebuilt from it, was written stale — aminmaxindex overdthen pruned away rows that a brute-force scan returns. EveryMATERIALIZEDcolumn that transitively reads a firingSETtarget is now recomputed from its default expression on both the merge and the mutation path, matching whatALTER TABLE ... UPDATEalready does throughcolumn_to_affected_materialized. AMATERIALIZEDcolumn that mixes anEPHEMERALinput with aSETtarget cannot be recomputed during a merge (ephemeral columns are never stored), so each one is reported with a warning instead of silently written stale, mirroringMutationsInterpreter::prepare.Several
GROUP BYTTLs in one table.TTLTransformruns eachTTLAggregationAlgorithmsequentially on the same block, and each assumes its input is contiguous in its ownGROUP BYkeys. An earlierSETthat rewrites a later TTL's key (TTL ts1 + 1d GROUP BY k SET k = max(v), ts2 + 1d GROUP BY k SET payload = sum(payload)) split one logical group into several partial aggregates, and a derived key or expiry input (a computed key, a subcolumn, or aMATERIALIZEDcolumn) was even read at its pre-SETvalue. Such a TTL is now detected by mapping its keys and expiry inputs back to their physical storage columns (groupByKeysAffectedByEarlierSet,groupByTTLExpiryAffectedByEarlierSet), told to aggregate the whole stream instead of taking the streaming flush-on-key-change path, and its stale derived columns are refreshed before it runs (buildRefreshGroupByKeysDAG). The lost-order state cascades, since finalizing an unsorted aggregation emits groups in hash-table order.That unsorted path holds every expired key of the part at once, so it is bounded by the new
ttl_group_by_unsorted_max_bytes_before_external_group_by. Making the bound effective exposed a second defect in the same code:Aggregatorcan only spill a two-level hash table, so with both two-level thresholds at 0 the bound was dead letter, and had it ever fired,finalizeAggregatesusedconvertToChunkson the in-memory state alone and would have silently dropped the spilled groups from the written part.group_by_two_level_threshold_bytesis now tied to the bound so the spill sites are reachable, andfinalizeAggregatesflushes the in-memory remainder and merges the spilled generations back bucket by bucket viaAggregator::mergeBlocks, keeping the merge-back high-water mark at one bucket. Both halves are new in this pull request, so no release ever had a live spill here.Tests.
03545_number_of_rows_in_ttltransform, which intentionally exercisesSETon sort-key columns while the output stays ordered, passes unchanged. New:04327_ttl_group_by_set_sort_key_resort—Float64,StringandLowCardinality(String)sort keys with a non-monotonicSET, subcolumn and wrapped-subcolumn sorting keys, theMATERIALIZE TTLmutation path with skip-index rebuild,MATERIALIZEDsort keys (direct, chained and over a subcolumn), the mixed-EPHEMERALshape, and a control case that only sets a non-sort-key column. Each asserts the part is physically ordered by the sorting key.04511_ttl_multi_group_by_set_rewrites_key— severalGROUP BYTTLs whose keys and expiry inputs are rewritten by an earlierSET, including derived keys, the cascade, the not-yet-expired case that must keep the fast path, and stale non-sort-keyMATERIALIZEDcolumns with aminmaxindex over them.04661_ttl_group_by_set_resort_external_sort_spill— forcesttl_resort_max_bytes_before_external_sort = 1and asserts viaExternalSortWritePartinsystem.part_logthat both the merge and theMATERIALIZE TTLpath really spill and still produce a correct, physically sorted part.04327_ttl_group_by_set_sort_key_resortalso forcesttl_group_by_unsorted_max_bytes_before_external_group_by = 1and asserts viaExternalAggregationWritePartthat the unsorted aggregation really spills while the group count is preserved.Workflow [PR]
Sync PR [sync-upstream/pr/108550]