2330 字
约 7 分钟
21
S11-A18 · SQL 评审该看什么:五种高频问题
2024-12-31
2026-11-09

一次 SQL 评审平均花七分钟。七分钟里,评审者通常在看"这段 SQL 语法对不对、字段名有没有拼错",而真正导致线上事故的五类问题,几乎从不在评审里被提到。

SQL 的评审价值不在正确性——语法错误跑一次就暴露了。它的价值在那些能通过测试、只在上量后才出问题的地方:索引是否可用、扫描行数会不会失控、事务边界是否过长、锁的范围有多大。


1. 五种该拦下的问题

问题 表现 上量后的后果
索引失效形态 函数包裹列、隐式类型转换、前导通配 全表扫描
分页深翻 LIMIT 100000, 20 越翻越慢
范围过大 无时间边界或边界过宽 扫描量不可控
事务内过多操作 事务里含远程调用或多表大更新 锁持有时间长
隐式锁升级 批量更新未分批 锁等待与死锁

第一项与第四项是"上量后才出现"的典型:小数据量时函数包裹的列照样走全表扫描,但全表只有几千行,一次几十毫秒,没人会注意;数据到千万行后,同一条 SQL 变成几秒。

第二条是中文互联网上被讨论最多、也最常被重复踩的一条:偏移分页的成本与偏移量成正比,翻到第一万页时要扫一万零二十行。评审时看到 LIMIT 后面那个数字很大,就该拦。

2. 索引失效的四种具体形态

形态 例子 修法
列被函数包裹 WHERE DATE(created_at) = '2026-11-10' 改为范围条件
隐式类型转换 字符串列与数字比较 统一类型
前导通配 LIKE '%abc' 反向索引或搜索引擎
复合索引断链 跳过最左列 调整条件或索引

第三项(前导通配)有个替代思路:如果检索确实是"包含匹配",那么关系库不是合适的工具,该用倒排索引(搜索引擎)而不是在库里硬撑。评审时看到 LIKE '%x%',先问"为什么需要中间匹配",而不是问"能不能加索引"。

第一项最容易被忽视,因为它"看起来完全正常",且在小数据量下执行很快。它的问题在于:DATE(created_at) 让索引无法用于过滤,只能作为函数计算后的结果参与。

-- 失效:函数包裹列,索引不可用
SELECT * FROM orders WHERE DATE(created_at) = '2026-11-10';

-- 有效:改成范围,索引可用
SELECT * FROM orders
WHERE created_at >= '2026-11-10' AND created_at < '2026-11-11';

3. 深分页的三种替代

方式 做法 适用
游标分页 用上一页最后一条的排序列值作为起点 列表类、可按序
延迟关联 先用索引取 ID,再回表取整行 需要总数与跳页
限制最大页数 只允许翻到第 N 页 后台管理类

游标分页有个前提容易被忽略:排序列必须唯一或近似唯一。用 created_at 排序时若有同秒记录,翻页会出现漏行或重复。可行做法是复合游标(时间 + ID)。

-- 复合游标:避免同值排序导致的漏行
SELECT id, amount FROM orders
WHERE (created_at, id) < ('2026-11-01 10:00:00', 88912)
ORDER BY created_at DESC, id DESC
LIMIT 20;
-- 需要 (created_at, id) 复合索引支撑,否则退化为范围扫描

第一种是标准解法,代价是不能随机跳页。第二种保留了跳页能力,仍要扫偏移量,只是减少了回表成本。

-- 深分页:先用覆盖索引取 ID,再回表
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders
      WHERE status = 'PAID'
      ORDER BY created_at DESC
      LIMIT 100000, 20) t ON o.id = t.id;
-- 内层只扫索引,不取整行;外层按 20 个 ID 回表

4. 事务边界与锁

问题 机制 修法
事务含远程调用 网络等待期间持有锁 把调用移出事务
事务内批量更新多行 行锁数量随行数增长 分批提交
事务里做查询再更新 间隙锁与范围锁 缩短范围或改隔离级别
长事务 undo 无法回收 拆分或异步化

第二项(批量更新多行)的判断有个简单口径:一次 UPDATE 影响的行数上界是多少。答不出上界,就不该合并成一条语句;答案超过几千,就该分批。

第一项是最常见的结构性错误:事务里调用第三方接口(发短信、调支付),网络一慢,锁就持有几十秒,其它请求全部排队。

5. 评审时该问的五个问题

// SQL 评审五问,按顺序,前三个必须回答
1. 这条查询走哪个索引?给 EXPLAIN 的 type 与 rows。
2. 最坏情况扫多少行?上界是多少?
3. 有没有范围没有上界(如只有 >= 没有 <)?
4. 这段在事务里吗?事务里还有别的操作吗?
5. 更新/删除影响多少行?会不会一次几万行?

第一问要求给出 EXPLAIN,而不是"应该走索引"。"应该"是评审里最没信息量的词,它既不能验证也不承担责任。

-- 评审材料:把执行计划贴进 PR,而不是口头保证
EXPLAIN FORMAT=JSON
SELECT id, amount FROM orders
WHERE user_id = 42 AND status = 'PAID' AND created_at >= '2026-01-01'
ORDER BY created_at DESC LIMIT 20;
-- 关注:type(ref/range 而非 ALL)、rows 估算、是否 Using filesort

评审材料的最小集合可以固化成 PR 模板,五格填完才允许合并:

// SQL 变更 PR 模板
索引:走 (user_id, status, created_at),EXPLAIN type=range, rows≈180
上界:单用户单状态最近一年,估算 ≤ 5000 行
边界:created_at 有上下界;status 为枚举,无范围
事务:不在事务内,单条只读
影响行数:不涉及写

6. 排错:五种"评审通过后慢查询"

现象 成因 处理方向
上线后变慢 数据量增长后索引选择性下降 看 rows 估算与统计信息
偶发慢 执行计划被数据分布影响 固定计划或强制索引
高峰期锁等待 事务过长或更新范围大 拆分事务、分批更新
分页越翻越慢 深偏移 改游标分页
单条 SQL 拖垮库 无边界扫描 加时间与状态边界

排错时还有一个高效手段:拿生产的数据分布做一次抽样执行。在只读副本上跑同一条 SQL 并看 EXPLAIN ANALYZE 的真实行数,比在测试库上跑一万遍都有用——因为差异往往来自数据分布而不是 SQL 写法。

第二项在评审时最难预见:同一段 SQL 在不同数据分布下可能选不同计划。评审能做的只有两件事——确认最坏情况的扫描上界,以及要求有慢查询告警覆盖。

7. 边界:什么情况可以简化评审

场景 为什么 做法
小表(万行内) 扫描成本可忽略 只看正确性
按主键点查 计划稳定 快速通过
只读报表库 不影响在线 只关注资源占用
一次性数据修复 有明确时间窗 关注分批与回滚

还有一条与流程有关的边界:评审不能替代慢查询监控。评审只能拦住"明显没有上界"的写法,拦不住"数据分布变化导致计划改变"。所以评审与监控是两道防线,缺一道都会漏。

判断口径一句话:这条 SQL 的最坏扫描量有没有上界。有上界且可接受,评审可以简化;没有上界,它就是一颗定时炸弹,无论当前数据量多小。


8. 自测

  1. SQL 评审的价值为什么不在"正确性"上?语法错误靠什么暴露?
  2. 五种该拦的问题里,哪一类最常被"看起来正常"掩盖?它的具体形态是什么?
  3. 深分页成本与偏移量成正比,三种替代方式各自的代价是什么?
  4. 事务里调用第三方接口为什么会造成锁等待?正确做法是什么?
  5. 评审五问里,为什么要求给出 EXPLAIN 而不是"应该走索引"?
  6. 判断"能否简化 SQL 评审"的那个问题是什么?为什么它与当前数据量无关?
S11-A18 · SQL 评审该看什么:五种高频问题
https://www.clxhxhhr.top/posts/6413/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。