索引过多是高并发写入抖动最常见根源——每多一个索引,写操作就得维护一棵B+树,导致写放大、锁等待变长、CPU刷脏页压力飙升;Index_length接近Data_length、Innodb_rows_inserted与Innodb_pages_written比值跌至2~5、EXPLAIN显示大量二级索引更新、LOG段中log i/o频率远超事务提交频率,均表明索引拖累写入;应优先删除仅服务于低频查询的单列WHERE索引(如created_at)、冗余索引及未被查询使用的索引。
索引过多是高并发写入抖动最常见、最容易被低估的根源——每多一个索引,INSERT、UPDATE、DELETE 就得多维护一棵 B+ 树,写放大直接翻倍。这不是“慢一点”,而是锁等待变长、CPU 刷脏页压力飙升、Innodb_buffer_pool_write_requests 和 Innodb_data_writes 比值严重失衡的信号。
别猜,用数据验证:
SHOW TABLE STATUS LIKE 'your_table',重点关注 Index_length:如果它接近甚至超过 Data_length,说明索引开销已和数据本身持平,非常危险Innodb_rows_inserted 和 Innodb_pages_written 的比值。正常应在 10~50 之间;若跌到 2~5,大概率是索引在疯狂刷盘EXPLAIN FORMAT=JSON 看写操作是否触发了大量二级索引更新(used_columns 里出现非主键字段)SHOW ENGINE INNODB STATUSG 中的 LOG 段:如果 log i/o's done 频率远高于事务提交频率,说明 redo 日志在为索引更新“擦屁股”优先砍掉对写入无贡献、只服务于低频查询的索引:
WHERE 索引,但该列 SELECT 查询占比 performance_schema.events_statements_summary_by_digest 查)VARCHAR 索引,比如 INDEX (content(255)) —— 实际查询几乎不用到那么长(user_id, status),又单独建了 (user_id),后者可删EXPLAIN 任何查询命中的索引(用 sys.schema_unused_indexes 视图,MySQL 8.0+)ORDER BY 或 GROUP BY 的字段索引(例如 updated_at 单独建索引)保留必要索引,但让它们更“轻”:
INDEX (a,b,c,d,e) 只用于 WHERE a=? AND b=? ORDER BY c,可精简为 INDEX (a,b,c)
TINYINT 或 ENUM 替代 VARCHAR 做状态字段索引(如 status ENUM('pending','done','failed')),索引体积小、比较快WHERE YEAR(created_at) = 2024 会让整个索引失效,改用 WHERE created_at >= '2024-01-01' AND created_at
GENERATED COLUMN + STORED 上,再建索引——减少主表 DML 对索引的直接冲击索引删了,但抖动没消失,往往卡在这些细节:
innodb_flush_neighbors = 1 在 SSD 上没关:即使索引少了,这个参数仍会强制批量刷邻页,造成 I/O 放大。务必运行 SELECT @@innodb_flush_neighbors 确认是 0
UUID 或 CHAR(36),导致页分裂持续发生,新插入总要调整 B+ 树结构 —— 这种抖动和索引数量无关,但表现相似FULLTEXT 索引:它不走普通 B+ 树,而是独立的倒排索引,每次写入都要同步更新,且无法用常规方式禁用innodb_log_file_size 太小(如默认 48MB):索引删了,但日志切换太频繁,照样触发 checkpoint 抖动。建议设为 1G~2G