Lesson 0003 · Internals & Performance

Reading EXPLAIN

The optimizer already wrote down its plan for every query — this lesson teaches you to read it in one fixed pass and know instantly whether it's good.

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

In 0001 and 0002 you predicted how MySQL would use an index. EXPLAIN is how you check — for any query, on any table. It prints the optimizer's chosen plan without running the statement. The trick is to stop reading it left-to-right and instead read five columns in a fixed order of importance.

The one idea: read it in this order

The reading order

type → key → rows → filtered → Extra. type is the headline (how it reaches rows), key is which index it actually used, rows × filtered is how many rows it expects to carry forward, and Extra is where the warnings hide. Read those five and you know the plan.

1 · type — the access-method ladder

This is the single most important value. It tells you how MySQL reaches rows, and it has a strict best-to-worst order. Learn the ladder and you can grade a plan at a glance. MySQL Manual — EXPLAIN Join Types

typeMeaningTies back to
constAt most one matching row, read once. Unbeatable.PK / unique = constant
eq_refOne row per join combination via a PK / UNIQUE NOT NULL key. Best join type.Clustered lookup (0001)
refAll rows matching an index value — non-unique key or a leftmost prefix. Good if few rows.Secondary / prefix (0002)
rangeRows in a range, via an index (>, BETWEEN, IN).Range stops prefix (0002)
indexFull scan of the index tree (smaller than the table; often covering).Covering (0001)
ALLFull table scan — every row. Usually the thing to fix.No usable prefix (0002)

Rule of thumb: const/eq_ref/ref are usually fine; range depends on how wide; index and especially ALL on a big table are red flags unless the row count is tiny.

2 · key — which index actually won

possible_keys lists indexes MySQL could use; key is the one it chose (NULL = none, i.e. a scan). A common bug signal: possible_keys names your index but key is NULL — the optimizer decided a scan was cheaper, often because the filter isn't a leftmost prefix or the column is wrapped in a function. MySQL Manual: "the key column indicates the key (index) that MySQL actually decided to use."

3 · rows × filtered — the work estimate

rows is how many rows MySQL estimates it must examine (for InnoDB, an estimate, not exact). filtered is the percentage expected to survive the WHERE. Their product is the rows carried into the next join step. MySQL Manual: "rows × filtered shows the number of rows that are joined with the following table."

Read it as a funnel

rows: 1000, filtered: 50.00 → about 500 rows flow forward. Big rows with tiny filtered means MySQL is reading a lot to keep a little — a sign an index could do the filtering earlier.

4 · Extra — where the warnings live

Small text, big meaning. The four you'll see constantly:

ExtraWhat it tells you
Using indexCovering — answered from the index alone, no clustered lookup (0001).
Using whereRows are filtered after the storage engine returns them.
Using index conditionIndex Condition Pushdown — part of the WHERE checked in the index first.
Using filesortA separate sort was needed — the index didn't supply the order (0002).

Estimates vs reality: EXPLAIN ANALYZE

Plain EXPLAIN shows estimates. When the estimate lies (stale statistics, skewed data), run EXPLAIN ANALYZE — it actually executes the query and prints actual time and rows next to the estimates, in a tree of iterators. When estimated rows and actual rows diverge wildly, you've found why the optimizer chose badly. MySQL Manual: "EXPLAIN ANALYZE … runs a statement and produces EXPLAIN output along with timing and … information about how the optimizer's expectations matched the actual execution."

-> Filter: (t3.pk > 17) (cost=1.26 rows=5) (actual time=0.013..0.016 rows=6 loops=1) ← estimated 5, actually 6 -> Index range scan on t3 using PRIMARY (cost=1.26 rows=5) (actual time=0.012..0.014 rows=5 loops=1)

Check yourself

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

The type column in EXPLAIN reports:

type is the access/join type — how MySQL reaches rows (const, eq_ref, ref, range, index, ALL). It's the headline value for grading a plan.

A PK or UNIQUE-NOT-NULL equality join shows access type:

eq_ref reads one row per combination via a PK / UNIQUE-NOT-NULL index — the best join type after const. ref is for non-unique keys or prefixes.

An access type of ALL means the query did:

ALL is a full table scan — every row examined. On a large table it's the usual thing to fix by adding a usable index.

Multiplying rows by filtered estimates the:

rows × filtered is the estimated number of rows that survive the WHERE and are joined with the next table — the funnel's output.

Extra: Using index is telling you there is:

Using index means the query was answered from the index alone (covering), skipping the clustered-index lookup from Lesson 0001. Don't confuse it with Using where.

EXPLAIN ANALYZE differs from plain EXPLAIN because it:

EXPLAIN ANALYZE executes the statement and reports actual time and rows beside the estimates, exposing where the optimizer's guesses were wrong.

Hands-on: grade some plans

Reuse the people table from Lesson 0002 (index name (last_name, first_name), PK id). Read the five columns each time.

-- A) PK equality → expect type: const
EXPLAIN SELECT * FROM people WHERE id = 1;

-- B) prefix equality → expect type: ref, key: name
EXPLAIN SELECT * FROM people WHERE last_name = 'Jones';

-- C) covering → expect Extra: Using index
EXPLAIN SELECT last_name, first_name FROM people WHERE last_name = 'Jones';

-- D) non-prefix filter → expect type: ALL, key: NULL
EXPLAIN SELECT * FROM people WHERE first_name = 'John';

-- E) estimates vs reality — actual rows beside estimated
EXPLAIN ANALYZE SELECT * FROM people WHERE last_name = 'Jones';
What you should observe

A const (top of the ladder). B type: ref, key: name. C adds Extra: Using index — covering. D falls to type: ALL, key: NULL — the leftmost-prefix rule biting. E prints actual time … rows=N next to the estimate. On this tiny table estimates match; on a real table, that gap is your diagnostic. Bring me any plan where key was NULL but you expected an index.

Primary source — read this next

MySQL 8.0 Reference Manual — EXPLAIN Output Format. The definitive column-by-column reference, including every type and every Extra value. Pair it with EXPLAIN ANALYZE for the execution-time view.