Sort keys and the sparse primary index

ClickHouse · Practical · 14 min

The sort key is the single most important performance decision for a ClickHouse table.

A MergeTree table's data is sorted by its ORDER BY key and cut into granules of 8,192 rows (index_granularity). The primary index stores the key values of only the first row of each granule. That makes it sparse: a billion-row table needs about 122,000 index entries, small enough to sit in memory.

When a query filters on key columns, ClickHouse binary-searches this index to find which granules can contain matching rows and reads only those. Filtering on a column that is not a prefix of the key gives far less pruning, because those values are scattered across all granules.

Designing the key:

  • Lead with the columns that appear in most WHERE clauses, often a tenant or category, then time: ORDER BY (tenant_id, event_type, ts).
  • Order the key columns roughly by ascending cardinality. Low-cardinality first gives long runs of equal values, which helps pruning on later columns and compresses much better.
  • Avoid leading with a near-unique column like a UUID unless point lookups on it are the main use.
  • A query can only use the key efficiently from the left: with (a, b, c), filters on a or a, b prune well; a filter on c alone barely does.

EXPLAIN indexes = 1 shows how many granules each query selects, which is the quickest way to check a key design.

Example

CREATE TABLE logs (tenant UInt16, level LowCardinality(String), ts DateTime, msg String)
ENGINE = MergeTree ORDER BY (tenant, level, ts);

INSERT INTO logs SELECT number % 100, ['info','warn','error'][number % 3 + 1],
       now() - number, 'm' FROM numbers(2000000);

-- Filter on the key prefix: only a few granules are read
EXPLAIN indexes = 1 SELECT count() FROM logs WHERE tenant = 7 AND level = 'error';
--   PrimaryKey  Keys: tenant, level ... Granules: 3/245

-- Filter on ts alone (third key column): no pruning
EXPLAIN indexes = 1 SELECT count() FROM logs WHERE ts > now() - 60;
--   PrimaryKey  Keys: ts ... Granules: 245/245

Remember

  • The primary index has one entry per 8,192-row granule
  • Filters prune well only on a left prefix of ORDER BY
  • Order key columns from low to high cardinality, frequent filters first

Watch out for

  • Putting a timestamp or UUID first in ORDER BY, which makes filters on the other key columns scan everything.

Check yourself

  1. A table has ORDER BY (customer_id, ts). Which filter benefits least from the primary index?
  2. Why is ClickHouse's primary index called sparse?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: clickhouse.com/docs/engines/table-engines/mergetree-family/mergetree