一次加索引建议的复核记录:从报告到签字
这份复核记录从一个报告开始,到一张签字单结束。报告的建议是给 trade_flow 加 (status, created_at) 索引;我的处理是先退回补证据,再按结果集、计划、写入副作用三步验,过了才排窗口。
记录按“报告、验证、签字、归档”四段写。文中的表名、行数、延迟、索引大小都是示例环境,只用于说明复核方法,不代表任何环境的结果。
一、报告:建议具体,证据不齐
报告写得很具体:trade_flow 表的 status 字段选择性好,示例环境里 status = 'SUCCESS' 约占 3%;建议加 (status, created_at) 联合索引;相关查询一小时约 200 次;表每分钟新增 1.2 万行,另有批量更新。
我拿到报告先看五样:SQL 原文和参数、采样窗口、现有索引清单、写入基线、回滚方案。报告只给了前三个数,后两样没有。没有写入基线,没法判断索引维护成本;没有回滚方案,出了问题只能现场想办法。
值班记录
现象:报告有选择性、频次、写入量,没有 SQL 原文、采样窗口、现有索引清单和回滚脚本。
动作:退回补充,暂不排窗口。
结论:报告可以立项,不能签字。
退回不是否定这份建议。它说明这条 SQL 值得看,但报告还不足以支撑一次变更。退回时我写了三行:缺 SQL 原文、缺写入基线、缺回滚脚本;补完交给复核人,不用重新走一遍立项。
二、补材料:把“相关查询”变成一条 SQL
退回后补上来的材料包括:SQL 原文、绑定参数、采样窗口、现有索引清单、写入峰值时段、回滚脚本草稿。这里最容易被忽略的是采样窗口。同一个 3%,来自业务低峰和来自跑批高峰,含义不一样。
补材料时我把基线也定了下来:
| 基线项 | 记录方式 | 用途 |
|---|---|---|
| SQL 原文与参数 | 原样存档,参数用占位符 | 复现同一条查询 |
| 数据快照时间 | 记录副本的数据时间点 | 保证前后比的是同一份数据 |
| 结果行数与校验和 | 加索引前保存一份 | 后面双向比对 |
| 执行时间 | 三次中位数,写清缓存和并发 | 避免拿单次波动当结论 |
| 缓冲区读取 | 记录读了多少块 | 判断扫描量有没有真的下降 |
基线不是形式。没有基线,后面的“快了”没有参照;没有快照时间,前后比的可能是两份不同的数据。
补材料的过程中还发现一件事:现有索引里有一个列序相近的索引。它不一定能被新索引替代,但要在变更单里写清两者的关系。加新索引之前先看旧的还有没有使用,加完之后再看一次,避免白占空间和维护成本。
三、验证一:结果集双向比对
读验证在只读副本上做。先保存加索引前的结果,再在加索引后跑同一个查询。示例写法,语法因库而异:
-- 1) 保存基线
CREATE TABLE baseline_trade_flow AS
SELECT id, account_id, amount
FROM trade_flow
WHERE status = 'SUCCESS'
AND created_at >= :start_time
AND created_at < :end_time;
-- 2) 加索引后,双向比对
SELECT 'baseline_minus_after' AS direction, COUNT(*) AS diff_rows
FROM (
SELECT * FROM baseline_trade_flow
EXCEPT
SELECT id, account_id, amount FROM trade_flow
WHERE status = 'SUCCESS'
AND created_at >= :start_time
AND created_at < :end_time
) d
UNION ALL
SELECT 'after_minus_baseline', COUNT(*)
FROM (
SELECT id, account_id, amount FROM trade_flow
WHERE status = 'SUCCESS'
AND created_at >= :start_time
AND created_at < :end_time
EXCEPT
SELECT * FROM baseline_trade_flow
) d;
两个方向都是 0,才进入下一步。还要看行数、校验和、时间端点、NULL、重复行。索引改变访问路径,不应该改变结果;结果变了,先查语义,不要继续看性能。
这一步的复核意见要写进记录:比对了哪两个方向、结果行数是多少、校验和是否一致、边界条件造了哪几个。只写“结果一致”不够,下一次别人没法复现。
四、验证二:计划看四个位置
结果集一致后,跑 EXPLAIN ANALYZE。示例写法,语法因库而异:
EXPLAIN ANALYZE
SELECT id, account_id, amount
FROM trade_flow
WHERE status = 'SUCCESS'
AND created_at >= :start_time
AND created_at < :end_time;
看四个位置:访问方式是全表扫描还是索引扫描;条件是进入 Index Cond 还是只在 Filter;rows 估算与实际差多少;buffers 读了多少块。条件只在 Filter 里,说明索引没有参与访问,耗时可能没变,甚至更慢。
计划通过的标准不是“节点名好看”,而是条件真的进了索引访问,估算和实际没有数量级偏差。偏差大,先查统计信息的时间点,再考虑索引列序。
还要看回表。索引扫描读的块数如果没降,说明读放大只是换了个位置;回表次数高,可能是索引没有覆盖查询列。遇到这种情况,先别签,把计划贴进复核记录,交给下一轮判断。
五、验证三:写入副作用与回滚
只读副本只能验读。写入副作用在影子库或低峰小范围测。示例环境的前后对比:
| 指标 | 加索引前 | 加索引后 | 口径 |
|---|---|---|---|
| 写入 p99 | 18 ms | 31 ms | 示例环境,1.2 万行/分钟 |
| 复制延迟 | 0.4 s | 0.9 s | 影子库,示例值 |
| 索引大小 | — | 2.3 GB | 示例环境 |
示例里 p99 从 18 ms 升到 31 ms,复制延迟从 0.4 s 升到 0.9 s。这个幅度能不能接受,要看变更单里提前写的阈值。阈值没写,观察期就没有判据。
在线建索引(各库写法不同)的步骤要单独演练。执行前确认长事务、锁等待、磁盘空间;执行后查索引是否有效,不要只看命令返回。失败后可能留下无效索引,清理方案要写在变更单里。
回滚脚本示例写法,语法因库而异:
-- 回滚:按各库的在线删除方式执行
DROP INDEX idx_trade_status_created;
回滚不是一句“有问题就退”。要写清执行人、预计耗时、执行后怎么确认索引已删除、业务查询有没有恢复。观察期至少覆盖一个完整业务日和一个跑批窗口。
值班记录
现象:结果集双向为 0,计划进入 Index Cond;影子库写入 p99 从 18 ms 升到 31 ms,复制延迟从 0.4 s 升到 0.9 s(示例值)。
动作:把观察期、阈值、回滚执行人写进变更单;旧索引保留,不在窗口内删除。
结论:读通过,写入代价在阈值内,附条件签字。
六、签字:六步复核单
| 顺序 | 复核项 | 通过 | 退回或不签 |
|---|---|---|---|
| 1 | SQL、参数、频次、写入量 | 三个数有来源,能复现 | 只有报告结论 |
| 2 | 结果集 | 双向差异为 0 | 只比耗时 |
| 3 | 执行计划 | 条件进 Index Cond,估算接近 | 只看节点名 |
| 4 | 写入副作用 | p99、复制延迟在阈值内 | 没测写入 |
| 5 | 回滚与观察期 | 脚本可执行,覆盖业务日/跑批 | 只有“可回滚” |
| 6 | 签字与归档 | 建议、执行、复核都能追到人 | 退回 |
签字不是签“同意加索引”,是签“证据齐了、条件写清了、出了问题知道怎么退”。这三句话缺一句,我就不签。
签字页还要写适用条件:这套结论只对这份数据快照、这个写入量级、这个观察期成立。换个业务高峰、换张表、换个写入比例,要重新验,不能把这次的签字复制到下一次。
七、归档:把证据留在同一个地方
归档清单:
- 报告原文与退回意见
- SQL 原文、参数、采样窗口
- 加索引前的基线结果与校验和
- 加索引前后的执行计划
- 写入压测的 p99、复制延迟、索引大小
- 回滚脚本与执行人
- 观察期阈值与复盘结论
归档的目的不是留痕好看,是下一次遇到同类建议时,能拿出上一次的基线和阈值,不用从零开始争。复核记录写得越具体,下一次退回或签字就越快。
归档时把“通过”和“退回或不签”的理由都留下。只留通过的记录,下一次还会在同一个地方卡住;把退回理由写清楚,后来的人才知道该补什么。

八、适用条件与规则
这套复核适用于有只读副本、写入量可观测、能安排观察期的业务库。如果表是纯写入流水、查询又是偶发,先算收益;如果窗口很短,回滚和观察指标要提前准备。不同库的在线建索引写法不同,步骤要按所在库的文档改写。
规则:报告可以立项,证据齐了才签字;结果集、计划、写入副作用、回滚,四项缺一不排窗口。
同样的现象可能有别的根因,欢迎带数字来讨论。
原文地址: https://www.cveoy.top/t/topic/qHV0 著作权归作者所有。请勿转载和采集!