Basic form – one output row per table the optimizer plans to access.
| id | Sequence number of the SELECT within the query. Subqueries and unioned SELECTs get their own id. |
| select_type | SIMPLE, PRIMARY, SUBQUERY, DERIVED, UNION, etc. |
| table | The table (or derived table alias) this row describes. |
| partitions | Matching partitions, if the table is partitioned (see Partitioning). |
| type | The join/access method – the single most important column. See below. |
| possible_keys | Indexes the optimizer considered. |
| key | The index actually chosen, or NULL if a full scan was used. |
| key_len | Bytes of the chosen index actually used – useful for confirming how much of a composite index is engaged. |
| ref | What is compared against the index: a constant, a column from another table, or func. |
| rows | Estimated rows the optimizer expects to examine for this table (an estimate, not an exact count). |
| filtered | Estimated percentage of those rows that survive the WHERE conditions. |
| Extra | Additional plan details – often the most actionable column. See below. |
| system / const | At most one matching row, read once – e.g. a lookup by PRIMARY KEY on a constant. |
| eq_ref | One matching row per outer row, via a unique or primary key – typical for well-indexed joins. |
| ref | Multiple matching rows via a non-unique index equality lookup. |
| range | An index range scan (<, >, BETWEEN, IN, LIKE 'prefix%'). |
| index | A full scan of an index (not the table), still reads every entry but can avoid a sort or a table read. |
| ALL | Full table scan – no usable index. Acceptable for tiny tables, a red flag on large ones. |
| Using index | A covering index satisfied the query without touching the table row – the ideal outcome. |
| Using where | A WHERE condition filters rows after the storage engine returns them (in addition to any index lookup). |
| Using index condition | Index Condition Pushdown – part of the WHERE clause is evaluated against the index before rows are fetched. |
| Using temporary | MySQL builds a temporary table, typically for GROUP BY/DISTINCT over columns that don't match the index used, or certain UNIONs. Expensive on large result sets. |
| Using filesort | An extra sort pass is required because ORDER BY couldn't be satisfied by index order. Despite the name, it may happen in memory; it's still added CPU/I/O work. |
| Using join buffer | No index was usable for the join, so MySQL buffers rows and compares them in batches (Block Nested-Loop or hash join). See JOIN Optimization. |
CREATE TABLE orders (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
customer_id INT UNSIGNED NOT NULL,
status VARCHAR(20) NOT NULL,
created_at DATETIME NOT NULL,
KEY idx_cust_status (customer_id, status)
) ENGINE=InnoDB;
-- Classic tabular plan
EXPLAIN SELECT id, status
FROM orders
WHERE customer_id = 42 AND status = 'SHIPPED';
-- Real timings and row counts (executes the query)
EXPLAIN ANALYZE
SELECT id, status
FROM orders
WHERE customer_id = 42 AND status = 'SHIPPED';
-- Machine-readable plan with cost detail
EXPLAIN FORMAT=JSON
SELECT id, status
FROM orders
WHERE customer_id = 42 AND status = 'SHIPPED';
-- Refresh optimizer statistics when EXPLAIN's row estimate
-- diverges sharply from EXPLAIN ANALYZE's actual rows
ANALYZE TABLE orders;
CREATE TABLE orders (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
customer_id INT UNSIGNED NOT NULL,
status VARCHAR(20) NOT NULL,
created_at DATETIME NOT NULL,
KEY idx_cust_status (customer_id, status)
) ENGINE=InnoDB;
-- Classic tabular plan
EXPLAIN SELECT id, status
FROM orders
WHERE customer_id = 42 AND status = 'SHIPPED';
-- Real timings and row counts (executes the query)
EXPLAIN ANALYZE
SELECT id, status
FROM orders
WHERE customer_id = 42 AND status = 'SHIPPED';
-- Machine-readable plan with cost detail
EXPLAIN FORMAT=JSON
SELECT id, status
FROM orders
WHERE customer_id = 42 AND status = 'SHIPPED';
-- Refresh optimizer statistics when EXPLAIN's row estimate
-- diverges sharply from EXPLAIN ANALYZE's actual rows
ANALYZE TABLE orders;