单条DELETE语句无法“不锁表”,必须通过索引WHERE、分批限速、主键范围切片、显式提交及规避隐式锁源来保障安全。

不能靠单条 DELETE 语句“不锁表”——它天然加行锁,大批量时必然触发锁升级。真正安全的路径只有一条:控制每次锁的范围、持续时间和并发冲击。
WHERE 条件必须走索引,否则等于全表锁
没索引的 created_at 或 status 字段在 DELETE 中用作条件,InnoDB 就得全表扫描;每扫一行加行锁 + 间隙锁,锁数量爆炸后自动升级为表锁。
- 用
EXPLAIN SELECT * FROM logs WHERE created_at 替代 <code>EXPLAIN DELETE,确认type是range或ref,不是ALL - 别写
WHERE DATE(created_at) = '2024-01-01'—— 函数导致索引失效 - 别写
WHERE user_id = '123'(user_id是INT)—— 隐式类型转换跳过索引 - 加索引务必选低峰期,MySQL 5.6+ 可用
ALGORITHM=INPLACE减少锁表时间
分批必须带 ORDER BY + 主键/索引列,不能只靠 LIMIT
单独 LIMIT 很危险:优化器可能忽略它,或因无序导致重复删、漏删;并发写入时更易错乱。
- 正确写法:
DELETE FROM logs WHERE created_at (<code>id必须有索引) - 别用
LIMIT 10000, 5000这种 offset 分页——偏移越大越慢,且中间插入新行会导致跳过 - 每批后查
ROW_COUNT(),返回0就停;别硬设循环次数 - 每批必须显式
COMMIT(或确保autocommit=1),锁释放时机取决于事务结束,不是语句执行完
主键范围切片比游标式循环更稳
游标式 ORDER BY ... LIMIT 在高并发下行为不稳定:新插入的行可能挤进下一批范围,导致漏删;而主键范围(如 BETWEEN)边界确定、可预测、易监控。
- 先查边界:
SELECT MIN(id), MAX(id) FROM logs WHERE created_at - 再按步长删:
DELETE FROM logs WHERE id BETWEEN 100001 AND 105000 AND created_at - 步长建议 500–5000 行,太大易锁争抢,太小增加解析开销
- 每批后加
DO SLEEP(0.1)(MySQL)或应用层time.sleep(0.1),缓解 I/O 和 binlog 压力
真正容易被忽略的隐形锁源
你加了索引、分了批、限了速,还是卡——大概率不是 SQL 写得不对,而是别的进程在“偷偷锁着表”。这类问题不报错,但会让所有优化失效。
- 监控脚本每 5 秒跑一次
SELECT COUNT(*) FROM t,拿的是共享锁,而你的DELETE在等排他锁 - 有外键约束或触发器的表,删前必须评估它们对单次耗时的影响
- binlog 日志空间是否够撑住整个过程?主从延迟是否已偏高?这些不提前摸清,脚本一跑,DBA 就得半夜爬起来救火

















