Lesson 0001 · Internals & Performance

The clustered index

Why an InnoDB table is a B+tree ordered by its primary key — and why almost every index decision falls out of that one fact.

~9 minRetrieval quiz + hands-on EXPLAINCheat sheet: here

You already write SELECT, JOIN, and CREATE INDEX. This lesson turns a fact you may have half-heard — "InnoDB tables are clustered by primary key" — into the keystone mental model this whole course hangs off. Get this one idea and a surprising amount derives itself: why your primary-key choice is really a storage decision, why a lookup by a secondary index can cost twice as much as a lookup by primary key, what a "covering index" actually buys you, and why a random UUID primary key can quietly wreck write performance.

The one idea

In InnoDB, the table is not a pile of rows with indexes pointing at it. The table is an index — a B+tree ordered by the primary key, whose leaf nodes contain the entire row. This is the clustered index, and there is exactly one per table. MySQL Manual: "Each InnoDB table has a special index called the clustered index that stores row data… the clustered index is synonymous with the primary key."

CLUSTERED INDEX (the table itself, ordered by PK = id) ┌───────────────┐ internal nodes │ [ 50 | 90 ] │ ← keys only, to route the search └───┬───┬───┬───┘ ┌──────────┘ │ └───────────┐ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ leaves │ 10 →row │ │ 55 →row │ │ 92 →row │ ← leaf holds the WHOLE row │ 30 →row │ │ 70 →row │ │ 98 →row │ (id, name, email, …) └─────────┘ └─────────┘ └─────────┘ └──────────── linked for range scans ─────────▶
The key insight

Because the row lives in the primary-key tree, reaching a leaf by primary key hands you the complete row with no second lookup. "Access by primary key" is the cheapest read InnoDB can do — the search leads directly to the page holding the data. MySQL Manual: "Accessing a row through the clustered index is fast because the index search leads directly to the page that contains the row data."

So what is a secondary index?

Every other index — the ones you make with CREATE INDEX — is a secondary index: its own B+tree, ordered by the columns you indexed. But here is the twist that everything else depends on. A secondary index leaf does not store a disk address for the row. It stores the primary-key value, and uses that to find the row back in the clustered index. MySQL Manual: "each record in a secondary index contains the primary key columns for the row, as well as the columns specified for the secondary index… InnoDB uses this primary key value to search for the row in the clustered index."

SECONDARY INDEX (email) CLUSTERED INDEX (id = PK) ┌──────────────────────┐ ┌──────────────────────────┐ │ a@x.com → (id 42) │ ── step 2 ─▶│ 42 → { id, name, email } │ │ b@x.com → (id 17) │ find row │ 17 → { id, name, email } │ └──────────────────────┘ by that PK └──────────────────────────┘ step 1: find the PK for the email fetch the rest of the row

The consequence: the double lookup

Finding a full row through a secondary index is therefore two B+tree descents, not one:

  1. Walk the secondary index to find the matching primary-key value.
  2. Walk the clustered index by that primary key to fetch the rest of the row.

This second step has a name — a bookmark lookup — and it is why a query filtered by an indexed column can still be slower than you'd expect: each matched row pays for a second descent.

The payoff — covering indexes

Now the definition of a covering index is obvious instead of magic. If the secondary index already contains every column the query asks for (remember: it also carries the PK columns for free), step 2 is unnecessary — InnoDB answers from the index alone. That is exactly the Using index you'll see in EXPLAIN, and you'll trigger it on purpose in the hands-on below.

The other consequence: primary-key choice is a storage decision

Two rules that beginners memorize as arbitrary "best practices" are really just this model talking:

1. Keep the primary key short

Every secondary index copies the primary-key columns into every one of its entries (that's how it points back). So a fat PK — say a 40-byte string — is duplicated across every secondary index, inflating all of them. MySQL Manual: "If the primary key is long, the secondary indexes use more space, so it is advantageous to have a short primary key."

2. Prefer a monotonically increasing primary key

Because rows are physically ordered by PK, an AUTO_INCREMENT integer means every insert lands at the right edge of the tree — append-only, cheap, compact. A random primary key (a random UUIDv4) forces inserts into the middle of the tree constantly, causing page splits and fragmentation. MySQL recommends adding an auto-increment column when you have no natural short key. MySQL Manual: "If there is no logical unique and non-null column… add an auto-increment column."

The classic footgun

Define no primary key and InnoDB doesn't skip the clustered index — it invents one you can't use: a hidden GEN_CLUST_INDEX on a synthetic 6-byte row ID. You pay the full cost of a clustered index and get nothing queryable back. Always declare an explicit primary key. MySQL Manual — GEN_CLUST_INDEX fallback

Check yourself

Answer from memory — the effortful recall is what builds retention. Feedback is immediate.

What does a leaf node of the InnoDB clustered index store?

The clustered index leaves hold the full row in primary-key order — the table is this tree. That's why a primary-key read needs no second lookup.

Besides the indexed columns, a secondary index entry also stores:

A secondary index points back to the row by its primary-key value, not a disk address. InnoDB then searches the clustered index by that PK.

Fetching name via a non-covering index on email costs:

One descent walks the secondary index to get the PK; a second descent (the bookmark lookup) walks the clustered index for the rest of the row.

The main reason to keep an InnoDB primary key short is that:

Every secondary index stores the PK in every entry as its row pointer, so a long PK bloats all of them. Short PK → smaller indexes everywhere.

With no PRIMARY KEY and no UNIQUE-NOT-NULL index, InnoDB clusters on:

InnoDB always clusters. Lacking a usable key it builds a hidden GEN_CLUST_INDEX on a 6-byte row ID you can't query — pure overhead. Declare a real PK.

Hands-on: watch the double lookup disappear

Recall is one thing; seeing EXPLAIN flip to Using index is another. You need a local MySQL 8 — e.g. docker run --rm -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=1 mysql:8, then mysql -h127.0.0.1 -uroot.

1 · A table with a secondary index

CREATE DATABASE lab; USE lab;
CREATE TABLE users (
  id     INT AUTO_INCREMENT PRIMARY KEY,   -- clustered index
  email  VARCHAR(120) NOT NULL,
  name   VARCHAR(120) NOT NULL,
  INDEX idx_email (email)                  -- secondary index
);
INSERT INTO users (email, name) VALUES
  ('a@x.com','Ann'), ('b@x.com','Bo'), ('c@x.com','Cy');

2 · Predict, then run — a NON-covering query

EXPLAIN SELECT name FROM users WHERE email = 'a@x.com';
Predict first

The query filters by email (indexed) but wants name (not in that index). Will the Extra column say Using index? Commit to an answer before you run it.

3 · Now a covering query

EXPLAIN SELECT id, email FROM users WHERE email = 'a@x.com';
What you should observe

Query 2 shows Extra: NULL (or blank) — name isn't in idx_email, so InnoDB does the bookmark lookup into the clustered index. Query 3 shows Extra: Using index — everything it needs (email, and id which the secondary index carries for free) is right there in the index, so the second lookup is skipped. You just made a covering index by asking only for columns the index already holds.

Bonus: run EXPLAIN SELECT * FROM users WHERE id = 1; — access type const/eq_ref by primary key, the cheapest read there is. Bring any surprising plan to your teacher and we'll read it together.

Primary source — read this next

MySQL 8.0 Reference Manual — Clustered and Secondary Indexes. Every claim in this lesson traces back to this one short page, in the authors' own words. For the physical picture underneath it — pages, records, tree layout — follow with Jeremy Cole's "InnoDB: A journey to the core."