先确认是否未走索引:删一行卡住、并发写入超时、trx_rows_locked接近表总行数是典型信号;必须用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是不是接近全表。子查询写在WHERE ... IN (SELECT ...)里基本不走索引,换成DELETE t1 FROM t1 JOIN t2 ON ...更稳。
分批删LIMIT值设多少才不伤性能
单次LIMIT不是越大越好,也不是越小越安全。500–5000是实测较稳妥的区间,但得看实际负载:
- 主键连续且范围明确(如
id BETWEEN @start AND @end),用1000–5000,避免LIMIT偏移带来的扫描开销 - 非主键条件(如
create_time ),建议从500起步,观察慢查询日志里<code>Rows_examined是否明显下降 - 每批执行后加
SLEEP(0.1)(应用层)或DO SLEEP(0.05)(存储过程),给其他事务喘息时间
90%以上数据要删,别硬删,换表
保留数据远少于待删数据时,DELETE本质是做减法,代价极高。正确做法是“插入→重命名→删旧表”:
- 建新表结构:
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 <blocking_pid></blocking_pid>,而不是随便杀SHOW PROCESSLIST里看着像的
真正容易被忽略的是:阻塞往往来自一个长时间未提交的事务,它可能根本不是你在查的那个DELETE语句——得顺着TRX_WAITING_TRX_ID和TRX_BLOCKING_TRX_ID往回追。











