Clustered versus non-clustered: the only lever on pages touched
Why do you get only one clustered index?
Recall the crossover from Why indexes exist: the index wins when k/N < s/r, where k counts pages, not rows. Clustering is the only mechanism that reduces pages without reducing rows.
Let f be rows per page. A range query returning k rows costs:
- rows the range query returns
- rows per page, typically 20 to 200
- rows in the table
Data table
| Rows returned k | series | Pages read |
|---|---|---|
| 100 | clustered (sequential) | 1 |
| 119 | clustered (sequential) | 1.19 |
| 141 | clustered (sequential) | 1.41 |
| 168 | clustered (sequential) | 1.68 |
| 200 | clustered (sequential) | 2 |
| 237 | clustered (sequential) | 2.37 |
| 282 | clustered (sequential) | 2.82 |
| 335 | clustered (sequential) | 3.35 |
| 398 | clustered (sequential) | 3.98 |
| 473 | clustered (sequential) | 4.73 |
| 562 | clustered (sequential) | 5.62 |
| 668 | clustered (sequential) | 6.68 |
| 794 | clustered (sequential) | 7.94 |
| 944 | clustered (sequential) | 9.44 |
| 1.12K | clustered (sequential) | 11.2 |
| 1.33K | clustered (sequential) | 13.3 |
| 1.58K | clustered (sequential) | 15.8 |
| 1.88K | clustered (sequential) | 18.8 |
| 2.24K | clustered (sequential) | 22.4 |
| 2.66K | clustered (sequential) | 26.6 |
| 3.16K | clustered (sequential) | 31.6 |
| 3.76K | clustered (sequential) | 37.6 |
| 4.47K | clustered (sequential) | 44.7 |
| 5.31K | clustered (sequential) | 53.1 |
| 6.31K | clustered (sequential) | 63.1 |
| 7.5K | clustered (sequential) | 75 |
| 8.91K | clustered (sequential) | 89.1 |
| 10.6K | clustered (sequential) | 106 |
| 12.6K | clustered (sequential) | 126 |
| 15K | clustered (sequential) | 150 |
| 17.8K | clustered (sequential) | 178 |
| 21.1K | clustered (sequential) | 211 |
| 25.1K | clustered (sequential) | 251 |
| 29.9K | clustered (sequential) | 299 |
| 35.5K | clustered (sequential) | 355 |
| 42.2K | clustered (sequential) | 422 |
| 50.1K | clustered (sequential) | 501 |
| 59.6K | clustered (sequential) | 596 |
| 70.8K | clustered (sequential) | 708 |
| 84.1K | clustered (sequential) | 841 |
| 100K | clustered (sequential) | 1K |
| 119K | clustered (sequential) | 1.19K |
| 141K | clustered (sequential) | 1.41K |
| 168K | clustered (sequential) | 1.68K |
| 200K | clustered (sequential) | 2K |
| 237K | clustered (sequential) | 2.37K |
| 282K | clustered (sequential) | 2.82K |
| 335K | clustered (sequential) | 3.35K |
| 398K | clustered (sequential) | 3.98K |
| 473K | clustered (sequential) | 4.73K |
| 562K | clustered (sequential) | 5.62K |
| 668K | clustered (sequential) | 6.68K |
| 794K | clustered (sequential) | 7.94K |
| 944K | clustered (sequential) | 9.44K |
| 1.12M | clustered (sequential) | 11.2K |
| 1.33M | clustered (sequential) | 13.3K |
| 1.58M | clustered (sequential) | 15.8K |
| 1.88M | clustered (sequential) | 18.8K |
| 2.24M | clustered (sequential) | 22.4K |
| 2.66M | clustered (sequential) | 26.6K |
| 3.16M | clustered (sequential) | 31.6K |
| 3.76M | clustered (sequential) | 37.6K |
| 4.47M | clustered (sequential) | 44.7K |
| 5.31M | clustered (sequential) | 53.1K |
| 6.31M | clustered (sequential) | 63.1K |
| 7.5M | clustered (sequential) | 75K |
| 8.91M | clustered (sequential) | 89.1K |
| 10.6M | clustered (sequential) | 106K |
| 12.6M | clustered (sequential) | 126K |
| 15M | clustered (sequential) | 150K |
| 17.8M | clustered (sequential) | 178K |
| 21.1M | clustered (sequential) | 211K |
| 25.1M | clustered (sequential) | 251K |
| 29.9M | clustered (sequential) | 299K |
| 35.5M | clustered (sequential) | 355K |
| 42.2M | clustered (sequential) | 422K |
| 50.1M | clustered (sequential) | 501K |
| 59.6M | clustered (sequential) | 596K |
| 70.8M | clustered (sequential) | 708K |
| 84.1M | clustered (sequential) | 841K |
| 100M | clustered (sequential) | 1M |
| 100 | unclustered (random) | 100 |
| 119 | unclustered (random) | 119 |
| 141 | unclustered (random) | 141 |
| 168 | unclustered (random) | 168 |
| 200 | unclustered (random) | 200 |
| 237 | unclustered (random) | 237 |
| 282 | unclustered (random) | 282 |
| 335 | unclustered (random) | 335 |
| 398 | unclustered (random) | 398 |
| 473 | unclustered (random) | 473 |
| 562 | unclustered (random) | 562 |
| 668 | unclustered (random) | 668 |
| 794 | unclustered (random) | 794 |
| 944 | unclustered (random) | 944 |
| 1.12K | unclustered (random) | 1.12K |
| 1.33K | unclustered (random) | 1.33K |
| 1.58K | unclustered (random) | 1.58K |
| 1.88K | unclustered (random) | 1.88K |
| 2.24K | unclustered (random) | 2.24K |
| 2.66K | unclustered (random) | 2.66K |
| 3.16K | unclustered (random) | 3.16K |
| 3.76K | unclustered (random) | 3.76K |
| 4.47K | unclustered (random) | 4.47K |
| 5.31K | unclustered (random) | 5.31K |
| 6.31K | unclustered (random) | 6.31K |
| 7.5K | unclustered (random) | 7.5K |
| 8.91K | unclustered (random) | 8.91K |
| 10.6K | unclustered (random) | 10.6K |
| 12.6K | unclustered (random) | 12.6K |
| 15K | unclustered (random) | 15K |
| 17.8K | unclustered (random) | 17.8K |
| 21.1K | unclustered (random) | 21.1K |
| 25.1K | unclustered (random) | 25.1K |
| 29.9K | unclustered (random) | 29.9K |
| 35.5K | unclustered (random) | 35.5K |
| 42.2K | unclustered (random) | 42.2K |
| 50.1K | unclustered (random) | 50.1K |
| 59.6K | unclustered (random) | 59.6K |
| 70.8K | unclustered (random) | 70.8K |
| 84.1K | unclustered (random) | 84.1K |
| 100K | unclustered (random) | 100K |
| 119K | unclustered (random) | 119K |
| 141K | unclustered (random) | 141K |
| 168K | unclustered (random) | 168K |
| 200K | unclustered (random) | 200K |
| 237K | unclustered (random) | 237K |
| 282K | unclustered (random) | 282K |
| 335K | unclustered (random) | 335K |
| 398K | unclustered (random) | 398K |
| 473K | unclustered (random) | 473K |
| 562K | unclustered (random) | 562K |
| 668K | unclustered (random) | 668K |
| 794K | unclustered (random) | 794K |
| 944K | unclustered (random) | 944K |
| 1.12M | unclustered (random) | 1M |
| 1.33M | unclustered (random) | 1M |
| 1.58M | unclustered (random) | 1M |
| 1.88M | unclustered (random) | 1M |
| 2.24M | unclustered (random) | 1M |
| 2.66M | unclustered (random) | 1M |
| 3.16M | unclustered (random) | 1M |
| 3.76M | unclustered (random) | 1M |
| 4.47M | unclustered (random) | 1M |
| 5.31M | unclustered (random) | 1M |
| 6.31M | unclustered (random) | 1M |
| 7.5M | unclustered (random) | 1M |
| 8.91M | unclustered (random) | 1M |
| 10.6M | unclustered (random) | 1M |
| 12.6M | unclustered (random) | 1M |
| 15M | unclustered (random) | 1M |
| 17.8M | unclustered (random) | 1M |
| 21.1M | unclustered (random) | 1M |
| 25.1M | unclustered (random) | 1M |
| 29.9M | unclustered (random) | 1M |
| 35.5M | unclustered (random) | 1M |
| 42.2M | unclustered (random) | 1M |
| 50.1M | unclustered (random) | 1M |
| 59.6M | unclustered (random) | 1M |
| 70.8M | unclustered (random) | 1M |
| 84.1M | unclustered (random) | 1M |
| 100M | unclustered (random) | 1M |
So clustering buys up to an f-fold reduction in I/O, and f is typically 20 to 200 for a row store on 8 KB pages.
Substitute this into the crossover and the "5% rule" dissolves into two rules:
- An unclustered index stops helping at low single-digit selectivity (here 0.25% to 1%).
- A clustered index on a range predicate essentially never loses to a scan, because it is the scan, started in the right place.
This is the highest-leverage number in this article. Almost every "why isn't my index being used" question is this arithmetic.
There is only one clustered order per table, because the clustered index is the layout. Indexing dropped the single-order limitation for secondary access and relocated it onto one privileged index. The constraint was not eliminated; it was moved somewhere it hurts less.
Heap-organized versus index-organized tables is a genuine engineering choice, not a settled question. For this mechanism we only need its consequence for index references (physical or logical pointer). In outline:
| Heap (PostgreSQL, Oracle by default) | Index-organized (InnoDB, Oracle IOTs, SQL Server clustered tables) | |
|---|---|---|
| Secondary indexes | cheap | each pays the double descent |
| Updates | cheap | wide PK taxes everything |
| PK order | not forced | forced |
| Clustering | no guarantee | guaranteed by PK |
| PK range scans | normal | fast; no separate heap |
In PostgreSQL, CLUSTER is a one-time physical reorder that decays with every later write. The planner's correlation statistic is literally a measurement of how far it has decayed.
In column stores the same mechanism returns under other names (sort keys, Z-order, liquid clustering), and the skipping is done by zone maps rather than a tree (sparse indexes and zone maps). Same constraint, same scarcity, different organ.