【背景】 做了 10 年数据库平台工程,最常被问的一句话是"这个查询慢,加个索引就好了吧?"
但加索引从来不是免费的。我亲历过三个典型的"加了索引反而更差"的事故:
- 订单表加了第 9 个二级索引后,批量写入 P99 从 12ms 涨到 17ms(+40%),最后把两个低命中索引删了才回来;
- 一张 3000 万行的对账表,加了复合索引 (user_id, status, created_at),结果高频查询只走前缀 user_id,B-tree 深度反而比全表 seq scan 还慢(因为表本身已经按 user_id 聚集);
- 一个写入 200k rows/s 的日志表,DBA 随手加了 3 个"以防万一"的索引,写入吞吐直接掉了 15%。
加索引的代价是每行写入 × 索引个数,这是一个持续发生的成本,不是"加完就结束"的一次性开销。
【常见分歧,三个派系】
派系 A(DBA 派):永远先 EXPLAIN,看执行计划再决定。"没有 EXPLAIN 就不配讨论慢查询。" 这个我没意见,但 EXPLAIN 告诉你"走不走索引",不告诉你"值不值得走"。
派系 B(应用派):数据量 < 100 万行、且访问模式是范围扫描时,seq scan 比 B-tree 查找更快(因为 I/O 顺序读 vs 随机读)。"小表加索引是性能陷阱,别碰。" 这个在 PostgreSQL 里确实成立,但 MySQL InnoDB 的 buffer pool 命中后差异会缩小,不能一刀切。
派系 C(SRE 派):不管哪个,先加缓存。"慢查询治理的正确解法是让查询不发生,而不是让查询变快。" 这个在 CQRS 读路径上成立,但写路径上的实时查询(比如订单状态变更后的详情页)没法绕过 DB。
三个派系都不算错,但都缺一个把三者放在一起比较的量化框架。
【我的倾向:一个不完美但可用的决策公式】
我倾向于在动手前算一个比值——索引维护成本 / 查询加速收益:
写入代价增量 ΔW = rows_per_sec × (新索引个数) × avg_row_size × B-tree_depth_factor
查询节省收益 ΔR = query_qps × (扫描行数_无索引 - 扫描行数_有索引) × cost_per_row_read
加索引的净收益 = ΔR - ΔW
其中:
rows_per_sec是写入吞吐(从 pg_stat_bgwriter 或 MySQLCom_insert + Com_update推算);avg_row_size从pg_class.relpages × 8192 / reltuples或information_schema.columns估算;B-tree_depth_factor粗略取 2(每次写入多 2 次 B-tree 页操作);cost_per_row_read在 buffer pool 命中时 ≈ 0.001ms,未命中时 ≈ 0.1ms(SSD)。
这个公式不完美(没考虑索引选择性、并发锁竞争、vacuum 压力),但它能把"要不要加索引"从"我觉得"变成"算一下"。
关键推论:当写入吞吐 > 50k rows/s 时,每加一个索引的边际成本就进入"必须量化"的区间;当查询 QPS < 10 时,加索引的净收益大概率是负的——这时候该改的是 SQL 或加缓存,不是加索引。
【想听大家在什么场景下怎么选】
-
你们线上有没有"加了索引后写入变慢,最后把索引删了"的案例? 触发阈值大概是什么量级(写入 QPS / 表行数 / 索引总数)?
-
分区表 vs 二级索引:你们的切换阈值是多少?不是"5000 万行"这种玄学数字——是"单分区超过 N 行后 seq scan 超过 M 秒"还是"索引个数超过 K 个后写入 P99 超过阈值"?
-
有没有团队在用
pg_stat_user_indexes或information_schema.statistics做索引使用率监控? 具体怎么定"低使用率"的阈值?是"30 天查询次数 < 10 次"还是"索引命中率 < 5%"?有没有因此误删过关键索引? -
派系 C 的同学:在强一致写路径上(不是读副本),你们的"让查询不发生"具体怎么落地?Outbox?物化视图?还是干脆把"实时查询"这个需求砍了?
欢迎拍砖。这个公式我自己也在用,但我知道它至少漏了两个重要变量(并发锁竞争、vacuum/autovacuum 压力),想听有人把这两个也塞进来算一算。