Skip to content

Keep the part sorted when TTL GROUP BY ... SET rewrites a sorting key column - #119863

Open
alexey-milovidov wants to merge 2 commits into
masterfrom
fix-ttl-group-by-set-sort-order
Open

Keep the part sorted when TTL GROUP BY ... SET rewrites a sorting key column#119863
alexey-milovidov wants to merge 2 commits into
masterfrom
fix-ttl-group-by-set-sort-order

Conversation

@alexey-milovidov

@alexey-milovidov alexey-milovidov commented Sep 13, 2026

Copy link
Copy Markdown
Member

TTL ... GROUP BY ... SET emits the aggregated rows at the position of their group in the merged stream, but a SET expression may assign arbitrary values to a column of the sorting key (for example ORDER BY (k, toStartOfDay(ts)) with SET k = max(v)), so an aggregated row could be less than the previously emitted one. The merge wrote such a part as is: a debug build threw the logical error Sort order of blocks violated in CheckSortedTransform, and a release build silently produced a part whose primary index does not match the data.

Now TTLAggregationAlgorithm tracks the sorting key of the last emitted row. If the next row violates the order, its sorting key is replaced with the sorting key of the previous row, so the output stays sorted and the primary index stays consistent with the data. This is a simpler alternative to #108550, which re-sorts the output of the TTL step.

Closes: #108514
Related: #108550

Changelog category (leave one):

  • Bug Fix (user-visible misbehavior in an official stable release)

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

Fix the order of rows in a data part written by a merge with TTL ... GROUP BY ... SET when the SET clause assigns a new value to a column of the sorting key. Previously such a merge could produce a part whose primary index does not match the data (a logical error Sort order of blocks violated in debug builds). Now the sorting key of an out-of-order row is replaced with the sorting key of the previous row. Closes #108514.


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

alexey-milovidov and others added 2 commits September 13, 2026 22:18
… column

`TTL ... GROUP BY ... SET` emits the aggregated rows at the position of their group in the merged stream,
but a `SET` expression may assign arbitrary values to a column of the sorting key, so an aggregated row
could be less than the previously emitted one. The merge wrote such a part as is: a debug build threw
`Sort order of blocks violated` in `CheckSortedTransform`, and a release build produced a part whose
primary index does not match the data.

Now `TTLAggregationAlgorithm` tracks the sorting key of the last emitted row. If the next row violates
the order, its sorting key is replaced with the key of the previous row, so the output stays sorted
and the primary index stays consistent with the data.

Closes: #108514

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… on the number of merges

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

clickhouse-gh Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Workflow [PR], commit [aa22ea9]

Summary:

job_name test_name status info comment
AST fuzzer (amd_debug, targeted, old_compatibility) FAIL
Logical error: Sort order of blocks violated for column number A, left: B, right: C. Chunk D, rows read E.F (STID: 3413-350b) FAIL cidb
Stateless tests (amd_debug, flaky check) FAIL
Server liveness check failed FAIL cidb
Logical error: Sort order of blocks violated for column number A, left: B, right: C. Chunk D, rows read E.F (STID: 3413-350b) FAIL cidb
Stateless tests (amd_debug, parallel) FAIL
Server liveness check failed FAIL cidb
Logical error: Sort order of blocks violated for column number A, left: B, right: C. Chunk D, rows read E.F (STID: 3413-350b) FAIL cidb
Finish Workflow FAIL
python3 ./ci/jobs/scripts/workflow_hooks/new_tests_check.py FAIL
Fast test (arm_darwin) DROPPED
Build (amd_darwin) DROPPED
Build (arm_darwin) DROPPED
Build (arm_v80compat) DROPPED
Build (amd_freebsd) DROPPED
Build (ppc64le) DROPPED

AI Review

Summary

This PR tries to repair TTL ... GROUP BY ... SET sort-order corruption by clamping an out-of-order row's sorting key to the previous emitted key instead of re-sorting. The current implementation still leaves expression-based ORDER BY keys inconsistent with the stored row data, and it does not cover the MATERIALIZE TTL mutation path when the sorting key is longer than the primary key, so it does not close the original correctness bug yet.

Findings

❌ Blockers

  • [src/Processors/TTL/TTLAggregationAlgorithm.cpp:352-404] restoreSortOrder operates on the precomputed sorting-key helper columns already present in the block. For expression keys such as toStartOfDay(ts), finalizeAggregates leaves those helper columns at their pre-SET values, and this code copies the stale values into the repaired stream. MergeTreeDataPartWriter then rebuilds the primary index from those helper columns while the persisted row keeps the new base-column values, so expression-based keys can still leave the part/index contract broken. Suggested fix: recompute sorting-key expressions from the post-SET row before repairing or re-sorting the output.
  • [src/Processors/TTL/TTLAggregationAlgorithm.cpp:94-96] The mutation path still checks only a prefix of the sorting key. MutateTask materializes primary-key and skip-index expressions before TTLTransform, not the full sorting key, so a table like PRIMARY KEY k ORDER BY (k, toStartOfDay(ts)) reaches this break with only k available. restoreSortOrder then misses reorderings inside one k, so ALTER TABLE ... MATERIALIZE TTL can still write an invalid part. Suggested fix: materialize the full sorting key on mutations too, or reject this shape instead of silently checking only the prefix.
Tests
  • The new stateless test checks query output after ORDER BY ALL, so it does not validate the hidden sorting-key expression columns that feed the primary index, and it does not exercise the MATERIALIZE TTL path. A focused test should assert the physical sort-key behavior for an expression key and for ALTER TABLE ... MATERIALIZE TTL.
Final Verdict

Changes requested.

@clickhouse-gh clickhouse-gh Bot added the pr-bugfix Pull request with bugfix, not backported by default label Sep 13, 2026

@alexey-milovidov alexey-milovidov left a comment

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.

LGTM

@alexey-milovidov alexey-milovidov self-assigned this Sep 13, 2026

Columns sort_key;
for (const auto & elem : sort_description)
sort_key.push_back(block.getByName(elem.column_name).column->convertToFullColumnIfSparse());

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.

MergeTask materializes the ORDER BY expression columns before TTL, so here we are comparing and rewriting those helper columns, not the sort key that will be recomputed from the stored row later. In the ORDER BY (k, toStartOfDay(ts)) shape from the issue, finalizeAggregates keeps toStartOfDay(ts) at its pre-SET value even after ts is reassigned, and this code just copies that stale helper value forward. MergeTreeDataPartWriter then rebuilds the primary index from the helper columns while the persisted row keeps the new ts, so expression keys can still leave the part/index contract broken. I think the sort-key expressions need to be recomputed from the post-SET row before any order repair happens.

for (size_t i = 0; i < sorting_key.column_names.size(); ++i)
{
/// The header of a mutation may lack the sorting key expressions that are not in the primary key.
if (!header.has(sorting_key.column_names[i]))

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 drops the fix on the MATERIALIZE TTL path whenever the sorting key is longer than the primary key. MutateTask only materializes getPrimaryKeyAndSkipIndicesExpression() before TTLTransform, so for a table like PRIMARY KEY k ORDER BY (k, toStartOfDay(ts)) the header has k but not toStartOfDay(ts). Breaking here means restoreSortOrder silently checks only the k prefix, so a SET ts = ... that reorders rows inside one k still writes an invalid part during ALTER TABLE ... MATERIALIZE TTL. If we cannot build the full sorting key on mutations, this shape needs to be rejected instead of partially checked.

@clickhouse-gh clickhouse-gh Bot added the comp-ttl TTL rules and TTL merges (moving/recompressing/rolling-up/deleting parts). label Sep 13, 2026
@clickhouse-gh

clickhouse-gh Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Build profile diff (arm_release)

Comparing aa22ea9de with master 0c3b387da (stripped binary size, per-symbol sizes and ThinLTO time; object sizes against the warmup build of 9fcf3dbb8; 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

4 object files changed (+23.09 KiB total), 0 added.

Object file Master PR Δ
src/CMakeFiles/dbms.dir/Processors/TTL/TTLAggregationAlgorithm.cpp.o 228.27 KiB 248.73 KiB +20.45 KiB (+8.96%)

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

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

Job report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp-ttl TTL rules and TTL merges (moving/recompressing/rolling-up/deleting parts). pr-bugfix Pull request with bugfix, not backported by default

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TTL GROUP BY + SET on a sort-key column corrupts merge output (Sort order of blocks violated; silent primary-key index corruption in release builds)

1 participant