索引降低写入性能本质是这几笔开销——这话我跟人重复过太多次了。多数人的第一反应是把索引砍了,写是快了,读接口先顶不住。所以别急着删,先弄明白写一条数据的时候,数据库到底替你干了多少活,哪些是必须的,哪些是你自己给自己加的。

一次 insert 落地,索引那边要动几下手脚
表上只有一个聚簇索引的时候,写一条记录还算轻省:在B+树上找到该去的位置,写进数据页,改页目录,记redo,脏页排着队等刷盘。就这样已经是一串动作了。你再加三个二级索引,同一条insert就得在几棵树上各找一次位置、各写一次;要是这几个索引页还不在同一片区域,那基本等于几次随机写。写入放大这词听着玄,落到磁盘上就是多了几份数据,落到内存里就是多了几份脏页。
还有一种情况常被忽略:update。改的是非索引列,二级索引基本不用动,InnoDB 这类引擎有 in-place 更新,代价小得多;改的偏偏是索引列,那就是旧的删一条、新的插一条。所以“我这表更新特别慢”这句话,得先追问一句改的是哪个字段。同一条 SQL,改不同列,成本差得很远。
索引降低写入性能本质是这几笔开销,先认全再谈优化
拆开看大概五笔账,认全了才知道该动哪一笔。第一笔是随机IO,索引越多,写入时被打散的页就越多。第二笔是页分裂,后面单独说。第三笔是唯一性校验,唯一索引和主键在写之前必须确认没有重复,这一步本质是一次读,写操作里硬生生插进去一次查找。第四笔是缓存,缓冲池大小是固定的,索引页占得多,数据页就少,热数据被挤出去,读写一起遭殃。第五笔是日志和刷脏,redo、undo都要跟着写,脏页多了刷盘那一下的抖动也会明显。这五笔里,前三笔跟索引数量直接挂钩,后两笔是连锁反应。
顺带说一句,很多人一上来就想找参数调优,把刷脏节奏调快点、把日志缓冲加大点。能缓一缓,但治不了根。索引本身就是写放大源头,参数只是让它别那么难受。
页分裂这玩意儿,比你想的常见
自增主键顺序插入,页慢慢填满,填满了开新页,很顺。换成UUID、雪花ID里的随机段、或者业务上那种没规律的编号做主键,插入点就撒在整棵树里,每次落点都可能把一个已经装满的页劈成两半:申请新页、把一半数据挪过去、更新父节点的指针。分裂之后页的填充率掉下来,同样的数据量占的页更多,扫描要多读几页,刷盘也多写几页。碎片攒久了,你会觉得“数据没涨多少,磁盘和IO先涨了”。
这一笔开销跟索引数量关系不大,跟主键选型关系极大。写多读少、量又上得快的表,主键老老实实用有序值,能省掉一大截。业务上非要用UUID的,也有折中办法,比如有序UUID,各家实现不一样,具体以你用的数据库版本文档为准。
唯一索引,代价藏在“校验”那两个字里
唯一索引除了维护树结构,还要保证不重复。写进去之前得去查一遍,这一步在并发写入的时候还要加锁等一等,热点值上尤其明显。有些人喜欢给手机号、邮箱、单号全都挂唯一索引,逻辑上是干净了,可每一次写入都带着一次额外的查找,量一上来就现形。是不是每个都得挂唯一约束,值得一个一个过一遍——有些重复完全可以在业务层或者入库前拦掉,不必让数据库每写一次就查一次。
索引一多,缓存里就装不下热数据
缓冲池就那么大,索引占的页多了,留给数据页的空间就少。命中率一掉,读变慢,刷脏也更频繁。这个坑隐蔽在于,你单看写压测的曲线未必看得出来,等线上读写混跑的时候才发作。所以评估索引不能只算“这个查询能快多少”,还得算它常驻内存占多少。
怎么确认这笔账记在索引头上
别拍脑袋删。先看慢日志,把写入慢的语句捞出来;再看看写操作的耗时分布,是不是集中在某几张表。MySQL 这边 performance_schema 和 sys 库里有索引使用情况的视图,能看出哪些索引一次都没被读过。pt-index-usage 这类工具可以对着慢日志反推索引使用率,pt-duplicate-key-checker 能揪出重复和冗余的索引。做完这些,再在测试环境上删掉一两个索引,压一遍写,对比前后。生产上直接删索引这事,除非你已经确认过没有查询依赖它。
能落地的减法,从删冗余开始
冗余索引是最容易捡的便宜:(a,b) 上已经有联合索引了,再单独建一个 (a) 就是白养。能合并的合并,靠最左前缀去覆盖原来那些单列查询。历史数据该归档就归档,冷表上的索引纯粹是负担。批量插入比一条一条插省很多,这个不用多说。写远多于读的表,索引数量真的要克制,一两个够用的就别加到五六个。
还有一句提醒:覆盖索引、索引下推这些,优化的是读,别拿来当写入优化的理由。方向搞反了,越优化越慢。
遇到写入变慢,先别怪机器、别怪磁盘。把表上的索引列出来,一个个问它:最近一个月被用过吗,是不是被别的索引覆盖了,这个唯一约束非数据库来管不可吗。三个问题问完,通常就能砍掉一两个。剩下的那些,才是你为了查询必须付的钱。
【版权声明】:本站内容若无意侵犯到您的权利,请及时与我们联系将尽快删除相关内容!