Free tutorials & notes in Hindi & English · Clean code examples · Mobile friendly learning
MySQL + SQL · Lesson 92

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.

Faculty rule: Read a plan as evidence, not a scorecard. A full scan can be correct; an index can be inefficient; a low estimated cost is not elapsed time. Compare alternatives with representative data and measurements.

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;
1 | 2026-08-01 | 1200.00 5 | 2026-08-10 | 1500.00 8 | 2026-08-14 | 650.00

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';
FormatBest use
TRADITIONALCompact table; quick inspection of type, key, rows, filtered and Extra
TREEExecution hierarchy and iterator relationships; reveals hash joins
JSONDetailed 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

ColumnQuestion to ask
idWhich SELECT/query block does this row describe?
select_typeIs it SIMPLE, PRIMARY, subquery, derived or another form?
tableWhich table or materialized result is accessed?
partitionsWhich partitions can match?
typeWhat access/join method is used?
possible_keysWhich indexes are candidates?
keyWhich index was chosen?
key_lenHow much of the chosen key can participate?
refWhich constants or earlier-table columns are compared to the key?
rowsHow many rows are estimated for examination?
filteredWhat estimated percentage passes table conditions?
ExtraWhat 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

typeMeaningInterpretation
system / constAt most one matching row treated as constantVery narrow access
eq_refOne unique NOT NULL row per previous-row combinationExcellent unique join access
refRows matching a nonunique key or key prefixGood when matches are few
rangeBounded index interval(s)Common for dates, BETWEEN, IN and inequalities
indexFull index-tree scanCan be cheaper than table scan if narrow/covering
ALLFull table scanInvestigate 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 valueAccurate meaning
Using whereA condition is applied to restrict rows; it does not by itself mean slow
Using indexRequired column information is obtained from the index tree: covering access
Using index conditionIndex Condition Pushdown tests index tuples before fetching full rows
Using filesortAn extra sort is needed; the name does not guarantee a disk file
Using temporaryAn internal temporary table participates, often for grouping/distinct/ordering patterns
Backward index scanInnoDB 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.
Execution warning: EXPLAIN ANALYZE runs the statement. It is appropriate for safe SELECT work here. Do not casually analyze an expensive production query or data-changing statement. Use a controlled environment, realistic parameters and operational limits.

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

  1. Capture the exact SQL, parameters, frequency, latency and rows sent/examined.
  2. Verify logical output and obtain a reproducible representative dataset.
  3. Run EXPLAIN in TREE or JSON and identify the largest estimated work.
  4. Use EXPLAIN ANALYZE safely to compare actual rows, loops and timing.
  5. Check predicate sargability, data types, join keys and selected columns.
  6. Design the smallest workload-aligned composite index or SQL rewrite.
  7. Run the same measurements before and after; include write/storage impact.
  8. Test edge parameter values, because selectivity can change the best plan.
  9. 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

Formats, fields, access methods, Extra signals and ANALYZE execution semantics were checked against the official MySQL 8.4 manual.

Frequently Asked Questions

Does EXPLAIN execute a SELECT query?
Ordinary EXPLAIN asks the optimizer for the planned execution without returning the normal SELECT result. EXPLAIN ANALYZE is different: it executes the SELECT and measures iterator behavior.
Is type=ALL always bad?
No. It means a full table scan, which can be appropriate for tiny tables or queries needing most rows. Its significance depends on rows, frequency, filtering, joins and measured runtime.
What is the difference between possible_keys and key?
possible_keys lists candidate indexes considered relevant; key is the index actually selected for that table access. A candidate need not be chosen.
Does Using filesort mean MySQL writes a disk file?
Not necessarily. It means MySQL performs an extra sort rather than reading rows in the required index order; the sort can occur in memory or use temporary storage.
Why can estimated rows differ from actual rows?
Estimates come from statistics and the cost model. Skew, correlated columns, stale statistics and complex predicates can cause errors; EXPLAIN ANALYZE exposes actual rows and loops.
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

💻 Live Code Editor

This page's programs are ready here — run them, edit them, and learn. No installation needed.
Powered by OneCompiler. The code loads into the editor automatically — press Run to see the output. If the editor does not open, open it in a new tab.