MySQL 慢查询 5 轮递进式优化实战 — 10.2s → 9.7ms
来源:码农程序员 | 发布日期:2026-06-15
一条订单统计 SQL 导致接口全线超时,通过 5 轮递进式优化,从 10.2s 降到 9.7ms。每轮附带执行计划解读、根因分析和耗时变化,拒绝盲目加索引。
业务背景
电商后台每日对账报表:查近 30 天已支付有效订单,关联订单详情表,按创建时间倒序分页。
| 表 | 数据量 | 用途 |
|---|---|---|
order_main |
230w 行 | 订单主表 |
order_item |
580w 行 | 订单明细表 |
原始 SQL(耗时 10.2s):
SELECT *
FROM order_main om
LEFT JOIN order_item oi ON om.order_no = oi.order_no
WHERE DATE(om.create_time) >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
AND om.order_status = 1
ORDER BY om.create_time DESC
LIMIT 0, 20;
肉眼可见的问题:
SELECT *— 查询大量无用字段DATE(create_time)— 时间字段用函数包裹,索引失效- 两张大表 JOIN,无合理联合索引
- 大结果集先关联再排序 → 临时表 + 文件排序
EXPLAIN 执行计划分析
EXPLAIN SELECT ...(同上);
| 字段 | 值 | 结论 |
|---|---|---|
type |
ALL | 两张表均全表扫描 |
key |
NULL | 未使用任何索引 |
Extra |
Using temporary; Using filesort | 临时表 + 文件排序 |
rows |
229w + 576w | 海量 IO 读取 |
核心结论: 索引失效 + 全表扫描 + 临时表文件排序 + 无效字段查询 — 四重暴击。
五轮递进式优化
✅ 第一轮:解除索引函数失效
根因: DATE(create_time) 使时间索引彻底失效。
优化: 去掉函数包裹,直接范围比较。
WHERE om.create_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
结果: 10.2s → 5.6s(+45%)
全表扫描依旧,只是减少了函数计算开销。
✅ 第二轮:SELECT * → 按需查 + 加单字段索引
根因: SELECT * 产生大量无用 IO;无业务索引。
新增索引:
CREATE INDEX idx_order_status_time ON order_main(order_status, create_time);
CREATE INDEX idx_item_order_no ON order_item(order_no);
优化后 SQL(只查需要的字段):
SELECT om.order_no, om.create_time, om.total_amount, om.order_status,
oi.goods_name, oi.goods_num
FROM order_main om
LEFT JOIN order_item oi ON om.order_no = oi.order_no
WHERE om.create_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
AND om.order_status = 1
ORDER BY om.create_time DESC
LIMIT 0, 20;
结果: 5.6s → 1.8s
type 从 ALL 变为 range,但 Extra 仍有 Using temporary; Using filesort。
✅ 第三轮:联合索引,消灭文件排序
根因: 两个独立单字段索引,MySQL 无法合并,排序仍用临时表 + 文件排序。
优化: 遵循最左匹配,建联合索引覆盖 WHERE + ORDER BY。
DROP INDEX idx_order_status_time ON order_main;
CREATE INDEX idx_order_status_ctime ON order_main(order_status, create_time);
结果: 1.8s → 420ms
Extra 中的 Using temporary; Using filesort 彻底消失。索引直接有序。
✅ 第四轮:先分页再 JOIN
根因: 先全量关联两张大表(230w + 580w)再分页,中间结果集巨大。
优化: 子查询先分页取 20 条,再关联明细表。
SELECT om.order_no, om.create_time, om.total_amount, om.order_status,
oi.goods_name, oi.goods_num
FROM (
SELECT order_no, create_time, total_amount, order_status
FROM order_main
WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
AND order_status = 1
ORDER BY create_time DESC
LIMIT 0, 20
) om
LEFT JOIN order_item oi ON om.order_no = oi.order_no;
结果: 420ms → 48ms
只关联 20 条数据,彻底杜绝大表全量 JOIN。
✅ 第五轮:覆盖索引,杜绝回表
根因: 联合索引只包含 status, create_time,还需回表查 order_no, total_amount。
优化: 把 SELECT 所需字段全部放入索引。
DROP INDEX idx_order_status_ctime ON order_main;
CREATE INDEX idx_order_cover ON order_main(order_status, create_time, order_no, total_amount);
结果: 48ms → 9.7ms
Extra 出现 Using index,全程无回表、无排序、无临时表、无全表扫描。
全流程耗时汇总
| 优化阶段 | 核心动作 | 耗时 | 解决的核心问题 |
|---|---|---|---|
| 原始 | 无 | 10.2s | — |
| 第一轮 | 规避索引字段函数 | 5.6s | 索引函数失效 |
| 第二轮 | 去 SELECT * + 加单索引 | 1.8s | 减少 IO、告别全表扫描 |
| 第三轮 | 优化联合索引 | 420ms | 消灭临时表、文件排序 |
| 第四轮 | 先分页后 JOIN | 48ms | 避免大表无脑关联 |
| 第五轮 | 构建覆盖索引 | 9.7ms | 杜绝回表查询 |
5 条通用 SQL 优化铁律
- 禁止索引列使用函数或隐式类型转换 — 一旦使用直接索引失效,这是线上最高频坑
- 严禁
SELECT *— 按需取字段,减少 IO 且能轻松实现覆盖索引 - 联合索引遵循最左匹配 — WHERE 条件字段在前,ORDER BY 在后,从根源消灭 filesort
- 大表 JOIN 务必先分页再关联 — 不让海量无效数据参与联表
- 能走覆盖索引就走覆盖索引 — 回表是隐藏性能杀手
索引不是越多越好。精准贴合 SQL 的联合索引 > 一堆杂乱单字段索引。
慢 SQL 排查标准流程
① 看耗时 → 是否 > 500ms
② EXPLAIN 定位瓶颈 → type / key / Extra 三字段
③ 语法层优化 → 改时间条件、去 SELECT *、优化联表顺序
④ 索引层优化 → 按需建联合索引、覆盖索引,删无用索引
⑤ 复测耗时 → 确认达标,同时观测写入性能无下降
关联页面
| 页面 | 关联点 |
|---|---|
| mysql-slow-query-case-study | 慢查询排查案例复盘(通用方法论,companion) |
| mysql-performance-config | MySQL 性能调优总纲 |
| fullstack-performance-troubleshooting | 全栈排障方法论 |