结论先行:你缺的不是"公式",而是一套分层采样 → 归因拆解 → 折算决策的方法论。下面按你期望的三个问题逐条给出可落地的步骤。
一、可复现的量化方法:从 InnoDB 层拆出"每行 DML 多花多少"
核心思路:用 performance_schema 的 stage/IO 事件做差值法,把"加索引前后"的写入成本拆成 redo 写入、page IO、B+ 树维护三块。
步骤 1:采样窗口(建议 ≥10 分钟、避开批跑)
-- 采样前清零计数器
TRUNCATE TABLE performance_schema.events_waits_summary_global_by_event_name;
-- 记录基线
SELECT event_name, count_star AS cnt, sum_timer_wait / 1000000000 AS total_ms
FROM performance_schema.events_waits_summary_global_by_event_name
WHERE event_name IN (
'wait/io/file/innodb/ib_log_file', -- redo log 刷盘
'wait/io/table/innodb/index/t_task#idx_status_priority_created', -- 新增索引页IO
'wait/io/table/innodb/index/t_task#PENDING#PRIMARY', -- 聚簇索引页IO
'wait/io/table/innodb/table/t_task' -- 表级 IO
)
ORDER BY total_ms DESC;
等 10 分钟采样窗口结束后再查一次,取差值。
步骤 2:换算"每行多花多少 µs"
Δredo_bytes = (cnt_new - cnt_old) × 平均 redo 记录大小
-- 对 INSERT,每条 redo ≈ 16B header + 行宽 + 每个二级索引各 ~60B(含 B+ 树定位)
-- 你单行 60 字段 ≈ 300B,每个二级索引 redo ≈ 40–80B
Δpage_io = 新增索引的 index_write 增量 / innodb_buffer_pool_size 命中率
-- hit rate 99.9% → 实际物理 IO 极少,主要成本在 CPU(B+ 树定位 + 页锁)
per_row_overhead_µs = (Δtotal_ms / 1000) / (Δrow_count)
经验值参考(16C64G、buffer pool 足够大、行宽 300B):
- 每多一个二级索引,INSERT 每行多花 8–20 µs(主要是 B+ 树 page split 概率 + redo 多写 60–80B)
- UPDATE 多花 15–40 µs(多一次索引定位 + 多写一条 redo)
你的数据:1200 QPS → 1020 QPS,P99 从 45→58ms。58−45=13ms 的 P99 增量,除以 ~1200 QPS 的并发窗口,单行增量约 11 µs——和上面经验值吻合,说明瓶颈确实是每行多写的 redo + B+ 树维护,不是锁竞争(你已经排除了)。
步骤 3:用 SHOW ENGINE INNODB STATUS 抓 B+ 树分裂证据
SHOW ENGINE INNODB STATUS\G
看 INSERT BUFFER AND ADAPTIVE HASH INDEX 段和 SEMAPHORES 段。如果 log i/o size 在加索引后明显增大(比如从 16KB 变成 32KB),说明 redo 写入确实变宽了。
二、算账公式:读收益 vs 写成本怎么放同一把尺子
核心公式(TCO 折算):
索引净收益 = (读 QPS × Δ读延迟 × 读权重) − (写 QPS × Δ写延迟 × 写权重)
代入你的数据:
- 读收益:假设该慢查询占读 QPS 的 5%(约 60 QPS),Δ读延迟 = 480−35 = 445ms → 60 × 445ms = 26,700 ms·QPS
- 写成本:1200 QPS × 13ms = 15,600 ms·QPS
净收益 = 26,700 − 15,600 = +11,100 ms·QPS → 这个索引值得留。
经验阈值(我团队的判断标准)
| 指标 | 保留索引 | 考虑删除/替代 | |------|---------|-------------| | 读 QPS 占比 | > 5% 的读走该索引 | < 2% 的读走该索引 | | 写 QPS 占比 | 写 QPS < 读 QPS 的 3 倍 | 写 QPS > 读 QPS 的 5 倍 | | 读 P99 降幅 | > 50% | < 20% | | 写 P99 涨幅 | < 30% | > 50% | | 索引体积 | < 表体积的 20% | > 表体积的 40% |
你的场景(读收益 445ms、写涨幅 13ms/1200 QPS)明显落在"保留"区间。
什么时候该删旧索引或分表?
- 删旧索引:如果
idx_status(单列)已经被idx_status_priority_created(联合索引)完全覆盖(即前缀包含),直接ALTER TABLE t_task DROP INDEX idx_status,省掉一份写入放大。 - 分表:当单表 > 5 亿行、或索引 > 5 个且写 QPS > 5000 时,按
status或created_at做时间/状态分表,每表索引降到 2–3 个。你的 6500 万行还远没到这一步。
三、我们团队的 TCO 评估流程(可直接抄)
- 采样窗口:加索引前后各采 10 分钟(业务低峰期),performance_schema + INNODB STATUS 双源。
- 决策记录模板:
索引名: idx_status_priority_created 加索引日期: 2024-xx-xx 读 P99: 480ms → 35ms (↓93%) 写 P99: 45ms → 58ms (↑29%) 写 QPS: 1200 → 1020 (↓15%) 每行写开销增量: ~11µs 索引体积: +1.8GB (占表 19%) TCO 净收益: +11,100 ms·QPS → 保留 复审日期: 2024-xx-xx(季度复审) - 季度复审:每季度跑一次同样采样,如果读 QPS 占比从 5% 掉到 1%(业务模式变了),重新算账,可能就不值得留了。
总结:你这个索引的写入代价(每行 ~11µs)远低于读收益(每条查询省 445ms),TCO 净正。真正该做的是检查旧的 idx_status 是否可以删掉,省一份冗余写入放大。