Note

Partitioning by modulo in ClickHouse: the DDL I wrote that made queries 13.7 times slower

PARTITION BY supplier_id % 100 sounds like it spreads the load evenly. In practice it destroyed primary-key pruning: a lookup read 64% of the table instead of 3%. Here are the measurements, and why it happens.

2 min read · Markdown

The table and the query

A ReplacingMergeTree with 266 million rows of stock by seller, product, size and warehouse:

I wrote that DDL myself, by analogy with an older table, when we moved the report from PostgreSQL.

The service query asks for about 1,000 (seller, product) pairs at a time:

SELECT …
FROM stock FINAL
WHERE (supplier_id, nm_id) IN (…1,000 pairs…)

The requested sellers' rows were 9.6% of the table. The query read 64% of all granules (20,916 of 32,540) and 4.65 GiB, and took about 2.6 seconds with a warm cache.

Three variants, same data

Table Granules read Time Data read
PARTITION BY supplier_id % 100 20,916 of 32,540 (64%) ~2.6 s 4.65 GiB
No partitioning 972 of 31,729 (3.1%) 191 ms 189 MiB
PARTITION BY intDiv(supplier_id, 5000000) 1,758 of 32,036 (5.5%) 221 ms —

Without partitions the query was 13.7 times faster and read 25 times less.

The range partitioning did well too, but the divisor was badly chosen: seller IDs are skewed, and 93% of rows landed in partition 0. Something like intDiv(supplier_id, 100000) would spread them better.

Even the access pattern partitioning was supposed to help, one seller at a time (2.7 million rows), lost: 326 granules and 57–71 ms without partitions, against 337–446 granules and 75–129 ms with the modulo. The primary-key prefix already narrows the read to one seller; the partition only adds fragmentation.

Why modulo breaks pruning

ClickHouse skips granules using the [min, max] range of the key columns in each granule (KeyCondition). With supplier_id % 100:

A thousand requested pairs then intersect the ranges of almost every granule. The index check knows ranges, not residues; it cannot tell that seller 1,234,567 is absent from a granule spanning sellers 1,200,000 to 1,300,000.

It is not a planner bug, and it is not FINAL: both were ruled out by measurement.

What I do now


All writing · A similar problem? contact@ivanblinov.com