软删除慢的主因是WHERE未命中索引、批量过大、全表扫描;需为deleted_at或status字段建索引,避免函数包裹字段导致索引失效。

软删除的UPDATE语句本身不慢,慢的是没加WHERE条件索引、没控制批量大小、没避开全表扫描——这三者比SQL写法本身影响大得多。
WHERE子句必须命中索引,否则UPDATE会锁全表
MySQL在执行UPDATE时,若WHERE无法使用索引,就会走全表扫描+行锁升级为表锁(尤其在RR隔离级别下),导致其他查询被阻塞。软删除通常基于status或deleted_at字段,但这两个字段往往没建索引。
- 检查索引:
SHOW INDEX FROM users WHERE Key_name LIKE '%deleted%'; - 补索引(按实际字段名调整):
ALTER TABLE users ADD INDEX idx_deleted_at (deleted_at);或ADD INDEX idx_status (status); - 避免用函数包裹字段:
WHERE DATE(deleted_at) = '2024-01-01'会失效索引;改用WHERE deleted_at >= '2024-01-01 00:00:00' AND deleted_at
单次UPDATE别超5000行,用主键范围分批
大事务会撑爆undo log、延长锁持有时间、拖慢复制延迟。靠LIMIT分批最直接,但LIMIT在UPDATE中不支持OFFSET,所以得用主键切片。
- 先查出待删ID范围:
SELECT MIN(id), MAX(id) FROM users WHERE status = 0 AND deleted_at IS NULL; - 再分段更新(每次1000行):
UPDATE users SET deleted_at = NOW(), status = 2 WHERE id BETWEEN 10000 AND 10999 AND status = 0 AND deleted_at IS NULL; - 确认影响行数为0时停止循环(避免死循环)
避免在UPDATE中计算字段值,尤其是NOW()以外的时间
NOW()是安全的,但DATE_ADD(NOW(), INTERVAL 1 DAY)这类表达式在每行都会重新求值,虽开销小,但在百万级更新中会放大。更危险的是引用子查询或UDF,可能触发临时表或全表扫描。
- 禁止写法:
UPDATE users SET deleted_at = (SELECT MAX(created_at) FROM logs) WHERE ... - 推荐写法:先算好值:
SET @del_time = NOW(); UPDATE users SET deleted_at = @del_time WHERE ...; - 如果需不同时间戳(如按用户创建时间推延),必须确保该字段有索引,且WHERE里不破坏sargable条件
UPDATE后立刻检查rows affected,别信“成功”日志
MySQL客户端或ORM常把“Query OK, 0 rows affected”也当成功返回,但软删除场景下,0行影响往往意味着WHERE条件写错(比如误用status = 1而非status = 0),或者索引失效导致没匹配到数据。
- 执行后立刻查:
SELECT ROW_COUNT();—— 返回0就要停,别继续下一批 - 上线前用
EXPLAIN UPDATE ...(MySQL 8.0.19+ 支持)或改写为EXPLAIN SELECT验证执行计划是否走了预期索引 - 生产环境首次跑批量前,先在从库用
SELECT COUNT(*)核对预估数量是否合理
真正卡住批量软删除的,从来不是UPDATE语法多难写,而是没人去确认那条WHERE到底扫了几行、锁了多久、有没有在凌晨三点把主库binlog写满。


















