索引越多insert越慢是必然的,因每条记录需同步更新所有相关b+树;二级索引写放大明显,主键索引可能触发页分裂;批量插入、禁用非必要索引、避免高离散字段建索引可优化。

索引越多,INSERT 越慢是必然的
每新增一条记录,MySQL 不仅要写数据行,还要同步更新所有相关索引的 B+ 树结构。尤其是二级索引(非主键索引),需要额外回表或维护独立的叶子节点,写放大效应明显。主键索引(聚簇索引)本身和数据存储绑定,插入时可能触发页分裂;而每个二级索引都是一份独立拷贝,INSERT 操作会按索引列顺序分别插入多棵 B+ 树。
实操建议:
- 单条
INSERT语句带多行(如INSERT INTO t VALUES (1, 'a'), (2, 'b'), (3, 'c');)比循环执行多次快得多——减少索引批量更新的开销 - 大批量导入前可临时禁用非必要索引:
ALTER TABLE t DROP INDEX idx_col;,导入完成再重建 - 避免在 UUID、随机字符串等高离散度字段上建索引,这类值导致 B+ 树频繁分裂,恶化插入性能
UPDATE 涉及索引列时性能下降最明显
UPDATE 的代价取决于是否修改了索引列。如果只改非索引字段,InnoDB 通常只需定位并修改聚簇索引中的行记录;但一旦 SET 子句里包含索引列(比如 UPDATE t SET status = 1, name = 'new' WHERE id = 100 中 name 是索引列),MySQL 就必须在对应二级索引中删除旧值、插入新值——相当于一次 delete + insert。
常见错误现象:
- 执行
UPDATE t SET created_at = NOW() WHERE user_id = 123,而user_id和created_at都有单独索引 → 触发两棵索引树更新 - 联合索引
(a, b)上执行UPDATE ... SET a = ?→ 整个索引项重定位,开销接近删除+重建 - 使用
LIKE '%xxx'或函数索引(如JSON_EXTRACT())做条件更新时,即使没改索引列,也可能因无法走索引导致全表扫描,间接拖慢更新
唯一索引的写入检查带来额外 I/O 开销
普通索引允许重复值,插入/更新时只需定位到位置插入即可;但唯一索引(含主键)必须先做存在性校验——即在 B+ 树中查找该值是否已存在。这个查找过程哪怕命中缓存,也比普通索引多一次逻辑判断和潜在的磁盘读(尤其当索引页不在 innodb_buffer_pool 中时)。
性能影响点:
- 高并发下多个线程同时插入相同唯一键前缀(如订单号前缀固定),容易引发
duplicate key冲突等待,甚至死锁 -
INSERT IGNORE或ON DUPLICATE KEY UPDATE本质仍是先查后插/更,不省去唯一性检查步骤 - 自增主键虽避免了唯一性冲突,但若业务层依赖
SELECT MAX(id)+1手动赋值,则丧失了自增的并发优势,反而引入竞争
如何平衡查询与写入:几个硬指标参考
没有银弹,但可以基于数据特征做取舍。例如日志类表(写多读少)通常只保留主键,最多加一个时间范围查询用的 created_at 索引;用户中心表(读写均衡)则需谨慎评估每个 WHERE / ORDER BY / JOIN 字段是否真有必要建索引。
实操判断依据:
- 单表索引总数超过 5–6 个,且写入 QPS > 500,就要警惕写放大问题
- 用
SHOW INDEX FROM t查看Cardinality,若某索引基数远低于表总行数(比如CARDINALITY / TABLE_ROWS ),大概率是低效索引 - 开启
innodb_monitor_output或使用performance_schema.table_io_waits_summary_by_index_usage,观察哪些索引从不被用于查询却持续消耗写入资源
真正难的是动态场景:有些索引白天支撑报表查询,晚上 ETL 批量更新时却成了瓶颈。这种时候,靠静态分析不够,得结合具体时间段的慢日志和索引访问统计来决策。











