一、为什么后端一定要会"慢 SQL 实战"
学索引、学 B+Tree,如果不能落到一个真实 SQL 上,本质上都是"概念"。
真实项目里,慢 SQL 往往长这样:
- 功能很正常
- 代码没报错
- 数据一多,接口直接炸
所以这一篇不讲抽象理论,只做一件事:
👉 带你完整走一遍:发现慢 SQL → explain → 判断瓶颈 → 改索引 + 改 SQL → 再 explain 验证。
二、真实业务场景
一张订单表:
t_order
--------------------------------
id (PK)
user_id
status
amount
create_time
remark
典型业务 SQL(订单列表页):
SELECT *
FROM t_order
WHERE user_id = 123
AND status = 1
ORDER BY create_time DESC
LIMIT 20 OFFSET 2000;
业务含义:
👉 查询某个用户的已完成订单,按时间倒序,翻到第 101 页。
数据量:
👉 t_order 表 500 万行
👉 user_id = 123 大约 10 万条订单
现象:
👉 页面明显卡
👉 接口 RT 几百 ms ~ 几秒
三、第一步:EXPLAIN 看清数据库在干嘛
EXPLAIN
SELECT *
FROM t_order
WHERE user_id = 123
AND status = 1
ORDER BY create_time DESC
LIMIT 20 OFFSET 2000;
典型你会看到类似:
key: idx_user
rows: 100000
Extra: Using where; Using filesort
这三项非常关键:
-
key = idx_user
👉 只用了 user_id 单列索引
-
rows = 100000
👉 扫描了十万行
-
Using filesort
👉 排序是额外做的
数据库真实在干嘛:
- 用 idx_user 找到 user_id = 123 的十万条
- 再一条条判断 status
- 再把结果全部排序
- 再跳过 2000 条
- 再取 20 条
慢是必然的。
四、第二步:先解决"走路方式"(联合索引)
where 条件:
- user_id = ?
- status = ?
order by:
- create_time desc
所以第一版联合索引:
CREATE INDEX idx_user_status_ct
ON t_order(user_id, status, create_time);
再 explain:
key: idx_user_status_ct
rows: 3000
Extra: Using where
变化非常明显:
- 扫描行数从 10 万 → 3000
- 排序消失(顺着索引顺序拿)
👉 这一步解决的是:范围缩小 + 排序走索引
但还没完。
五、第三步:解决"回表成本"(字段 & 覆盖)
原 SQL 是:
SELECT *
这意味着:
👉 每一条都要回表取整行(amount / remark 等大字段)
改为:
SELECT id, status, amount, create_time
FROM t_order
WHERE user_id = 123
AND status = 1
ORDER BY create_time DESC
LIMIT 20 OFFSET 2000;
如果列表页只需要这几个字段,就不要 *。
如果你愿意进一步优化覆盖索引:
CREATE INDEX idx_user_status_ct_amt
ON t_order(user_id, status, create_time, amount);
再 explain,通常会看到:
Extra: Using index
👉 这意味着:不回表(覆盖索引)
六、第四步:真正的性能杀手 ------ 深分页
即使这样,这条 SQL 还有一个结构性问题:
LIMIT 20 OFFSET 2000;
👉 offset 越大,数据库越难受
👉 本质还是:先走过前 2000 条
工程里真正推荐的是:游标分页(基于最后一条记录)
✅ 第一页
SELECT id, status, amount, create_time
FROM t_order
WHERE user_id = 123
AND status = 1
ORDER BY create_time DESC, id DESC
LIMIT 20;
假设最后一条是:
👉 create_time = '2026-01-21 10:00:00'
👉 id = 880001
✅ 下一页(关键 SQL)
SELECT id, status, amount, create_time
FROM t_order
WHERE user_id = 123
AND status = 1
AND (
create_time < '2026-01-21 10:00:00'
OR (create_time = '2026-01-21 10:00:00' AND id < 880001)
)
ORDER BY create_time DESC, id DESC
LIMIT 20;
对应索引:
CREATE INDEX idx_user_status_ct_id
ON t_order(user_id, status, create_time, id);
这条 SQL 的本质是:
👉 从索引树某个位置开始往后扫,不用再跳过前 N 条。
这一步,通常是:
👉 秒级 → 毫秒级
七、完整优化路线总结
这一条慢 SQL,实际做了四层优化:
✅ 第一层:让 where + order by 走同一棵索引
(联合索引)
✅ 第二层:减少回表
(字段收敛 / 覆盖索引)
✅ 第三层:干掉 filesort
(排序字段进索引)
✅ 第四层:干掉 offset
(游标分页)
八、后端在 explain 里真正该盯的 4 个点
每次看执行计划,优先级永远是:
1️⃣ key:走了哪棵索引
2️⃣ rows:大概扫了多少行
3️⃣ Extra:有没有 filesort / temporary / using index
4️⃣ 是否存在回表路径
九、给第五课的一句"工程总结"
👉 慢 SQL 优化,本质不是调参数,而是重构"数据的查找路径"。
你要做的不是:
"SQL 能不能再写短一点"
而是:
"数据库是不是还在替我干脏活。"