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 消耗了最多总时间,这才是优化优先级。

二、索引策略

建什么索引:

不该做什么:

三、分页:深翻页的经典陷阱

-- 慢:需要先扫描并丢弃 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 膨胀。

做法:

五、优化顺序

慢查询日志(找到 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 页 耗时随页码增长的曲线
一个大事务更新十万行 锁等待、主从延迟、回滚耗时
去掉隐式类型转换(字符串字段传数字) 索引是否恢复可用

自测题

  1. EXPLAIN 里 type=ALL 与 Using filesort 分别意味着什么?处理方式相同吗?
  2. 联合索引 (a, b, c) 能加速哪些查询?哪些不行?为什么?
  3. 为什么范围查询列应放在联合索引最后?
  4. 什么情况下你会放弃游标分页?替代方案是什么?
  5. 索引是不是越多越好?写出三条索引的代价。

进入 keel 阅读