Every prior file has referenced EXPLAIN output without fully unpacking
it. This file is the complete reference: what every column means, how to
read a plan tree, and how the same concepts map across MySQL,
PostgreSQL, SQL Server, and Oracle.
EXPLAIN outputEXPLAIN and EXPLAIN ANALYZEA query that ran fine in staging times out in production. The schema is
identical; the row counts aren’t. EXPLAIN (and especially EXPLAIN
ANALYZE) is how you find out why — without it, you’re debugging
performance by guesswork.
The optimizer’s chosen plan is invisible unless you ask for it.
EXPLAIN surfaces the plan without running the query; EXPLAIN ANALYZE
runs it and reports actual measured behavior against the plan’s
estimates — the gap between the two is often the most valuable
diagnostic signal available.
EXPLAIN shows the estimated plan: what the optimizer intends to
do, based on statistics, without executing anything. EXPLAIN ANALYZE
executes the query and reports actual row counts, actual timing, and
actual loop counts alongside the original estimates — letting you spot
exactly where the optimizer’s assumptions diverged from reality.
-- MySQL: plan only, no execution
EXPLAIN
SELECT * FROM orders WHERE customer_id = 88291;
-- MySQL 8.0.18+: adds actual execution stats
EXPLAIN ANALYZE
SELECT * FROM orders WHERE customer_id = 88291;
-- PostgreSQL: plan only
EXPLAIN
SELECT * FROM orders WHERE customer_id = 88291;
-- PostgreSQL: executes and reports actual timing/rows
EXPLAIN ANALYZE
SELECT * FROM orders WHERE customer_id = 88291;
EXPLAIN alone never executes the query — safe to run on production
even for expensive queries.EXPLAIN ANALYZE does execute the query — use with caution on
production for write-heavy or extremely expensive statements (it will
actually perform an UPDATE/DELETE if you EXPLAIN ANALYZE one).id select_type table type key rows Extra
1 SIMPLE orders ref idx_orders_customer_id 5 Using index
system > const > eq_ref > ref >
range > index > ALL (roughly best to worst).Using index (covering), Using
filesort (extra sort step), Using temporary (temp table needed).EXPLAIN
SELECT o.order_id, c.email
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.status = 'completed';
Plan tree (conceptual):
Nested Loop Join
/ \
Index scan Index/PK lookup
orders.status customers.id
(outer, driving) (inner, probed per outer row)
id/nesting carefully).rows (estimated) vs. actual rows (with ANALYZE)
tells you where the optimizer’s model diverged from reality.Loops (visible in EXPLAIN ANALYZE / PostgreSQL’s EXPLAIN ANALYZE)
tells you how many times an inner plan node executed — critical for
nested loop joins, where a small per-loop cost multiplied by millions of
outer rows becomes the dominant cost of the entire query, even though
each individual loop looks cheap in isolation.
Plans themselves aren’t stored persistently in MySQL/PostgreSQL by
default (unlike SQL Server’s plan cache) — every EXPLAIN reflects
current statistics at call time, which is exactly why stale statistics
(File 07) matter operationally.
cost (PostgreSQL’s EXPLAIN shows startup cost and total cost in
arbitrary units, not milliseconds) is a relative number for comparing
candidate plans — it is not directly a time prediction, which is why
EXPLAIN ANALYZE’s actual time figures matter for real diagnosis.
EXPLAIN syntax and output format are entirely vendor-specific; the
ANSI standard has no EXPLAIN concept at all.
EXPLAIN FORMAT=JSON gives a much more detailed, machine-readable
plan including per-step cost estimates.EXPLAIN ANALYZE (8.0.18+) returns a tree-formatted plan with actual
timing embedded per node, replacing the older tabular-only output for
this use case.EXPLAIN (ANALYZE, BUFFERS) additionally reports actual disk/cache
buffer hits — extremely useful for diagnosing whether a query is
I/O-bound or CPU-bound.EXPLAIN ANALYZE — includes actual vs. estimated row counts directly
on each operator icon.EXPLAIN PLAN FOR <query> followed by
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY) is the standard two-step
Oracle workflow for plan-only inspection.DBMS_XPLAN.DISPLAY_CURSOR shows actual execution statistics for a
query that has already run, analogous to EXPLAIN ANALYZE.EXPLAIN
ANALYZE still had real side effects during execution (locks
acquired, triggers fired) — EXPLAIN ANALYZE is not a safe dry-run
mechanism for write statements.EXPLAIN first on production for any expensive-looking
query; reserve EXPLAIN ANALYZE for read-only statements or a safe
staging replica.EXPLAIN ANALYZE on a production DELETE/UPDATE without
wrapping it in a transaction you intend to roll back — and even then,
side effects like triggers and locks still occur.type/access-method column and ignoring Extra
(MySQL) or the buffers/timing detail (PostgreSQL).rows/cost as a direct time prediction.Using filesort or Using temporary in MySQL’s Extra
column — both indicate expensive steps beyond the index access itself.EXPLAIN and EXPLAIN ANALYZE, and
when would you avoid running the latter in production?EXPLAIN output, what does Using filesort in the Extra
column tell you, and why is it a signal to investigate?EXPLAIN ANALYZE plan. What does that suggest, and what’s your next
step?EXPLAIN reveals the optimizer’s intended plan without running the
query; EXPLAIN ANALYZE runs it and reports actual measured behavior
alongside the original estimates. Reading a plan means understanding
access type (index vs. scan), join strategy (nested loop, hash, merge),
and the gap between estimated and actual rows — that gap is usually the
fastest path to diagnosing a real production performance problem.
See 12_PRACTICE_PROBLEMS.md, Production Debugging section, Problems 1–4.