根本原因是每行INSERT都需同步更新所有索引、生成WAL并触发B-tree分裂或GIN重构,属ACID刚性要求;而CREATE INDEX是离线批量构建,绕过逐行开销。

因为每插入一行,数据库必须同步更新所有相关索引——这不是配置问题,而是 ACID 语义的刚性要求。
INSERT 时每个索引都在做“实时维护”
插入不是只写数据页,而是对每一行执行 N+1 次写操作:1 次主键/聚簇索引 + N 次二级索引。这些操作必须原子、一致、可回滚:
-
UNIQUE索引要先查重,再插入,两次 B+ 树查找 + 一次写入 -
FULLTEXT或GIN索引需分词、生成倒排项、合并 posting list,开销呈倍数增长 - 多列复合索引(如
(a, b, c, d))比较和分裂成本随键长上升,不是线性而是近似平方级 - 低区分度字段(如
status只有 'pending'/'done'/'error')导致大量键值挤在少数叶子页,加剧页锁争用与分裂频率
WAL 和缓冲区压力被指数放大
索引越多,WAL 日志体积越大、刷盘越频繁,buffer pool 压力也越重:
- 每次索引更新都触发
WAL记录,5 个索引 ≈ 5 倍 WAL 写入量,IO:WALWrite等待事件飙升 - InnoDB 的
innodb_buffer_pool_size若小于活跃索引+数据总大小,就会频繁淘汰页、刷脏页、重加载——等于用内存模拟磁盘行为 - PostgreSQL 中
shared_buffers不足时,DataFileRead等待明显,尤其在插入同时触发 vacuum 或 autovacuum 时
哪些索引最拖后腿?别盲目删,要看类型
不是所有索引对写入的影响相同。以下三类在批量插入中性能衰减最剧烈,且原因各异:
-
GIN索引:默认fastupdate = on会堆积 pending list,虽写入看似快,但后续 vacuum 或查询时集中爆发;设为off则每次插入都走完整 Entry Tree 路径,更慢 - 含大字段的索引:如对
JSONB或TEXT字段建全文索引,单行可能生成数十个倒排项,每个都要独立定位并写入 - 冗余复合索引:比如已有
(user_id, created_at),又额外建了(user_id)—— 后者不仅不提升查询(前缀已覆盖),还白增一次索引维护
真正有效的缓解手段不是“调优”,而是“绕过”
优化器不会帮你跳过索引更新,所以高吞吐写入场景下,核心思路是把“在线更新”换成“离线构建”:
- 大批量导入前,用
DROP INDEX CONCURRENTLY(PG)或ALTER INDEX ... DISABLE(SQL Server)临时移除非关键索引;MySQL 8.0+ 可改用ALTER INDEX idx_name INVISIBLE避免 DDL 锁 - 务必禁用非业务强依赖的触发器:
SHOW TRIGGERS LIKE 'table_name'查出后,用 session 变量或条件逻辑临时跳过 - 对日志类、报表类表,考虑设为
UNLOGGED(PG)或切换至simple recovery model(SQL Server),但要接受崩溃丢失风险 - 用
LOAD DATA INFILE(MySQL)或COPY(PG)替代INSERT INTO ... VALUES,它们跳过 SQL 解析层,直接构造元组
最容易被忽略的一点:索引是否被查询真正用到,比“看起来有用”重要得多。长期未被 idx_scan 触发的索引,删掉它对写入的帮助,远大于保留它对某条半年没跑过的报表 SQL 的潜在好处。

















