先确认是否因为未命中索引而导致删除阻塞:执行删除时卡顿、并发写入持续超时、TRX_ROWS_LOCKED接近整表总行数,都是 MySQL 大批量 DELETE 锁表的典型信号;必须通过EXPLAIN DELETE实际验证,重点关注type=ALL、key=NULL、rows≈全表扫描;如果子查询IN未生效,建议改写为JOIN删除。

DELETE卡住时,先确认是不是没走索引
如果一执行删除就卡住、并发写请求频繁超时,或者在INFORMATION_SCHEMA.INNODB_TRX中看到TRX_ROWS_LOCKED几乎逼近整张表的总行数,这些都属于非常明显的预警信号。不要凭经验判断“字段建了索引就一定会命中索引”,像EXPLAIN DELETE FROM t WHERE status = 'old'这样的执行计划检查一定要真实跑一遍。重点查看几个核心字段:type是否为ALL,key是否为NULL,rows是否已经接近全表扫描规模。还有一个在 MySQL 优化中很常见的陷阱:当子查询写成WHERE ... IN (SELECT ...)时,往往难以走到理想的索引访问路径;改写为DELETE t1 FROM t1 JOIN t2 ON ...,通常执行更稳定,也更有利于降低锁表风险。
分批删LIMIT值设多少才不伤性能
单次删除时的LIMIT并不是越大越高效,也不是越小越安全。一般来说,500–5000 是实战中相对稳妥的区间,但最终仍要结合数据库实际负载来调整:
- 如果主键连续且删除范围明确(如
id BETWEEN @start AND @end),可优先使用1000–5000,尽量避免LIMIT偏移带来的额外扫描成本 - 如果使用非主键条件(如
create_time < '2024-01-01'),建议先从500开始,重点观察慢查询日志中的Rows_examined是否明显下降 - 每批执行完成后增加短暂间隔,例如
SLEEP(0.1)(应用层)或DO SLEEP(0.05)(存储过程),给其他事务留出处理空间,缓解锁竞争
90%以上数据要删,别硬删,换表
如果待删除数据占比超过90%,而真正需要保留的数据很少,那么继续直接执行DELETE本质上是在做高成本减法,性能和锁影响都很重。更合适的 MySQL 大批量删除方案通常是“插入→重命名→删除旧表”:
- 先创建新表结构:
CREATE TABLE user_history_new LIKE user_history - 只导入需要保留的数据:
INSERT INTO user_history_new SELECT * FROM user_history WHERE create_time >= '2026-07-15' - 再进行原子切换:
RENAME TABLE user_history TO user_history_old, user_history_new TO user_history - 最后在后台异步删除旧表:
DROP TABLE user_history_old
这种方式在整个过程中通常不会锁住原表读写,但需要特别注意两点:切换前必须确认新表上的索引完整无误;同时RENAME属于原子操作,整体失败概率非常低,因此非常适合大批量数据清理场景。
杀进程前,先搞清谁真在阻塞
KILL并不是处理锁等待的第一选择,盲目终止进程反而可能让事务回滚更慢、日志压力更大。更稳妥的做法,是先查清楚真实的阻塞链:
- MySQL 5.7 及以上版本可直接执行:
SELECT * FROM sys.innodb_lock_waits,结果中的blocking_pid才是真正的阻塞源头 - 老版本可以使用:
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX ORDER BY TRX_STARTED LIMIT 10,重点查找TRX_STATE = 'LOCK WAIT'且TRX_ROWS_LOCKED异常偏高的事务 - 确认阻塞来源后,再执行:
KILL,而不是随意终止SHOW PROCESSLIST里看起来可疑的线程
实际排查中最容易被忽略的一点是:真正造成阻塞的,往往是一个长时间未提交的事务,它甚至可能并不是你当前正在分析的那条DELETE语句——因此需要顺着TRX_WAITING_TRX_ID和TRX_BLOCKING_TRX_ID继续向上追踪,才能准确定位问题根源。
