Lesson 0001 · Internals & Performance
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.
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.
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."
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."
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."
Finding a full row through a secondary index is therefore two B+tree descents, not one:
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.
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.
Two rules that beginners memorize as arbitrary "best practices" are really just this model talking:
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."
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."
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
Answer from memory — the effortful recall is what builds retention. Feedback is immediate.
What does a leaf node of the InnoDB clustered index store?
Besides the indexed columns, a secondary index entry also stores:
Fetching name via a non-covering index on email costs:
The main reason to keep an InnoDB primary key short is that:
With no PRIMARY KEY and no UNIQUE-NOT-NULL index, InnoDB clusters on:
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.
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');
EXPLAIN SELECT name FROM users WHERE email = 'a@x.com';
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.
EXPLAIN SELECT id, email FROM users WHERE email = 'a@x.com';
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.
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."