delete仅标记行删除,不回收页空间,b+树节点长期低利用率却无法自动合并,需optimize table重建或分区表物理删除才能真正释放空间。

删除操作不回收页空间,B+树节点长期“空着”
MySQL InnoDB 的 B+树索引页(默认 16KB)在执行 DELETE 时,并不会立即把整页清空或归还给操作系统。它只是把对应记录标记为“已删除”,并把该记录占用的空间加入页内的空闲链表(free list)。只要页里还有其他活跃记录,这个页就继续保留在树中,且仍参与查询路径——哪怕实际填充率只剩 10%。
这意味着:频繁增删后,很多非叶子节点和叶子节点的物理利用率会持续走低,但树结构不变、高度不变、页数也不减。你用 SHOW INDEX FROM table_name 看不到异常,但用 INFORMATION_SCHEMA.INNODB_SYS_INDEXES 或 innodb_metrics 查 index_page_merge_attempts、index_page_splits 就会发现合并失败率上升——因为页太“虚”,没足够连续空闲空间触发自动合并。
页合并(merge)被抑制的三个常见原因
InnoDB 本会在后台尝试把两个半空页合并成一个较满页,以压缩树体积。但以下情况会让 merge 失效:
- 事务未提交:被删除记录所在的页若仍有活跃事务(如长事务持有 undo log),页无法被合并
- 页内存在大字段(
TEXT/BLOB):这类数据可能外存于溢出页(off-page),即使主记录删了,溢出页引用未清理,页仍被视为“不可合并” - 自增主键 + 高频删除:比如按时间范围批量删老订单,但新订单持续插入高 ID 段,导致删除集中在树的左半部分,而合并策略更倾向处理相邻页,物理位置不邻近则跳过
如何验证索引是否已严重稀疏?
别只看行数,重点查页级实际使用率:
运行这条语句:
SELECT index_name, COUNT(*) AS page_count, AVG(FLOOR((data_length + index_length) / 16384.0)) AS avg_fill_ratio FROM information_schema.INNODB_BUFFER_PAGE WHERE table_name = 'your_db/your_table' AND index_name IS NOT NULL GROUP BY index_name;如果
avg_fill_ratio 长期低于 0.3(即平均每页只用了不到 5KB),基本可判定稀疏严重。注意:该视图需开启 innodb_buffer_page 监控(SET GLOBAL innodb_monitor_enable='innodb_buffer_page'),且仅反映缓冲池内热页,冷页需结合 OPTIMIZE TABLE 前后的 DATA_LENGTH 对比判断。
OPTIMIZE TABLE 不是万能解药
OPTIMIZE TABLE 本质是重建表+索引,会释放碎片、重排页、降低树高度(如有必要)。但它有硬伤:
- 锁表:在旧版本(
ALGORITHM=COPY)下全程锁表;即使 8.0+ 默认用INPLACE,对大表仍是长耗时 DDL,可能阻塞写入 - 临时空间爆炸:重建期间需要额外磁盘空间 ≈ 原表大小,空间不足直接失败
- 统计信息滞后:重建后优化器未必立刻更新索引基数(
cardinality),有时需手动ANALYZE TABLE
真正可持续的做法是:对高频删除场景,优先用分区表(PARTITION BY RANGE (create_time))按月/季度删整个分区,物理删除页,零碎片;或者改用归档表+定期 truncate,避免在主索引上反复打洞。











