第四篇(实战): 订单表索引设计实战:从慢 SQL 到毫秒级

一、为什么后端一定要会"慢 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

    👉 排序是额外做的

数据库真实在干嘛:

  1. 用 idx_user 找到 user_id = 123 的十万条
  2. 再一条条判断 status
  3. 再把结果全部排序
  4. 再跳过 2000 条
  5. 再取 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 能不能再写短一点"

而是:

"数据库是不是还在替我干脏活。"

相关推荐
日月云棠1 天前
各版本JDK对比:JDK 25 特性详解
java
用户8307196840821 天前
Spring Boot 项目中日期处理的最佳实践
java·spring boot
JavaGuide1 天前
Claude Opus 4.6 真的用不起了!我换成了国产 M2.5,实测真香!!
java·spring·ai·claude code
IT探险家1 天前
Java 基本数据类型:8 种原始类型 + 数组 + 6 个新手必踩的坑
java
花花无缺1 天前
搞懂new 关键字(构造函数)和 .builder() 模式(建造者模式)创建对象
java
用户908324602731 天前
Spring Boot + MyBatis-Plus 多租户实战:从数据隔离到权限控制的完整方案
java·后端
桦说编程1 天前
实战分析 ConcurrentHashMap.computeIfAbsent 的锁冲突问题
java·后端·性能优化
爱可生开源社区1 天前
2026 年,优秀的 DBA 需要具备哪些素质?
数据库·人工智能·dba
程序员清风1 天前
用了三年AI,我总结出高效使用AI的3个习惯!
java·后端·面试
beata1 天前
Java基础-13: Java反射机制详解:原理、使用与实战示例
java·后端