innodb_fill_factor不是调高就一定好,因为设得过高(如99)会加剧非顺序写下的页分裂频次,设过低(如50)则浪费缓冲池空间、降低查询局部性;它对自增主键表影响有限,主要作用于二级索引或uuid主键表。

MySQL
编写正确的MySQL查询,避免字符集、索引和锁方面的常见陷阱。
下载
为什么 innodb_fill_factor 不是调高就一定好
innodb_fill_factor 控制页分裂前的填充比例,默认 93.75(即预留 1/16 空间)。设得太高(比如 99),看似节省空间,但实际会让页更容易“刚写满就分裂”,尤其在非顺序写场景下,反而增加分裂频次和数据迁移量。设得太低(比如 50),又浪费缓冲池空间、降低页内查询局部性。
- 生产中常见误操作:为“减少碎片”盲目调高该值,结果
Handler_read_rnd_next 上升、Innodb_pages_written 暴涨
- 对主键自增表,
innodb_fill_factor 影响有限——因为最后一页的插入点分裂不依赖预留空间
- 对二级索引或 UUID 主键表,它才真正起作用;建议从 80 开始压测,观察
Innodb_page_splits 和慢查比例变化
主键必须是自增整型,否则顺序插入无效
InnoDB 的“顺序插入优化”只对**聚簇索引(即主键)** 生效,且前提是插入值严格单调递增。用
UUID()、
UUID_SHORT() 或时间戳+随机数拼接的主键,哪怕看起来“时间上接近”,也会被当作随机写入,触发标准页分裂。
- 常见错误现象:
INSERT INTO t (id, name) VALUES (UUID(), 'xxx') 导致每插几行就分裂一次,DATA_FREE 持续增长
- 即使你用
SELECT MAX(id)+1 模拟自增,也因并发竞争导致间隙和乱序,无法触发插入点分裂
- 正确做法:用
BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,并确保 auto_increment_increment=1、auto_increment_offset=1
UPDATE 索引列比 INSERT 更容易引发页分裂
插入新行时,InnoDB 至少还能按主键定位到大致位置;而
UPDATE 修改索引列(比如
UPDATE t SET status = 2 WHERE id = 123),若该行原在页 A、更新后需挪到页 B(因新值排序位置变了),就会触发“页内移动 + 页间迁移”,比单纯插入更重。
- 尤其危险的是二级索引字段更新:
UPDATE t SET name = 'new' WHERE id = 123,会同时修改聚簇索引行 和 name 二级索引页,两棵树都可能分裂
- 如果只是状态变更,优先把字段挪出索引:比如把
status 从联合索引 (user_id, status, created_at) 里去掉,改用覆盖查询 + 应用层过滤
- 监控关键指标:
Innodb_row_lock_waits 配合 information_schema.INNODB_TRX 查锁等待链,常能发现隐式分裂争抢
碎片不是等它爆了才处理,而是看 LEAF_PAGES 和 SIZE 的差值
information_schema.INNODB_SYS_INDEXES 中的
LEAF_PAGES 是当前真实使用的叶子页数,
SIZE 是分配的总页数(含空洞和分裂残留)。当
SIZE - LEAF_PAGES > 1000 且持续增长,说明页分裂后没合并回来,物理碎片已在积累。
-
OPTIMIZE TABLE 或 ALTER TABLE ... REBUILD 能清碎片,但会锁表或加 MDL 锁;MySQL 8.0.23+ 推荐用 ALTER TABLE t REBUILD,它跳过统计信息重建,更快
- 不要依赖
DATA_FREE 判断——它只反映表级别空闲空间,无法区分是索引碎片还是行格式膨胀(如 ROW_FORMAT=COMPACT 下的 off-page 存储)
- 最容易被忽略的一点:页合并(merge)默认阈值是 50%,但合并只发生在 DELETE 后,且需相邻页都低于阈值;UPDATE 导致的页内空洞 不会触发合并,只会留在那里拖慢后续插入