EXPLAIN Query Plan in MySQL
What MySQL EXPLAIN Tells You
EXPLAIN shows the execution plan chosen by the MySQL optimizer: table access order, access methods, candidate and selected indexes, estimated rows, filtering and extra operations. It helps answer how MySQL intends to obtain a result, not whether the SQL result is logically correct.
EXPLAIN supports SELECT and several data-changing statement forms. This lesson uses safe SELECT examples. It also separates ordinary EXPLAIN, which reports an estimated plan, from EXPLAIN ANALYZE, which executes a SELECT and reports measurements.
Build a Verified Query-Plan Lab
DROP TABLE IF EXISTS orders_plan_lab;
CREATE TABLE orders_plan_lab (
order_id INT PRIMARY KEY,
customer_id INT NOT NULL,
status VARCHAR(20) NOT NULL,
order_date DATE NOT NULL,
total_amount DECIMAL(10,2) NOT NULL,
notes VARCHAR(100)
) ENGINE = InnoDB;
INSERT INTO orders_plan_lab VALUES
(1,101,'PAID', '2026-08-01',1200.00,'School books'),
(2,101,'PENDING', '2026-08-05', 500.00,'Stationery'),
(3,102,'PAID', '2026-08-03', 750.00,'Uniform'),
(4,103,'CANCELLED','2026-08-04', 300.00,'Art kit'),
(5,101,'PAID', '2026-08-10',1500.00,'Lab equipment'),
(6,102,'PENDING', '2026-08-11', 900.00,'Sports kit'),
(7,104,'PAID', '2026-08-12',2200.00,'Computer accessory'),
(8,101,'PAID', '2026-08-14', 650.00,'Notebooks');SELECT order_id, order_date, total_amount
FROM orders_plan_lab
WHERE customer_id = 101 AND status = 'PAID'
ORDER BY order_date;First verify the result. Then study the access path. This lab is intentionally tiny, so MySQL may prefer a scan before or even after an index is added. That is not an error; scale and distribution determine cost.
Traditional, TREE and JSON Formats
-- Tabular output
EXPLAIN
SELECT order_id, order_date, total_amount
FROM orders_plan_lab
WHERE customer_id = 101 AND status = 'PAID';
-- Iterator-style hierarchy
EXPLAIN FORMAT=TREE
SELECT order_id, order_date, total_amount
FROM orders_plan_lab
WHERE customer_id = 101 AND status = 'PAID';
-- Machine-readable details
EXPLAIN FORMAT=JSON
SELECT order_id, order_date, total_amount
FROM orders_plan_lab
WHERE customer_id = 101 AND status = 'PAID';| Format | Best use |
|---|---|
| TRADITIONAL | Compact table; quick inspection of type, key, rows, filtered and Extra |
| TREE | Execution hierarchy and iterator relationships; reveals hash joins |
| JSON | Detailed properties, costs, conditions, used key parts and tooling |
MySQL 8.4 can have a server-level explain_format default, so specify a FORMAT when reproducible output matters. EXPLAIN ANALYZE always uses TREE; TRADITIONAL and JSON are not supported for ANALYZE.
Read Traditional EXPLAIN Columns
| Column | Question to ask |
|---|---|
| id | Which SELECT/query block does this row describe? |
| select_type | Is it SIMPLE, PRIMARY, subquery, derived or another form? |
| table | Which table or materialized result is accessed? |
| partitions | Which partitions can match? |
| type | What access/join method is used? |
| possible_keys | Which indexes are candidates? |
| key | Which index was chosen? |
| key_len | How much of the chosen key can participate? |
| ref | Which constants or earlier-table columns are compared to the key? |
| rows | How many rows are estimated for examination? |
| filtered | What estimated percentage passes table conditions? |
| Extra | What additional operations or optimizations occur? |
For InnoDB, rows is an estimate. In a join, rough fan-out can be explored by multiplying estimated rows and filtering effects across steps, but TREE/JSON and measured actuals provide a more complete picture.
possible_keys is not a recommendation list. key=NULL is not automatically a bug. Check how much data is needed and why the chosen plan has lower estimated cost.Understand Access Types Without Memorized Hype
| type | Meaning | Interpretation |
|---|---|---|
| system / const | At most one matching row treated as constant | Very narrow access |
| eq_ref | One unique NOT NULL row per previous-row combination | Excellent unique join access |
| ref | Rows matching a nonunique key or key prefix | Good when matches are few |
| range | Bounded index interval(s) | Common for dates, BETWEEN, IN and inequalities |
| index | Full index-tree scan | Can be cheaper than table scan if narrow/covering |
| ALL | Full table scan | Investigate on large/frequent selective queries; acceptable in other cases |
The often-repeated ranking from const to ALL is only a starting vocabulary. A ref access returning millions of rows can cost more than a small scan. Join order, row counts, loops, page access, coverage and result size matter.
CREATE INDEX idx_customer_status_date
ON orders_plan_lab (customer_id, status, order_date);
EXPLAIN FORMAT=JSON
SELECT order_id, order_date, total_amount
FROM orders_plan_lab
WHERE customer_id = 101 AND status = 'PAID'
ORDER BY order_date;Inspect key and used_key_parts. On eight rows, do not force the index merely to make the output look impressive.
Decode Important Extra Signals
| Extra value | Accurate meaning |
|---|---|
| Using where | A condition is applied to restrict rows; it does not by itself mean slow |
| Using index | Required column information is obtained from the index tree: covering access |
| Using index condition | Index Condition Pushdown tests index tuples before fetching full rows |
| Using filesort | An extra sort is needed; the name does not guarantee a disk file |
| Using temporary | An internal temporary table participates, often for grouping/distinct/ordering patterns |
| Backward index scan | InnoDB scans a descending-capable index in reverse direction |
Using filesort or Using temporary deserves attention when row counts and latency are large, but neither is an automatic failure. Sorting ten rows can be cheaper than maintaining a wide index for every write.
-- Check whether index order can serve filtering and sorting together.
EXPLAIN
SELECT order_id, order_date
FROM orders_plan_lab
WHERE customer_id = 101 AND status = 'PAID'
ORDER BY order_date;EXPLAIN ANALYZE: Estimates vs Actuals
EXPLAIN ANALYZE FORMAT=TREE
SELECT order_id, order_date, total_amount
FROM orders_plan_lab
WHERE customer_id = 101 AND status = 'PAID'
ORDER BY order_date;For each iterator, ANALYZE reports estimated cost/rows and measured time to first row, time range, actual rows and loops. Compare:
- estimated rows vs actual rows: large errors can distort join and access choices;
- loops: a cheap inner operation repeated thousands of times can dominate;
- time to first row vs total: useful for interactive latency and blocking sorts;
- rows flowing between nodes: find where filtering happens too late;
- selected index and lookup count: confirm intended access.
If estimates are badly wrong after major data changes, inspect data skew and refresh key distribution with ANALYZE TABLE orders_plan_lab;. Histograms can help estimates for appropriate nonindexed columns, but they are not a substitute for access indexes.
Evidence-Based Query Tuning Workflow
- Capture the exact SQL, parameters, frequency, latency and rows sent/examined.
- Verify logical output and obtain a reproducible representative dataset.
- Run EXPLAIN in TREE or JSON and identify the largest estimated work.
- Use EXPLAIN ANALYZE safely to compare actual rows, loops and timing.
- Check predicate sargability, data types, join keys and selected columns.
- Design the smallest workload-aligned composite index or SQL rewrite.
- Run the same measurements before and after; include write/storage impact.
- Test edge parameter values, because selectivity can change the best plan.
- Deploy safely, monitor regressions and keep a rollback path.
Common mistakes: adding an index for every ALL, treating rows as exact, confusing Using index with “index chosen,” ignoring loops, tuning an unrepresentative empty database, and using FORCE INDEX before fixing statistics or design.
Continue with composite indexes and the full query optimization guide.
Official References
- MySQL 8.4: EXPLAIN Statement and EXPLAIN ANALYZE
- MySQL 8.4: EXPLAIN Output Format
- MySQL 8.4: Optimizing Queries with EXPLAIN
Formats, fields, access methods, Extra signals and ANALYZE execution semantics were checked against the official MySQL 8.4 manual.