EXPLAIN and query planning — reading execution plans
Advanced5 min read·eng-06-004
sqlinterviewperformance
Concept
EXPLAIN shows the database's query execution plan — how it WILL execute a query before you actually run it. It's the primary tool for diagnosing slow queries.
MySQL EXPLAIN key columns:
type: The join type.const(best) →ref→range→index→ALL(worst — full table scan).key: Which index was used.NULLmeans no index.key_len: How many bytes of the index were used. Helps identify if a composite index is partially used.rows: Estimated number of rows examined. Lower is better.filtered: Percentage of rows passed by the WHERE condition.Extra: Important flags:Using index(covering index, good),Using filesort(sort not using an index, potentially slow),Using temporary(needs a temp table, slow for large data).
EXPLAIN ANALYZE (PostgreSQL / MySQL 8.0+): Actually runs the query and shows real execution time and row counts (vs. estimated). More accurate. Use in a read replica or dev environment.
EXPLAIN FORMAT=JSON (MySQL): More detailed output including cost estimates.
Reading the plan: Start from the innermost operation (the first table accessed). Work outward. Look for:
type = ALLon large tables — missing index.Using filesort— consider adding an index that covers the ORDER BY columns.rowsestimate much larger than actual — stale statistics. RunANALYZE TABLE.
PostgreSQL: Uses different terminology. Seq Scan = full scan. Index Scan or Index Only Scan = index used. Hash Join / Nested Loop / Merge Join = join strategies.
Code Example
sql
-- MySQL EXPLAIN
EXPLAIN SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.active = 1
GROUP BY u.id
ORDER BY order_count DESC
LIMIT 10;
-- Sample EXPLAIN output (MySQL)
-- id | select_type | table | type | key | rows | Extra
-- 1 | SIMPLE | u | ref | idx_users_active | 1250 | Using index condition; Using temporary; Using filesort
-- 1 | SIMPLE | o | ref | idx_orders_user_id | 5 | NULL
--
-- Analysis:
-- u: type=ref, key=idx_users_active — good, uses index
-- Extra: "Using temporary; Using filesort" — the GROUP BY + ORDER BY can't be satisfied by an index
-- Fix: ensure the GROUP BY and ORDER BY columns are in an index, or accept the filesort for small datasets
-- EXPLAIN ANALYZE (PostgreSQL — actually runs the query)
EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 42 ORDER BY created_at DESC LIMIT 10;
-- PostgreSQL output:
-- Limit (cost=0.42..2.85 rows=10 width=...) (actual time=0.043..0.087 rows=10 loops=1)
-- -> Index Scan using idx_orders_user_created on orders
-- Index Cond: (user_id = 42)
-- Filter: (deleted_at IS NULL)
-- Planning Time: 0.1 ms
-- Execution Time: 0.1 ms ← actual time
-- Detecting missing index — type=ALL on a large table
EXPLAIN SELECT * FROM orders WHERE status = 'pending';
-- type=ALL, key=NULL → add index on status column
-- After adding index
CREATE INDEX idx_orders_status ON orders (status);
EXPLAIN SELECT * FROM orders WHERE status = 'pending';
-- type=ref, key=idx_orders_status → index usedphp
<?php
// Get the raw SQL from Eloquent to paste into EXPLAIN
$query = Order::where('user_id', 42)->orderBy('created_at', 'desc');
$sql = $query->toSql();
$bindings = $query->getBindings();
// Replace ? with actual values, then run EXPLAIN <that SQL>
// Or use DB::select to run EXPLAIN directly
$plan = DB::select('EXPLAIN ' . $query->toSql(), $query->getBindings());
dd($plan); // array of stdClass with type, key, rows, Extra, etc.