返回首页

MySQL 慢查询 5 轮递进式优化实战 — 10.2s → 9.7ms 完整复盘

📅 创建于 2026-06-17 🔄 更新于 2026-06-17 📝 590 字

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;

肉眼可见的问题:

  1. SELECT * — 查询大量无用字段
  2. DATE(create_time) — 时间字段用函数包裹,索引失效
  3. 两张大表 JOIN,无合理联合索引
  4. 大结果集先关联再排序 → 临时表 + 文件排序

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 优化铁律

  1. 禁止索引列使用函数或隐式类型转换 — 一旦使用直接索引失效,这是线上最高频坑
  2. 严禁 SELECT * — 按需取字段,减少 IO 且能轻松实现覆盖索引
  3. 联合索引遵循最左匹配 — WHERE 条件字段在前,ORDER BY 在后,从根源消灭 filesort
  4. 大表 JOIN 务必先分页再关联 — 不让海量无效数据参与联表
  5. 能走覆盖索引就走覆盖索引 — 回表是隐藏性能杀手

索引不是越多越好。精准贴合 SQL 的联合索引 > 一堆杂乱单字段索引。


慢 SQL 排查标准流程

① 看耗时 → 是否 > 500ms
② EXPLAIN 定位瓶颈 → type / key / Extra 三字段
③ 语法层优化 → 改时间条件、去 SELECT *、优化联表顺序
④ 索引层优化 → 按需建联合索引、覆盖索引,删无用索引
⑤ 复测耗时 → 确认达标,同时观测写入性能无下降

关联页面

页面关联点
mysql-slow-query-case-study慢查询排查案例复盘(通用方法论,companion)
mysql-performance-configMySQL 性能调优总纲
fullstack-performance-troubleshooting全栈排障方法论