UPDATE本质是“删旧+插新”,更新索引列会触发所有相关二级索引重写;页满时插入导致页分裂,引发碎片上升、缓冲池效率下降。

UPDATE 怎么悄悄撕裂索引页
UPDATE 不是“改一行”,而是数据库底层的一连串物理操作:先定位原记录、标记删除(或迁移)、再插入新版本——尤其当更新的是索引列时,所有含该字段的二级索引都得同步重写。B+ 树页容量固定(InnoDB 默认 16KB,SQL Server 8KB),一旦某页已满,插入新索引项就会触发页分裂:原页一半数据搬进新页,两页都留下大量空闲空间(avg_page_space_used_in_percent 下降),且新页大概率不在原页物理邻近位置。
典型表现不是报错,而是:sys.dm_db_index_physical_stats 显示 avg_fragmentation_in_percent 突增、page_count 持续上涨但 record_count 几乎不变;MySQL 中 Data_free / (Data_length + Index_length) 超过 20%,同时 innodb_buffer_pool_reads 周环比上升超 15%。
哪些 UPDATE 最容易引爆碎片
不是所有 UPDATE 都一样危险。真正高危的是:
- 更新高频变更字段(如
status、updated_at)且该字段在多个复合索引中靠前 - 更新变长字段(
VARCHAR、TEXT)导致行尺寸膨胀,引发行迁移或页内重排 - 在非顺序主键表上执行 UPDATE(例如用
NEWID()或 UUID 作聚集索引),让新索引位置完全随机 - 触发器内嵌套 UPDATE(如审计日志触发器顺带更新统计表),把单次逻辑写放大成多次索引维护
重构 UPDATE 相关索引的实操路径
别急着 ALTER INDEX ... REBUILD,先做减法和结构调整:
- 删掉没被查询实际用到的索引:
ALTER TABLE t DROP INDEX idx_status;—— 先在备库压测,确认UPDATE延迟下降 30%+ 再上线 - 把高频更新字段从复合索引前缀挪到末尾:
CREATE INDEX idx_user_status ON t(user_id, status)→CREATE INDEX idx_user_status ON t(user_id, created_at, status),等值查user_id仍能走索引,但更新status不再强制分裂整个前缀段 - SQL Server 2016+ 可设填充因子缓解:
CREATE INDEX ... WITH (FILLFACTOR = 80),但仅对新增页生效,已有碎片需配合REORGANIZE(碎片 ≤30%)或REBUILD(碎片 >30%) - MySQL 8.0.23+ 优先用
ALTER TABLE t REBUILD替代OPTIMIZE TABLE,它跳过统计信息更新,避免执行计划抖动
监控和干预的临界点在哪
碎片本身不报警,但它是缓冲池效率恶化的信号灯。真正该动手的时刻不是 avg_fragmentation_in_percent > 30%,而是:
-
innodb_buffer_pool_read_requests / innodb_buffer_pool_reads(MySQL)或Page life expectancy(SQL Server)连续三天跌超 15% - 同一张表的
UPDATE语句平均逻辑读(logical reads)比上周同时间上升 2 倍以上 - 执行计划里原本走索引查找(
INDEX SEEK)的语句,开始频繁出现INDEX SCAN或KEY LOOKUP
这时才值得停业务窗口跑 REBUILD,否则只是给问题打补丁。










