skip to content

In an engine that stores table rows clustered by the primary key — InnoDB, for example — how does the choice of primary key affect physical storage, secondary index lookups, and insert throughput?

level: seniorimportance: must knowfreq 56%

answer

  1. Clustered index IS the table; leaves hold whole rows
  2. Secondary index → PK value → second descent
  3. Key width multiplied by every secondary index
  4. Random UUID = page splits and cache-miss cliff
  5. No PK declared → hidden internal row id

basics

~20 s

There the table is the primary key's B-tree: leaf pages hold whole rows in key order. Secondary indexes store the primary key as the row pointer, so wide keys bloat them and lookups cost two descents. Random keys scatter inserts across pages; monotonic keys append but concentrate contention on the rightmost page.

solid answer

~60 s

In a clustered (index-organised) layout the primary key B-tree **is** the table: leaf pages contain the full rows, ordered by key. Three consequences follow. **Secondary indexes point by key, not by physical address.** Their leaf entries hold the indexed columns plus the primary key value. A lookup through a secondary index therefore descends that index, then descends the clustered index to fetch the row — two traversals unless the index covers the query. A wide primary key is copied into every entry of every secondary index, inflating them all. **Insert pattern follows key order.** A monotonically increasing key appends to the rightmost page: dense pages, sequential writes, few splits — but all concurrent inserters contend on that page. A random key such as a version-4 UUID inserts everywhere, causing page splits, poor fill, and a working set that no longer fits in the buffer pool once the table exceeds memory. **Range scans on the key are sequential**, because neighbouring keys are neighbouring rows. If no primary key is declared, InnoDB uses a unique non-null index or a hidden internal row id you cannot use.

code

text · 3 lines
text
secondary index (email)                 clustered index (PRIMARY KEY id)
  leaf: ('[email protected]', id=4711)  ------->    leaf: id=4711 | full row bytes
         ^ indexed cols + PK value                ^ second descent to fetch row

go deeper

for a junior

Know that in such an engine rows are stored in primary key order and that the key should be small; details of secondary index layout are not expected.

for a middle

Explain the two-descent secondary lookup, the key value stored in secondary entries, and why random keys hurt inserts.

for a senior

Quantify: buffer-pool pressure, page splits and fill factor, the cliff when the index exceeds memory, the right-edge contention tradeoff, and covering indexes as the mitigation.

for a principal

Weigh key format against sharding, external exposure, storage cost, and replication, and be explicit that the analysis differs for heap-organised engines.

## Clustered versus heap storage Engines organise table rows in one of two ways. In a **heap**, rows live wherever there is room and every index — including the primary key's — stores a physical row locator. In a **clustered** or index-organised layout, the primary key's B-tree *is* the table: internal pages route by key, and leaf pages contain the entire rows, ordered by key. InnoDB is the canonical clustered engine; PostgreSQL heaps are the canonical alternative. The distinction changes how much your key choice matters, and it is why the same advice does not transfer between engines. ## Consequence 1: secondary indexes carry the primary key In a clustered engine rows move as pages split and merge, so a physical address is not a stable pointer. Secondary index entries therefore store the **primary key value** as the row locator. Two things follow. *Lookup cost.* Reading a row through a secondary index means descending the secondary B-tree to find the key, then descending the clustered B-tree to reach the row — roughly double the logical I/O of a primary key lookup. This second step is often called a bookmark lookup. It disappears when the secondary index *covers* the query, i.e. contains every column the query needs (the primary key columns come along for free, which is why key columns are effectively present in every index). *Index size.* The key is duplicated into every entry of every secondary index. Replacing an 8-byte key with a 36-character textual UUID on a table with five secondary indexes and 100M rows adds gigabytes of index, and those bytes compete for buffer-pool memory with the data you actually query. ## Consequence 2: insert order is physical order Inserts land where the key sorts. *Monotonic keys* (auto-increment, sequence, time-ordered id) always append to the rightmost leaf. Pages fill densely, writes are sequential, splits are cheap right-edge splits, and the recently written pages — the ones most likely to be read again — stay hot in cache. The downside is contention: many concurrent inserters serialise on that last page and, in some configurations, on the counter that hands out values. *Random keys* (version-4 UUID, hash) insert into arbitrary existing pages. Each insert may dirty a different page, so a batch of N inserts touches N pages instead of a handful; pages split near the middle leaving both halves half-full, wasting space and increasing the tree's page count; and once the index exceeds RAM, every insert becomes a read-modify-write of a page that has to be fetched from disk. Throughput does not degrade gently — it falls off a cliff at the point the index stops fitting in memory. Time-ordered identifiers (UUIDv7, ULID, snowflake-style) exist precisely to recover locality while keeping decentralised generation. *Updates that change the key* physically relocate the row and force every secondary index entry to be rewritten, which is one more reason key values should never change. ## Consequence 3: ordering you can exploit Range scans and ordered reads on the primary key are sequential page reads with no sort. For a child table keyed `(order_id, line_no)`, all lines of an order sit on the same page or two — fetching them is one seek. That is a genuine design tool: choosing the leading key column chooses which access pattern is cheap. ## If you declare no primary key InnoDB will use the first suitable unique NOT NULL index; failing that it invents a hidden 6-byte row id from a shared counter. The table works, but you have an identifier you cannot query, replication and change-data-capture tools struggle to identify rows, and the hidden counter is a global contention point. Always declare a key. ## Practical guidance - Prefer a narrow key: a 64-bit integer or a 16-byte binary UUID, not a 36-character string. - Prefer a key whose insert order has locality: sequence or time-ordered id rather than random UUID, unless the write-hotspot on the right edge is the greater problem. - If external identifiers must be opaque and unguessable, you can still store a compact internal clustered key and carry the public identifier in a separate indexed column. - Remember this is engine-dependent. In a heap-organised engine, random keys cost less because index entries point at physical locations and the table itself is not reordered — though random insertion still fragments the primary key's own index.

  • Why does a covering secondary index avoid the second B-tree descent?
    The bookmark lookup exists only to fetch columns the secondary index does not hold. If the index already contains every column the query projects and filters on, the engine answers from the index leaves alone. The primary key columns are always present in the entry, so they never need to be added explicitly.
  • What changes about this advice in a heap-organised engine such as PostgreSQL?
    There the table is a heap and indexes store physical row pointers, so a random primary key does not scatter table writes and secondary indexes do not carry the key value. Key width still costs in the primary key's own index, and random insertion still fragments that index, but the cliff-edge insert degradation typical of random keys in a clustered engine is far milder.
  • An auto-increment key gives perfect locality — why might you still not want one on a very hot table?
    Every concurrent inserter targets the same rightmost page and the same value generator, so the last page becomes a latch and lock hotspot and can cap insert concurrency. Remedies include hashing or prefixing the key to spread inserts across several regions, or accepting the contention because it is still cheaper than random I/O.

saying these in an interview costs you the question

  • Believing secondary indexes store a physical row address in a clustered engine
  • Saying a 36-character UUID string key is fine because "an index is an index"
  • Assuming a table without a declared primary key simply has no index structure at all
  • Treating the clustered-engine advice as universal without noting heap engines behave differently
  • Ignoring that changing a key value relocates the row and rewrites every secondary index entry

context