Query plans show whether MySQL or MariaDB will reach rows through an index lookup, a full scan, a temporary table, or a filesort before the statement ever becomes a production incident. Reading that plan first turns SQL tuning from guesswork into a controlled check of how much data the optimizer expects to touch.
Base EXPLAIN describes the optimizer's chosen access path for a statement, usually one row per table in the plan. Shared fields such as type, possible_keys, key, rows, and Extra show how the server will reach data, which indexes it considered, which index it chose, and whether extra work such as sorting or temporary tables is still required.
Current MySQL and MariaDB releases still overlap on plain EXPLAIN, but the deeper inspection commands differ. MySQL keeps the default traditional plan table, adds SHOW WARNINGS, FORMAT=JSON, optional FORMAT=TREE, and EXPLAIN ANALYZE, while MariaDB uses EXPLAIN EXTENDED for the rewritten query text plus filtered and uses ANALYZE or ANALYZE FORMAT=JSON for execution-time statistics. Any execution-time variant runs the statement, so keep it on representative but safe workloads.
Plans are cost estimates built from current statistics plus the specific constants in the predicate, so customer_id = 1 and customer_id = 9001 can choose different paths on the same table.
mysql> EXPLAIN SELECT id, total FROM orders WHERE customer_id = 1 ORDER BY created_at DESC LIMIT 3; +----+-------------+--------+------------+------+------------------------+------------------------+---------+-------+------+----------+----------------+ | id | select_type | TABLE | partitions | TYPE | possible_keys | KEY | key_len | REF | ROWS | filtered | Extra | +----+-------------+--------+------------+------+------------------------+------------------------+---------+-------+------+----------+----------------+ | 1 | SIMPLE | orders | NULL | REF | idx_orders_customer_id | idx_orders_customer_id | 4 | const | 6 | 100.00 | USING filesort | +----+-------------+--------+------------+------+------------------------+------------------------+---------+-------+------+----------+----------------+ 1 ROW IN SET, 1 warning (0.00 sec)
The example above shows MySQL's traditional tabular output. MariaDB's plain EXPLAIN usually shows a shorter column set and omits partitions and filtered until EXPLAIN EXTENDED or ANALYZE is used.
Single-table statements usually return one row. Join plans usually return one row per table, and row order reflects the join order chosen by the optimizer rather than the order used in the SQL text.
possible_keys lists indexes that might work for that step. key shows the index actually chosen. A NULL key means the optimizer did not select an index for that access path.
Common type values range from ALL for a full scan to range, ref, and eq_ref for progressively tighter lookups. rows is an estimate of work, and Extra flags side operations such as Using filesort, Using temporary, Using index condition, or Using join buffer.
mysql> SHOW WARNINGS; +-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Level | Code | Message | +-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Note | 1003 | /* select#1 */ SELECT `appdb`.`orders`.`id` AS `id`,`appdb`.`orders`.`total` AS `total` FROM `appdb`.`orders` WHERE (`appdb`.`orders`.`customer_id` = 1) ORDER BY `appdb`.`orders`.`created_at` DESC LIMIT 3 | +-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 1 ROW IN SET (0.00 sec)
MySQL uses SHOW WARNINGS after EXPLAIN to expose the extended form of the statement. This is especially useful when ref shows func and the chosen expression is not obvious from the tabular plan alone.
MariaDB [appdb]> EXPLAIN EXTENDED SELECT id, total FROM orders WHERE customer_id = 1 ORDER BY created_at DESC LIMIT 3; +------+-------------+--------+------+------------------------+------------------------+---------+-------+------+----------+-----------------------------+ | id | select_type | TABLE | TYPE | possible_keys | KEY | key_len | REF | ROWS | filtered | Extra | +------+-------------+--------+------+------------------------+------------------------+---------+-------+------+----------+-----------------------------+ | 1 | SIMPLE | orders | REF | idx_orders_customer_id | idx_orders_customer_id | 4 | const | 6 | 100.00 | USING WHERE; USING filesort | +------+-------------+--------+------+------------------------+------------------------+---------+-------+------+----------+-----------------------------+ 1 ROW IN SET, 1 warning (0.00 sec) MariaDB [appdb]> SHOW WARNINGS; +-------+------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Level | Code | Message | +-------+------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Note | 1003 | SELECT `appdb`.`orders`.`id` AS `id`,`appdb`.`orders`.`total` AS `total` FROM `appdb`.`orders` WHERE `appdb`.`orders`.`customer_id` = 1 ORDER BY `appdb`.`orders`.`created_at` DESC LIMIT 3 | +-------+------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 1 ROW IN SET (0.00 sec)
MariaDB documents EXPLAIN ANALYZE as outdated syntax. Current MariaDB releases use EXPLAIN EXTENDED for the extra plan column and SHOW WARNINGS note, then use ANALYZE for runtime statistics.
mysql> EXPLAIN FORMAT=JSON SELECT id, total FROM orders WHERE customer_id = 1 ORDER BY created_at DESC LIMIT 3\G *************************** 1. ROW *************************** EXPLAIN: { "query_block": { "select_id": 1, "ordering_operation": { "using_filesort": TRUE, "table": { "table_name": "orders", "access_type": "ref", "possible_keys": [ "idx_orders_customer_id" ], "key": "idx_orders_customer_id", "rows_examined_per_scan": 6 } } } } ##### snipped #####
Both servers support FORMAT=JSON, but the field names and nesting differ. Use it to confirm which node is sorting, scanning, or applying predicates when the tabular plan feels too compressed. On MySQL, FORMAT=TREE is a shorter human-readable alternative when JSON is more detail than needed.
mysql> EXPLAIN ANALYZE SELECT id, total FROM orders WHERE customer_id = 1 ORDER BY created_at DESC LIMIT 3; +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | EXPLAIN | +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | -> LIMIT: 3 ROW(s) (cost=1.1 ROWS=3) (actual TIME=0.0968..0.0976 ROWS=3 loops=1) -> Sort: orders.created_at DESC, LIMIT INPUT TO 3 ROW(s) per chunk (cost=1.1 ROWS=6) (actual TIME=0.0962..0.0967 ROWS=3 loops=1) -> INDEX lookup ON orders USING idx_orders_customer_id (customer_id=1) (cost=1.1 ROWS=6) (actual TIME=0.0165..0.0231 ROWS=6 loops=1) | +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 1 ROW IN SET (0.01 sec)
EXPLAIN ANALYZE executes the query and returns tree-format output. Run it during a quiet window or against a replica when the statement is expensive.
MariaDB [appdb]> ANALYZE SELECT id, total FROM orders WHERE customer_id = 1 ORDER BY created_at DESC LIMIT 3; +------+-------------+--------+------+------------------------+------------------------+---------+-------+------+--------+----------+------------+-----------------------------+ | id | select_type | TABLE | TYPE | possible_keys | KEY | key_len | REF | ROWS | r_rows | filtered | r_filtered | Extra | +------+-------------+--------+------+------------------------+------------------------+---------+-------+------+--------+----------+------------+-----------------------------+ | 1 | SIMPLE | orders | REF | idx_orders_customer_id | idx_orders_customer_id | 4 | const | 6 | 3.00 | 100.00 | 100.00 | USING WHERE; USING filesort | +------+-------------+--------+------+------------------------+------------------------+---------+-------+------+--------+----------+------------+-----------------------------+ 1 ROW IN SET (0.00 sec)
ANALYZE executes the statement. ANALYZE SELECT discards the result set after collecting statistics, but ANALYZE UPDATE or ANALYZE DELETE still performs the change.
ANALYZE FORMAT=JSON adds runtime fields such as r_loops and r_total_time_ms on top of the JSON plan.
mysql> ANALYZE TABLE orders; +--------------+---------+----------+----------+ | TABLE | Op | Msg_type | Msg_text | +--------------+---------+----------+----------+ | appdb.orders | analyze | STATUS | OK | +--------------+---------+----------+----------+ 1 ROW IN SET (0.00 sec)
Bulk loads, large deletes, or skewed data can make row estimates drift. Fresh statistics can change possible_keys, key, rows, and join order.
mysql> EXPLAIN SELECT id, total FROM orders WHERE customer_id = 1 ORDER BY created_at DESC LIMIT 3; +----+-------------+--------+------------+------+-------------------------------------------------------+--------------------------------+---------+-------+------+----------+---------------------+ | id | select_type | TABLE | partitions | TYPE | possible_keys | KEY | key_len | REF | ROWS | filtered | Extra | +----+-------------+--------+------------+------+-------------------------------------------------------+--------------------------------+---------+-------+------+----------+---------------------+ | 1 | SIMPLE | orders | NULL | REF | idx_orders_customer_id,idx_orders_customer_created_at | idx_orders_customer_created_at | 4 | const | 6 | 100.00 | Backward INDEX scan | +----+-------------+--------+------------+------+-------------------------------------------------------+--------------------------------+---------+-------+------+----------+---------------------+ 1 ROW IN SET, 1 warning (0.00 sec)
The comparison above is the same query after adding a composite index on orders (customer_id, created_at). The plan stops reporting Using filesort because the index can satisfy the lookup and sort order together.
EXPLAIN is an optimizer view, not a latency guarantee. Confirm the improvement with application timings, a slow-log comparison, or the execution-time plan variants on a safe target.