3147 字
约 10 分钟
14
S11-A16 · 索引的重建决策:什么时候该重建、什么时候不该
"建议每周重建一次索引,能提升性能。"这类建议通常来自一次观察:重建后某个查询确实快了。但那个观察很可能与索引无关——重建过程中表被完整读了一遍,工作集被推进了 buffer pool,重建后第一次查询快,是缓存效应。
重建是一次全量写入:读全表、写全表、占额外空间、抢同一份 IO。代价明确,理由就必须明确。这篇只回答一个问题:什么情况下重建是正解,什么情况下它只是把问题挪到下一个维护窗口。
1. 重建的代价结构:先算账,再决定
| 代价项 | 表现 | 与表大小的关系 |
|---|---|---|
| IO 放大 | 全表顺序读 + 全表重写 + 日志写入 | 近似线性,且远大于一次全表扫 |
| 并发受限 | COPY 类重建阻塞 DML;INPLACE 类仍需元数据锁与额外空间 |
持续时间决定影响面 |
| 临时空间 | 需要约等于表大小(或索引大小)的空闲空间 | 空间不足直接失败 |
| 复制延迟 | 重建在主库产生大量变更,从库追赶变慢 | 与重建时长同向 |
| 统计信息重置 | 重建后统计信息需要重新采集 | 短期内执行计划可能更差 |
| 备份窗口挤占 | 全量备份与重建叠加,窗口被撑爆 | 与备份策略相关 |
─── 三种重建形态,代价与可用性各不相同(语法按 MySQL 8.0 核对)
$ mysql -e "OPTIMIZE TABLE orders;" // 重建以回收空间,期间 DML 表现按版本核对
$ mysql -e "ALTER TABLE orders ENGINE=InnoDB;" // 显式重建,等同于一次全量拷贝
$ gh-ost --table=orders --alter="ENGINE=InnoDB" ... // 在线工具:影子表 + 增量回放,慢但不阻塞
判断清单:
- 表的数据量与索引量各多大?只重建索引与重建整表的代价不同。
- 重建期间能否接受写阻塞?不能接受就只能走在线工具或影子表,时间成本翻倍。
- 是否有足够的空闲空间?空间不足时重建失败会留下中间产物。
- 从库延迟是否有预算?重建产生的变更量会把延迟推高。
- 重建是否落在备份窗口内?两者叠加是典型的自伤。
- 重建后谁来验证?没有验证的重建,"变快了"只是感觉。
- 这轮重建的预期收益能否写成一句话?写不出来,说明理由还不成立。
一句话判据:重建是"重写一遍",不是"整理一下"。任何以"整理"为理由的重建,都该先被质疑。
2. 重建只能解决三类问题
| 问题 | 重建有效吗 | 说明 |
|---|---|---|
| 碎片化(页填充率低、区不连续) | 部分有效 | 回收空间、提升页利用率;但对点查与范围查的实际影响常被高估 |
| 统计信息陈旧 | 不必要 | ANALYZE TABLE 单独更新即可,代价低一个量级 |
| 索引结构损坏 | 有效 | 页校验失败、索引与数据不一致时,重建是修复手段之一 |
| 索引选错、缺索引 | 无效 | 这是设计问题,重建只会让错的索引更快 |
| 查询写法差、函数包裹列 | 无效 | 重建不改变优化器的可用性判断 |
| 缓存命中率低 | 无效 | 重建带来的"变快"多半是工作集被预热 |
| 锁竞争、连接池不足 | 无效 | 与索引结构无关 |
碎片化的真实影响远小于直觉:真正决定代价的是扫描方式:顺序扫描按页连续读,预读有效;随机扫描(大量回表、非聚簇二级索引跳读)每行一次寻址,碎片带来的额外页跳转才被放大。一个以覆盖索引为主、几乎不回表的查询,碎片率再高也感觉不到。
─── 取碎片与页利用率的近似值(information_schema,字段按 MySQL 8.0 核对)
SELECT table_name,
data_length, index_length, data_free,
ROUND(data_free / (data_length + index_length + 1) * 100, 2) AS free_pct
FROM information_schema.tables
WHERE table_schema = 'app' AND table_name = 'orders';
─── 统计信息单独更新:不需要重建
ANALYZE TABLE orders;
判断清单:
- 慢查询的执行计划是否指向索引结构本身?不是的话,重建无的放矢。
- 该表是回表密集还是以覆盖索引为主?前者对碎片更敏感。
- 碎片率是统计估算还是精确值?
data_free本身是估算,别拿它当精确指标。 - 统计信息是否刚更新过?更新后仍慢,才轮到怀疑结构。
- 是否有页校验错误或索引与数据不一致的证据?这是重建的硬理由。
- 空间是否真的需要回收?磁盘不紧张时,回收空间本身不构成理由。
- 重建后查询计划会不会变?会变时,要准备回退方案。
3. 触发应该基于指标,而不是日历
"每周一次"是最省事的策略,也是最容易变成无收益开销的策略:表在长,重建时间在长,而收益在缩小。最后一轮重建永远跑不完——这不是夸张,是增长曲线与维护窗口的必然交叉。
| 指标 | 取法 | 触发判据(按自家基线校准) |
|---|---|---|
| 页利用率 / 空闲页占比 | data_free 与表大小之比 |
连续多个观察周期高于基线,且伴随扫描变慢 |
| 扫描效率 | 执行计划中的 rows examined / rows returned |
比值持续恶化,且索引未变 |
| 查询计划变化率 | 重建前后同一批 SQL 的计划对比 | 变化率显著,说明重建确有影响 |
| 重建耗时趋势 | 每轮重建的实际耗时 | 耗时接近维护窗口的某个比例时,停手 |
| 维护窗口余量 | 窗口长度减实际耗时 | 余量不足时,重建策略必须改 |
─── 重建前后抓同一批 SQL 的计划,用于判断"到底变了什么"
EXPLAIN FORMAT=JSON SELECT ... ; // 重建前存档
─── 重建后重跑,逐项比对 type / key / rows / filtered
// 触发条件写成可执行判断,并带停手条件
rebuild_guard:
page_free_pct: "高于基线且连续 3 个观察周期" // 看趋势,不看绝对值
rebuild_cost: "超过维护窗口 60%" // 触及即停手,改走分批
require_baseline: true
判断清单:
- 触发条件是否写成了可执行的判断?"感觉慢了"不是条件。
- 指标是否在重建之前采集?事后采集无法区分因果。
- 是否有基线?没有基线的阈值只是数字。
- 是否考虑过缓存效应?重建后的第一次查询快,是因为数据在内存里。
- 增长趋势是否被纳入判断?表每年增长一倍时,"每周重建"的可行性会逐年下降。
- 停手的条件是否明确?只写何时开始、不写何时停止,是维护任务的通病。
归因错误的经典形态:重建 → 立即压测 → 快了 → 归因于索引。重启实例或预热工作集后同样会快;两条路径分开验证,才知道收益来自结构还是缓存。
4. 副作用、失败中间状态与维护任务冲突
| 冲突对象 | 冲突形式 | 处理方式 |
|---|---|---|
| 全量备份 | 同一份 IO 与磁盘空间被争抢 | 错开窗口,或先备份后重建 |
| 统计分析 / 报表 | 大范围扫描与重建叠加 | 排进同一窗口的另一半,不并行 |
| 归档 / 删除任务 | 大量删除会再次制造碎片 | 归档完成后再判断是否需要重建,否则白做 |
| 主从复制 | 重建产生的变更把延迟推高 | 观察延迟,必要时暂停重建 |
| 在线 DDL 变更 | 两个重建同时进行,锁与空间双倍 | 串行化,禁止并发 |
─── 失败后先确认中间状态,再决定重试还是回滚(示意,工具与参数按所用版本核对)
$ mysql -e "SHOW CREATE TABLE orders\G" | grep -i engine // 结构是否被改坏
$ mysql -e "SHOW INDEX FROM orders;" // 索引是否齐全
$ ls -lh /var/lib/mysql/app/ | grep -E 'orders.*(tmp|old)' // 影子表与临时文件残留
$ mysql -e "CHECK TABLE orders;" // 页级一致性
判断清单:
- 失败时原表是否仍在?
COPY类重建在最后阶段才替换,中途失败通常可回退;在线工具要单独确认。 - 是否有临时表与磁盘残留?残留会静默吃掉空间,直到下一次重建失败。
- 回滚需要多久?需要重新导入的话,"可回滚"只是理论上的。
- 重建是否与变更发布同一窗口?两者叠加时,出问题无法区分原因。
- 是否有重建期间的监控?只看结束状态,会漏掉过程中的锁等待与延迟。
| 度量 | 说明 |
|---|---|
| 碎片率 / 页利用率 | 判断是否接近触发条件 |
| 重建耗时 | 与维护窗口余量对比 |
| 重建后查询计划变化率 | 判断重建是否影响了执行路径 |
| 维护窗口余量 | 剩余可用时间的比例 |
5. 排错:五种"重建了但没变好"
| 现象 | 原因 | 处理 |
|---|---|---|
| 重建后立刻变快,一周后回退 | 缓存效应,工作集被预热 | 用未重建的同类表做对照 |
| 重建后某些查询更慢 | 统计信息重置,优化器换了计划 | 重建后立即 ANALYZE TABLE |
| 重建耗时逐轮增长 | 表在增长,窗口没变 | 改为按指标触发,并设停手条件 |
| 重建期间业务超时 | IO 与锁被挤占 | 换在线工具,或压到更低峰时段 |
| 重建失败且空间被占满 | 临时表与日志残留 | 先清理残留,再评估是否重试 |
6. 边界:定期重建几乎没有收益的场景
| 场景 | 判断 | 理由 |
|---|---|---|
| 表很小(能整体放入内存) | 不必定期重建 | 页跳转代价可忽略 |
| 写入远多于删除,且以追加为主 | 不必 | 碎片主要来自删除与更新留下的空洞 |
| 工作集能被 buffer pool 容纳 | 收益低 | 缓存掩盖了结构差异 |
| 以覆盖索引查询为主 | 收益低 | 不回表,随机寻址少 |
| 索引结构损坏 | 必须重建 | 修复,不是优化 |
| 删除/归档后空间需归还磁盘 | 需要重建 | 空间回收是明确目标 |
把重建当修复手段而不是保养手段,判断就简单:有损坏、有明确的页利用率问题、有空间回收需求时做,其余先看执行计划。
自测
- 重建的六项代价里,哪两项最容易在评估时被漏掉?
- 为什么重建能解决碎片化,却解决不了"索引选错"?
- 统计信息陈旧为什么不需要重建,正确做法是什么?
- "重建后变快"最可能的另一种解释是什么,如何验证?
- 表持续增长时,为什么"每周重建"最终会失效?
- 哪些场景下定期重建的收益接近零?
S11-A16 · 索引的重建决策:什么时候该重建、什么时候不该
https://www.clxhxhhr.top/posts/6375/ 评论
0 条
还没有评论,先写一条吧。