KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
01 · 索引与查询调优 — keel 龙骨
这一章回答:慢查询为什么慢,以及看懂执行计划之后该做什么。
这一章回答:慢查询为什么慢,以及看懂执行计划之后该做什么。
数据库调优的第一原则:先定位,再优化。凭直觉改 SQL 和加索引,多数情况下只是把压力从一个地方挪到另一个地方。所有优化动作都必须以 EXPLAIN 的输出为依据。
一、怎么看执行计划
拿到一条慢 SQL,第一步永远是:
EXPLAIN SELECT * FROM orders WHERE tenant_id = 42 AND status = 'PAID' ORDER BY created_at DESC LIMIT 20;
重点看四列:
| 列 | 看什么 | 危险信号 |
|---|---|---|
type |
访问方式 | ALL(全表扫描)通常要处理;目标至少到 range,理想是 ref/const |
key |
实际用到的索引 | 为 NULL 说明没走索引 |
rows |
预估扫描行数 | 远大于返回行数说明过滤效率低 |
Extra |
附加信息 | 出现 Using filesort、Using temporary 需要关注;出现 Using index 是好事(覆盖索引) |
补充工具:EXPLAIN ANALYZE(真实执行并给出实际耗时)、慢查询日志 + pt-query-digest 做聚合分析——后者能直接告诉你哪几条 SQL 消耗了最多总时间,这才是优化优先级。
二、索引策略
建什么索引:
- 联合索引遵循最左前缀:
(tenant_id, status, created_at)能服务tenant_id、tenant_id+status、tenant_id+status+created_at三种查询,但不能单独服务status; - 区分度高的字段放前面,除非有排序/范围需求;
- 覆盖索引:如果索引里包含了查询需要的所有列,
Extra会出现Using index,省掉回表。对高频热点查询尤其有效; - 范围查询字段放在联合索引的最后(它之后的列无法再用于过滤)。
不该做什么:
- ❌ 索引列上做函数或运算:
WHERE DATE(created_at) = '2026-01-01'会让索引失效,改成范围查询created_at >= '2026-01-01' AND created_at < '2026-01-02'; - ❌ 隐式类型转换(字符串字段传数字、不同字符集比较);
- ❌
LIKE '%xxx'前置通配符; - ❌ 无脑给每个字段都建单列索引——每多一个索引就多一份写放大,经验上限单表不超过 5 个;
- ❌
SELECT *:能用覆盖索引的场景被它毁掉,还增加网络与序列化开销。
三、分页:深翻页的经典陷阱
-- 慢:需要先扫描并丢弃 10000 行
SELECT * FROM orders ORDER BY id LIMIT 10000, 20;
-- 快:从上次的位置继续
SELECT * FROM orders WHERE id > 10000 ORDER BY id LIMIT 20;
第二种写法叫游标分页 / Keyset pagination,依赖一个有序且唯一的列(通常是自增主键)。
取舍:游标分页快且稳定,但不能跳转到任意页。如果产品要求"跳到第 500 页",可以先用覆盖索引取出目标位置的 id,再回表取数据;或者接受深翻页的性能损耗并对页码设上限。
另外:强制分页。所有列表接口必须默认分页,且单页大小有上限。这不只是性能问题,也是防止一次请求拖垮数据库的护栏。
四、大事务拆分
一个大事务一次更新十万行会带来:长时间持锁、主从复制延迟、回滚代价巨大、undo log 膨胀。
做法:
- 按批次处理,每批
LIMIT 1000~5000,批与批之间提交并留出间隔; - 批处理要记录进度,支持中断后续跑;
- 尽量把事务安排在低峰期。
五、优化顺序
慢查询日志(找到 Top SQL)
→ EXPLAIN(判断是全表扫描 / 索引失效 / 排序临时表 / 深翻页)
→ 针对性处理(改 SQL / 加或改索引 / 改分页 / 拆事务)
→ 再 EXPLAIN + 压测对比
→ 记录前后数据
动手:可观察结果
| 产出 | 判断标准 |
|---|---|
| 一张百万行测试表 + 慢 SQL | 能用 EXPLAIN 指出问题类型,而不是猜测 |
| 一次索引优化记录 | 优化前后:type、rows、实际耗时三列对比(例如 800ms → 50ms) |
| 深翻页对比 | LIMIT 10000,20 与游标分页的耗时对比曲线 |
pt-query-digest 报告 |
输出消耗总时间前 5 的 SQL 清单 |
完成标志:能对着任意一条慢 SQL 说出它的瓶颈(type 或 Extra 哪一列暴露了问题),并给出对应的改法。
故障注入
| 注入方式 | 观察 |
|---|---|
在索引列上套 DATE() 函数 |
EXPLAIN 中 key 是否变成 NULL、耗时变化 |
| 给每个字段都建单列索引 | 写入耗时与磁盘占用的变化 |
去掉 ORDER BY 字段的索引 |
Extra 是否出现 Using filesort |
| 深翻到第 10000 页 | 耗时随页码增长的曲线 |
| 一个大事务更新十万行 | 锁等待、主从延迟、回滚耗时 |
| 去掉隐式类型转换(字符串字段传数字) | 索引是否恢复可用 |
自测题
EXPLAIN里type=ALL与Using filesort分别意味着什么?处理方式相同吗?- 联合索引
(a, b, c)能加速哪些查询?哪些不行?为什么? - 为什么范围查询列应放在联合索引最后?
- 什么情况下你会放弃游标分页?替代方案是什么?
- 索引是不是越多越好?写出三条索引的代价。