skip to content

In ClickHouse, how do you choose the column order for a MergeTree ORDER BY key?

level: seniorimportance: should knowfreq 58%

answer

  1. start from the WHERE clauses you actually serve
  2. only the front of the key prunes
  3. runs of equal values are worth money
  4. cardinality is the tie-break, not the rule
  5. a unique column at the front is the trap

basics

~20 s

Lead with the column almost every query filters on, then order the rest so that low-cardinality columns come before high-cardinality ones. Only a leading prefix of the key prunes granules, so the first column decides how much data a typical query reads.

solid answer

~50 s

Start from the query mix, not from the schema. The leading column should be the predicate that appears in nearly every query — usually a tenant, customer or date column — because the sparse index only prunes on a leading run of key columns. After that, prefer lower cardinality before higher: a low-cardinality column keeps long runs of equal values, which both narrows granule ranges effectively and compresses far better, while a high-cardinality column placed first makes every granule cover a distinct sliver and gives later columns nothing to work with. Keep the key short — three or four columns is typical — since trailing columns rarely prune and still cost index size and sort work on insert. If two query shapes want incompatible leading columns, the honest answers are a data-skipping index or a second copy of the table sorted the other way, not a compromise key that serves neither.

code

sql · 5 lines
sql
-- good: universal predicate first, ascending cardinality, short key
ORDER BY (tenant_id, event_date, user_id)

-- bad: near-unique column first, nothing prunes, compression collapses
ORDER BY (request_id, tenant_id, event_date)

go deeper

for a junior

Recall that the first column of ORDER BY matters most and should be something nearly every query filters on, and that unique identifiers do not belong at the front.

for a middle

Explain both effects of the ordering — prefix-only pruning and run-length-driven compression — and why ascending cardinality after the leading column is the usual guidance.

for a senior

Derive the key from the real query mix, measure it with EXPLAIN indexes = 1 and compressed size per column, and know the escape hatches when one key cannot serve two workloads.

for a principal

Treat the key as an irreversible commitment: it fixes access paths, storage cost and rebuild risk for the table's life. Decide when a second sorted copy is worth its ingest and consistency cost.

## The rule that drives everything The sparse primary index prunes only on a **leading run** of sorting-key columns. With `ORDER BY (a, b, c)`, a filter on `a` prunes well, `a` and `b` prune better, and `c` alone prunes not at all — `c` is ordered only within equal `(a, b)`, so every granule's range for `c` spans the whole domain. Column order is therefore not a stylistic choice; it is the access-path design for the table, and it is close to permanent, since changing a leading column requires rebuilding the table and swapping it in. ## Step 1 — find the universal predicate List the queries the table actually serves and find the column that appears in the `WHERE` clause of nearly all of them. In multi-tenant analytics this is almost always the tenant or account id; in event pipelines it is often a coarse time column; in device telemetry it is the device or site id. That column goes first. If no column is present in most queries, that is a signal the table is serving two workloads and may need two differently-sorted copies. A useful sanity check is that the leading column must be *selective enough to matter*. A boolean or a two-valued status column at the front splits the table into two ranges and prunes essentially nothing, while forcing every other column one position deeper. ## Step 2 — order the rest low-cardinality first After the leading column, the conventional guidance is ascending cardinality. Two independent effects push the same way: - **Pruning.** Within a granule range already narrowed by earlier columns, a low-cardinality column produces long runs of equal values, so its bounds in successive index entries stay tight and further narrow the search. A high-cardinality column such as a UUID or a raw nanosecond timestamp changes on nearly every row; placing it early makes each granule's range effectively unique and destroys the runs the later columns would have relied on. - **Compression.** Column files are compressed in blocks over physically adjacent rows. Sorting by a low-cardinality column first produces long identical runs, which the general-purpose compressor and specialised codecs (`Delta`, `DoubleDelta`, `Gorilla`, `T64`, `LowCardinality` dictionary encoding) exploit heavily. The same table with a high-cardinality leading column can occupy several times the disk and read several times the bytes for the same query. The guidance is a heuristic, not a law: if a medium-cardinality column is filtered in 90% of queries and a lower-cardinality one in 10%, the filtered-more-often column wins the earlier position. Query frequency outranks cardinality; cardinality is the tie-break. ## Step 3 — keep it short Each additional key column costs sort work on every insert, widens the in-memory primary index, and usually contributes no pruning because no query filters that deep. Three or four columns is the common shape. Where a column must be in the sort order for collapsing correctness — `ReplacingMergeTree` or `AggregatingMergeTree` identity — but is never filtered on, put it at the end of `ORDER BY` and leave it out of an explicitly declared `PRIMARY KEY`, so it costs sort work but not index size. ```sql -- serves: tenant filters, tenant + day ranges, tenant + day + user drill-down CREATE TABLE events ( tenant_id UInt32, event_date Date, user_id UInt64, request_id UUID, payload String ) ENGINE = MergeTree PARTITION BY toYYYYMM(event_date) ORDER BY (tenant_id, event_date, user_id); ``` Putting `request_id` — a UUID, effectively unique — anywhere near the front of that key would be the textbook mistake: no query filters on it alone, and it would fragment every run of equal values behind it. ## When one key cannot serve the workload This happens often enough that having a stock answer matters. Options, in escalating cost: 1. **A data-skipping index** on the secondary predicate — cheap to add, but it only helps if the values of that column are physically clustered under the existing sort order. 2. **A second table sorted the other way**, populated from the same ingest path, with queries routed by shape. Costs storage and write amplification, and makes consistency your problem, but it genuinely gives two access paths. 3. **Accepting the scan** for the rarer query shape, if it runs infrequently and the table is small enough that a full scan is within budget. This is a legitimate engineering answer and interviewers like hearing it stated as a deliberate choice rather than an oversight. ## Verifying the choice Run the representative queries behind `EXPLAIN indexes = 1` and compare surviving granules against the table total; check compressed size per column in `system.parts_columns` before and after a candidate reordering on a sample table. A key reordering that halves compressed size and cuts granules read by an order of magnitude is measurable in minutes on a copy — far cheaper than discovering the mistake after the table has grown to terabytes. ## Interview traps Answering "put the primary key first" or "the most selective column first" without qualification both miss. Highest selectivity first is the OLTP B-tree instinct, and here it is often exactly wrong: a unique column at the front prunes precisely one granule per lookup and wrecks compression and every other query. The strong answer starts from the query mix, then applies ascending cardinality, then keeps the key short.

  • Why is putting the most selective column first often wrong in ClickHouse, unlike in a B-tree index?
    A B-tree addresses individual rows, so maximum selectivity pays off. ClickHouse's index is sparse and reads whole granules, so a near-unique leading column prunes to one granule at best while destroying the long runs of equal values that make later key columns prune and make column files compress. Frequency of use should decide the leading column.
  • How would you measure whether a different ORDER BY is actually better?
    Build the candidate table on a representative sample, insert the same data, then compare two things: compressed bytes per column from `system.parts_columns`, and granules surviving `EXPLAIN indexes = 1` for each representative query. Both are cheap to obtain and directly reflect the bytes a production query would read.
  • What do you do when two query shapes want different leading columns?
    Try a data-skipping index for the secondary predicate first — it is cheap and sometimes enough if the values happen to cluster. If not, maintain a second table with the alternative sort order fed by the same ingest path and route by query shape, accepting the storage and write cost. Deliberately accepting a scan for a rare shape is also a valid answer.

saying these in an interview costs you the question

  • Putting the highest-cardinality column first, as in a B-tree
  • Including a UUID or timestamp-with-nanoseconds early in the key
  • Adding every filtered column to ORDER BY to cover all queries
  • Assuming any position in the key gives pruning
  • Ignoring the compression impact of the sort order

context