EXPLAIN SELECT*FROM orders WHERE status ='completed'AND create_time >'2026-01-01';
输出:
1 2 3 4 5
+----+-------------+--------+-------+---------------+------------+---------+------+---------+-----------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+------------+---------+------+---------+-----------------------+ | 1 | SIMPLE | orders | range | idx_status | idx_status | 4 | const| 4750000 | Using index condition | +----+-------------+--------+-------+---------------+------------+---------+------+---------+-----------------------+
优化器估算出 475 万行,实际就是全表的 95%,回表开销极大。
诊断思路
1 2 3 4
-- 查看 Trace 中的代价对比 SET optimizer_trace="enabled=on"; SELECT*FROM orders WHERE status ='completed'AND create_time >'2026-01-01'; SELECT*FROM information_schema.optimizer_trace\G
-- 方案1:用 Hint 强制全表扫描(临时方案) SELECT*FROM orders IGNORE INDEX(idx_status) WHERE status ='completed'AND create_time >'2026-01-01';
-- 方案2:增加优化器内存限制,进行完整评估 SET SESSION range_optimizer_max_mem_size =16777216;
-- 方案3:创建覆盖索引(根治方案) CREATE INDEX idx_status_time ON orders(status, create_time);
验证修复效果:
1 2 3
EXPLAIN SELECT*FROM orders WHERE status ='completed'AND create_time >'2026-01-01'; -- type: ALL, rows: 5000000, Extra: Using where -- 性能提升约 3-5 倍(取决于回表开销)
五、实战案例二:派生表合并失败
问题场景
子查询本该自动合并到外层(Derived Merge),但优化器没有做合并,导致生成了临时派生表。
1 2 3 4 5 6 7
EXPLAIN SELECT o.id, o.amount FROM orders o JOIN ( SELECT customer_id, MAX(order_date) AS last_date FROM orders GROUPBY customer_id ) recent ON o.customer_id = recent.customer_id AND o.order_date = recent.last_date;
输出中出现 <derived2>:
1 2 3 4 5 6 7
+----+-------------+------------+------+---------------+------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+------------+------+---------------+------+---------+------+------+-------------+ | 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 5000 | NULL | | 1 | PRIMARY | o | ref | idx_cust_date | idx_cust_date | 4 | recent.customer_id | 100 | Using where | | 2 | DERIVED | orders | index| idx_cust_date | idx_cust_date | 8 | NULL | 50000| Using index | +----+-------------+------------+------+---------------+------+---------+------+------+-------------+
Derived Table 无法合并通常因为以下原因:
子查询包含 GROUP BY、DISTINCT
子查询有 LIMIT 且没有 ORDER BY
子查询使用了聚合函数(MAX、SUM 等)
诊断与改写
1 2 3 4 5 6 7 8 9
-- 手工改写为 JOIN + 窗口函数,避免派生表 EXPLAIN WITH ranked AS ( SELECT id, customer_id, amount, order_date, ROW_NUMBER() OVER (PARTITIONBY customer_id ORDERBY order_date DESC) AS rn FROM orders ) SELECT id, amount FROM ranked WHERE rn =1;
执行计划:
1 2 3 4 5
+----+-------------+--------+----------+----------------+----------------+---------+------+-------+-----------------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+----------+----------------+----------------+---------+------+-------+-----------------------------------+ | 1 | PRIMARY | orders | index | idx_cust_date | idx_cust_date | 8 | NULL | 50000 | Using index; Using filesort | +----+-------------+--------+----------+----------------+----------------+---------+------+-------+-----------------------------------+
窗口函数避免了派生表物化,执行效率通常优于子查询方案。运行环境:MySQL 8.0.33。
六、实战案例三:Multi-Range Read 未生效
问题场景
回表操作是随机 IO,MRR(Multi-Range Read)能把随机 IO 变成顺序 IO,大幅提升大数据量回表的性能。但你发现 EXPLAIN 中 Extra 没有 Using MRR。
1
EXPLAIN SELECT*FROM orders WHERE customer_id BETWEEN100AND2000;
输出缺少 MRR 标志:
1 2 3 4 5
+----+-------------+--------+-------+-----------------+-----------------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+-----------------+-----------------+---------+------+------+-------------+ | 1 | SIMPLE | orders | range | idx_customer_id | idx_customer_id | 4 | NULL | 2000 | Using where | +----+-------------+--------+-------+-----------------+-----------------+---------+------+------+-------------+
MRR 未生效的原因
MySQL 的 MRR 默认不会自动启用,需要设置 optimizer_switch 中的两个标志:
1 2 3 4 5
-- 查看当前配置 SELECT @@optimizer_switch\G
-- 开启 MRR 和基于代价的 MRR SET optimizer_switch='mrr=on,mrr_cost_based=off';
EXPLAIN SELECT/*+ JOIN_ORDER(c, p, o) */ o.id FROM orders o JOIN products p ON o.product_id = p.id JOIN categories c ON p.category_id = c.id WHERE c.name IN ('电器', '服装');
通过在 Trace 中确认最优顺序后,用 Hint 固化下来,避免优化器因统计信息波动而改变执行计划。