Skip to content
About
Main content

Physical clustering: which units can we skip?

How do we arrange rows so that whole units can be skipped without reading them?

Fewer units, not less waste per unit

Layout (Layout models) reduced the waste inside a unit you read. (also called physical placement) reduces the number of units you read at all. This sounds like the same thing said twice. It is not. The two have very different risk profiles, and that difference is what this article is about.

  • Layout cannot fail. A columnar file is better than a row file for a query that reads 3 of 100 , always. You can compute the gain in advance and bound it: n/k times the ratio, maybe 100×, and never zero.
  • Clustering can fail completely. A table that is perfectly clustered on one column, queried with a filter on a different column, reads every byte, exactly as if you had done nothing. And you still pay the maintenance cost.

A hard constraint appears, and it is geometric, close to a theorem:

Sort keys, Z-order, , and (all explained below) are different negotiations with this one fact.

What stops mattering in this article is byte . Clustering does not care what is inside a unit. It cares only about the distribution of values across units. The working resolution jumps from bytes to units. And the unit size G (rows per unit) was fixed earlier, in Data at rest and the one ratio (about 128 MB), so here it arrives as a parameter you inherited, not one you choose. (Access and media shows where it really comes from.)

One more point reframes everything below. Data skipping is not a storage technique. It is an inference technique. You skip a unit because you can prove that it holds no qualifying row. The proof rests on a small summary of the unit (for example its smallest and largest value). How strong the summary is depends on clustering. The chain is:

clustering → tight summaries → provable exclusion → skipped I/O

Clustering has no direct benefit. It buys nothing you can read. It buys the tightness of a summary, and the summary buys the skip.

The governing question of this article:


The core arithmetic: clustering payoff

Let:

  • N = number of rows,
  • G = rows per unit (here a unit is a file or a ),
  • U = N/G = number of units,
  • s = of the query (the fraction of rows it wants).

Perfectly clustered on the filter column, the query reads max(1, s·U) units. It needs at least one unit, and otherwise only the fraction s of all units.

Randomly placed, each unit contains at least one matching row with probability 1 − (1−s)^G ≈ s·G (when s·G is small). So the query must read about U · min(1, s·G) units, if it has a summary that can tell which units have matches.

Divide the two:

rows per unit (a file or a row group)
number of units, N/G
the fraction of rows the query wants
G = 10⁶ rows per unitG = 10⁴
Data table
Clustering payoff by selectivity
Selectivity sseriesUnits read: random ÷ clustered
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit1,000,000×
0G = 10⁶ rows per unit794,000×
0G = 10⁶ rows per unit631,000×
0G = 10⁶ rows per unit501,000×
0G = 10⁶ rows per unit398,000×
0G = 10⁶ rows per unit316,000×
0G = 10⁶ rows per unit251,000×
0G = 10⁶ rows per unit200,000×
0G = 10⁶ rows per unit158,000×
0G = 10⁶ rows per unit126,000×
0G = 10⁶ rows per unit100,000×
0G = 10⁶ rows per unit79,400×
0G = 10⁶ rows per unit63,100×
0G = 10⁶ rows per unit50,100×
0G = 10⁶ rows per unit39,800×
0G = 10⁶ rows per unit31,600×
0G = 10⁶ rows per unit25,100×
0G = 10⁶ rows per unit20,000×
0G = 10⁶ rows per unit15,800×
0G = 10⁶ rows per unit12,600×
0G = 10⁶ rows per unit10,000×
0G = 10⁶ rows per unit7,940×
0G = 10⁶ rows per unit6,310×
0G = 10⁶ rows per unit5,010×
0G = 10⁶ rows per unit3,980×
0G = 10⁶ rows per unit3,160×
0G = 10⁶ rows per unit2,510×
0.001G = 10⁶ rows per unit2,000×
0.001G = 10⁶ rows per unit1,580×
0.001G = 10⁶ rows per unit1,260×
0.001G = 10⁶ rows per unit1,000×
0.001G = 10⁶ rows per unit794×
0.002G = 10⁶ rows per unit631×
0.002G = 10⁶ rows per unit501×
0.003G = 10⁶ rows per unit398×
0.003G = 10⁶ rows per unit316×
0.004G = 10⁶ rows per unit251×
0.005G = 10⁶ rows per unit200×
0.006G = 10⁶ rows per unit158×
0.008G = 10⁶ rows per unit126×
0.01G = 10⁶ rows per unit100×
0.013G = 10⁶ rows per unit79.4×
0.016G = 10⁶ rows per unit63.1×
0.02G = 10⁶ rows per unit50.1×
0.025G = 10⁶ rows per unit39.8×
0.032G = 10⁶ rows per unit31.6×
0.04G = 10⁶ rows per unit25.1×
0.05G = 10⁶ rows per unit20×
0.063G = 10⁶ rows per unit15.8×
0.079G = 10⁶ rows per unit12.6×
0.1G = 10⁶ rows per unit10×
0.126G = 10⁶ rows per unit7.94×
0.158G = 10⁶ rows per unit6.31×
0.2G = 10⁶ rows per unit5.01×
0.251G = 10⁶ rows per unit3.98×
0.316G = 10⁶ rows per unit3.16×
0.398G = 10⁶ rows per unit2.51×
0.501G = 10⁶ rows per unit2×
0.631G = 10⁶ rows per unit1.58×
0.794G = 10⁶ rows per unit1.26×
1G = 10⁶ rows per unit1×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴10,000×
0G = 10⁴7,940×
0G = 10⁴6,310×
0G = 10⁴5,010×
0G = 10⁴3,980×
0G = 10⁴3,160×
0G = 10⁴2,510×
0.001G = 10⁴2,000×
0.001G = 10⁴1,580×
0.001G = 10⁴1,260×
0.001G = 10⁴1,000×
0.001G = 10⁴794×
0.002G = 10⁴631×
0.002G = 10⁴501×
0.003G = 10⁴398×
0.003G = 10⁴316×
0.004G = 10⁴251×
0.005G = 10⁴200×
0.006G = 10⁴158×
0.008G = 10⁴126×
0.01G = 10⁴100×
0.013G = 10⁴79.4×
0.016G = 10⁴63.1×
0.02G = 10⁴50.1×
0.025G = 10⁴39.8×
0.032G = 10⁴31.6×
0.04G = 10⁴25.1×
0.05G = 10⁴20×
0.063G = 10⁴15.8×
0.079G = 10⁴12.6×
0.1G = 10⁴10×
0.126G = 10⁴7.94×
0.158G = 10⁴6.31×
0.2G = 10⁴5.01×
0.251G = 10⁴3.98×
0.316G = 10⁴3.16×
0.398G = 10⁴2.51×
0.501G = 10⁴2×
0.631G = 10⁴1.58×
0.794G = 10⁴1.26×
1G = 10⁴1×

(This holds when the query wants at least one unit's worth of rows, that is s ≥ 1/U. Below that, the clustered query still reads one whole unit, and the gain is capped by that floor of one unit.)

Two readings, both worth remembering:

  1. The maximum value clustering can deliver is the unit size in rows. With G = 10⁶ rows, clustering is worth up to a million-fold and not one unit more, however good your sort key is.
  2. It is also capped by the query's own selectivity. A query that returns half of the table cannot be helped by more than 2×, because you cannot skip what you need.

Both caps bind in practice from opposite ends. Queries with s near 1 do not need clustering. Queries with s below 1/N do not exist (they would return less than one row). Clustering is transformative for selective queries against large units.

Notice the perverse direction: the larger your unit, the more clustering is worth, and the more it costs to maintain. G went up when data moved to . That is exactly why clustering became a first-class concern in the "" era (data lakes with table formats on top) and not just a tuning detail.

Same data, same query, same hardware, three orders of magnitude of difference. And the bad case shows up only as "the query is slow". There is no error, no warning and no metric that tells it apart from a query that really needed all the data. This is the in action: failures of this mechanism are silent.