根本原因是未走聚集索引查找路径,导致全表扫描+行级锁逐行判断,I/O与锁开销剧增;WHERE条件必须命中聚集索引列才能实现范围定位和顺序I/O。

为什么大表 DELETE 会慢得像卡住?
根本原因不是 SQL 写得不对,而是没走聚集索引查找路径。当 DELETE FROM t WHERE status = 'archived' 这类语句缺少主键或聚簇键过滤时,数据库大概率触发全表扫描 + 行级锁逐行判断,I/O 和锁开销爆炸。尤其在千万级以上、无合适索引的表上,可能持续数小时甚至导致连接堆积。
WHERE 条件必须命中聚集索引列
聚集索引决定了数据物理存储顺序,只有利用它才能实现范围定位和顺序 I/O。如果表主键是 id(默认即聚集索引),但你删的是 WHERE create_time ,而 <code>create_time 没建索引——那就等于放弃聚集优势。
- 确认聚集索引列:
SELECT index_name, is_clustered FROM sys.dm_db_index_usage_stats s JOIN sys.indexes i ON s.object_id = i.object_id AND s.index_id = i.index_id WHERE s.object_id = OBJECT_ID('your_table')(SQL Server);MySQL 中看PRIMARY KEY或CLUSTERED属性 - 若业务删除条件天然不匹配聚集键(比如按时间删旧数据),必须在该列上建立**非聚集索引**,否则无法避免扫描
- 避免在聚集索引列上用函数:
WHERE YEAR(created_at) = 2022会跳过索引;改用created_at >= '2022-01-01' AND created_at
分批删除比单次 DELETE 更可靠
即使加了索引,一次性删百万行仍可能引发长事务、日志暴涨、阻塞其他操作。分批本质是把大 I/O 拆成多个小窗口,让聚集索引范围扫描真正“可控”。
- 用聚集索引列做游标分页:
DELETE TOP (5000) FROM orders WHERE id (SQL Server);MySQL 用 <code>LIMIT+ 子查询定位边界 - 每次删除后加
WAITFOR DELAY '00:00:00.1'(SQL Server)或SLEEP(0.1)(MySQL),缓解日志压力和锁竞争 - 务必在循环中检查
@@ROWCOUNT(SQL Server)或ROW_COUNT()(MySQL),避免无限循环
别忽略聚集索引碎片对删除性能的反向拖累
频繁增删后,聚集索引页会出现逻辑碎片,导致同样删 1 万行要读更多页。此时即使条件命中索引,实际 I/O 效率也大打折扣。
- 查碎片率:
SELECT avg_fragmentation_in_percent FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID('t'), NULL, NULL, 'LIMITED') - 碎片 > 30% 建议重建:
ALTER INDEX PK_t ON t REBUILD;10%~30% 可重组织:ALTER INDEX PK_t ON t REORGANIZE - 重建期间表不可写,生产环境需避开高峰;重组织可在线,但耗时更长
最常被忽略的一点:删除本身不释放空间给操作系统,只是标记为可复用。如果后续没有足够新数据填充,聚集索引页仍处于稀疏状态,下次删除还是得扫更多页。

















