选择在线重建索引还是离线操作,核心矛盾在于你对“锁”的容忍度。在大多数主流数据库引擎中,离线重建(Reorganize 或 Rebuild with ONLINE=OFF)通常会获取排他锁,导致表在操作期间不可读写。如果你的业务存在严格的 SLA 要求,那么唯一的出路就是利用数据库提供的在线机制。以 SQL Server 为例,你必须明确指定 ONLINE=ON,但这并非银弹,它本质上是利用行版本控制,在重建过程中允许并发 DML 操作,代价是会产生大量的临时日志写入,并且需要在操作开始和结束时获取短暂的架构修改锁。如果你的系统已经处于高负载状态,开启在线重建可能会直接撑爆你的事务日志或导致 TempDB 出现严重的争用。
很多新手 DBA 一看到碎片率超过 30% 就条件反射地去重建,这是极其危险的教条主义。你需要关注的不仅仅是 avg_fragmentation_in_percent,更要看 page_count。对于只有几百页的小表,即使碎片率高达 90%,重建带来的性能提升微乎其微,反而会浪费日志资源。真正的性能杀手是那些页数超过 50,000 且碎片率超过 30% 的大表。此外,要区分外部碎片和内部碎片。外部碎片是页的物理顺序与逻辑顺序不匹配,影响顺序扫描性能;内部碎片是页内存在大量未使用的空间,影响缓冲池命中率。对于内部碎片严重的情况,重组操作往往比完全重建更具性价比,因为它不创建全新的索引副本,而是原地整理页链。
估算时间窗口不能凭感觉,必须基于 I/O 子系统的实测指标。在线索引重建的时间窗口公式可以简化为:时间 ≈ (索引大小 + 预留空间) / 磁盘顺序写入速度。但在线操作远比这复杂,因为并发写入会放大日志生成量。假设你有一个 500GB 的表,重建期间有 20% 的数据发生了变更,那么实际产生的日志量可能是索引大小的 1.5 到 2 倍。如果你的日志磁盘只能提供 100MB/s 的写入速度,处理 1TB 的日志就需要近 3 小时,这还不包括数据文件的读写延迟。因此,在制定窗口前,务必通过 sys.dm_io_virtual_file_stats 检查日志文件的写入延迟,如果 io_stall_write_ms 持续高于 20ms,你的窗口必须预留出 30% 以上的缓冲时间。
在 SQL Server 环境中,在线重建的并行度设置直接决定了窗口长短。默认情况下,SQL Server 会使用所有可用的 CPU 核心,这看似高效,实则可能引发严重的闩锁竞争。对于超过 1TB 的大表,建议手动设置 MAXDOP 为物理核心数的一半,避免并行线程在等待 PAGEIOLATCH 时互相阻塞。另一个关键参数是 SORT_IN_TEMPDB = ON。开启此选项会将重建过程中的中间排序结果写入 TempDB 而非源数据库文件组。如果你的 TempDB 配置在高性能 SSD 上,而用户库在普通 SAS 盘上,开启此选项能缩短 20%-40% 的操作时间。但务必注意,这会瞬间撑大 TempDB,如果 TempDB 没有预分配足够的空间,操作会直接失败。
在 MySQL 5.6 及更高版本中,ALGORITHM=INPLACE 让很多人误以为可以零风险操作。实际上,InnoDB 的在线 DDL 在重建索引时,会在最后阶段尝试获取排他锁来更新数据字典。如果此时有长时间未提交的事务持有该表的共享锁,DDL 会被阻塞,进而引发雪崩效应——后续所有请求都在等待这个 DDL 释放锁。更隐蔽的是,在操作期间,InnoDB 会将变更记录写入临时日志文件(innodb_online_alter_log_max_size 控制),一旦并发写入量过大,临时日志爆满,操作就会回滚,之前运行的时间全部浪费。对于 MySQL 环境,操作窗口必须避开业务高峰期,并设置合理的 lock_wait_timeout 作为熔断机制。
PostgreSQL 提供的 REINDEX INDEX CONCURRENTLY 是处理索引膨胀的利器。它的机制极其巧妙:先基于旧索引构建新索引,然后等待当前活跃事务结束,最后在系统表中切换新旧索引并删除旧索引。这个过程全程不阻塞读写。但代价是操作时间可能是普通重建的 3 到 5 倍,且需要扫描表两次。如果在构建新索引过程中发生死锁或违反唯一约束,操作会失败,留下一个无效的索引需要手动清理。在执行此操作前,务必确保 maintenance_work_mem 设置得足够大,这能显著加快索引构建的排序阶段,从而缩短窗口期。
固定的每周日凌晨重建索引是一种落后的维护策略。现代的维护应该基于状态驱动。可以利用系统表编写动态维护脚本,逻辑如下:对于碎片率 > 50% 且页数 > 10000 的大索引,执行在线重建;对于碎片率在 10%-50% 之间的,执行在线重组;对于碎片率 < 10% 的,仅更新统计信息。更重要的是,要设置优先级队列。如果维护窗口只剩 30 分钟,而队列中还有 10 个未处理的大索引,脚本必须能够根据索引大小和碎片率自动计算性价比,跳过那些即使重建了也不会带来显著性能提升的索引,确保在窗口关闭前完成最重要的任务。
-- 动态阈值筛选示例(SQL Server 伪代码逻辑)
SELECT
OBJECT_NAME(ips.object_id) AS TableName,
i.name AS IndexName,
ips.avg_fragmentation_in_percent,
ips.page_count,
CASE
WHEN ips.avg_fragmentation_in_percent > 50 AND ips.page_count > 10000
THEN 'REBUILD ONLINE'
WHEN ips.avg_fragmentation_in_percent > 10
THEN 'REORGANIZE'
ELSE 'SKIP'
END AS Action
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') ips
JOIN sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_id
WHERE ips.database_id = DB_ID() AND ips.index_id > 0;
应对超大规模表:分区与切换技术
对于数十亿行级别的超大表,即使是在线重建,时间窗口也可能长达十几个小时,这在 7x24 小时业务中是不可接受的。此时不应直接在主表上操作,而应采用“影子表”或“分区切换”技术。对于非分区表,可以创建一个结构完全相同的新表,在新表上创建优化后的索引,然后通过全量同步加增量 CDC 捕获的方式,在业务低峰期进行毫秒级的 sp_rename 切换。对于分区表,可以针对单个分区进行在线重建,这大大缩短了每次维护的锁持有时间。维护完一个分区后,立即切换回主表,将单次大窗口拆解为多个微窗口。
任何没有监控的在线操作都是裸奔。在执行重建命令前,必须开启针对 blocking_session_id 和 percent_complete 的实时轮询。如果你发现进度长时间停滞在 0% 或某个百分比不动,大概率是遇到了锁阻塞或 I/O 冻结。此时需要预留手动终止操作的权限。终止在线索引重建是安全的,数据库会自动回滚已做的工作,不会导致数据损坏,但回滚本身也需要时间。在窗口设计中,必须预留出“止损时间”,即如果操作在窗口结束前 15 分钟仍未完成,必须强制终止并回滚,绝不能带着一个未完成的事务进入业务高峰期。
很多人忽略了一点:重建索引会顺带更新该索引的统计信息。这看似是好事,但在某些场景下会引发执行计划突变。如果你在维护窗口中重建了索引,统计信息直方图被重置,而查询优化器在第二天早上面对全新的统计信息可能会选择错误的连接顺序。因此,在操作窗口结束后,建议强制更新相关表的全部统计信息,并使用 PERSIST_SAMPLE_PERCENT 或 FULLSCAN 确保采样率足够高,避免因统计信息采样不足导致的性能抖动。将索引维护窗口与统计信息维护窗口视为一个整体,才是完整的优化闭环。
如果你使用的是云数据库(如阿里云 RDS、华为云 GaussDB 或腾讯云 TDSQL),操作窗口的估算逻辑需要调整。云厂商通常对磁盘 IOPS 和日志吞吐量有严格的限流。你看到的“磁盘性能”实际上是经过虚拟化层限速后的结果。在线重建这种密集 I/O 操作极易触发云盘的流控阈值,导致性能断崖式下跌。在云环境中,建议使用厂商提供的无锁变更工具(如 DMS 的无锁结构变更),这些工具通过全量拷贝加增量同步的方式,将主库的负载转嫁到临时实例上,彻底消除窗口限制,但这会增加额外的硬件成本。
总结操作清单在决定按下回车键执行在线重建前,请确认以下清单:事务日志是否有至少 1.5 倍于目标索引大小的剩余空间?TempDB 或临时表空间是否已预扩展?是否已关闭应用层的自动重试机制以防止锁超时风暴?是否已通知上下游系统进入“维护模式”?是否准备好了终止脚本和回滚预案?只有当你对这些问题都有肯定答案时,所谓的“在线”操作才是真正安全的,否则它就是一个伪装成在线操作的离线事故。
