delete join锁全表是因为执行计划崩坏导致全表扫描,只要explain出现type: all或key: null,innodb就会对扫描行加临键锁,即使删1行也可能锁几万行甚至整表。

DELETE JOIN 为什么一执行就锁全表
不是语句写错了,而是执行计划崩了——只要 EXPLAIN 里出现 type: ALL 或 key: NULL,说明某张参与 JOIN 的表正在被全表扫描,InnoDB 就会为扫描到的每一行加临键锁(next-key lock),哪怕你只删 1 行,锁住的可能是几万行甚至整张表。
- 驱动表 WHERE 条件没索引 → 扫描驱动表全量,每行都触发一次被驱动表全扫
- 被驱动表 JOIN 字段没索引 → 每次匹配都得全表比对,锁范围爆炸式扩大
- ON 子句用了函数(如
ON DATE(o.created_at) = u.join_date)→ 索引完全失效,强制走 ALL - 隐式类型转换(如
user_id = '123'对 INT 字段)→ 同样跳过索引,触发全扫
如何用 EXPLAIN 快速定位锁表根源
别等超时再查,删之前必须跑一遍 EXPLAIN,重点盯三处:
-
type列:ALL或index是红灯,ref、eq_ref、range才算安全 -
rows列:不是物理行数,是优化器估算的“要检查的行数”,若显示几十万/百万,说明锁范围失控 -
Extra列:出现Using join buffer说明没走索引,靠内存缓冲硬扛,高并发下极易锁等待
示例:EXPLAIN DELETE u FROM users u JOIN orders o ON u.id = o.user_id WHERE o.status = 'cancelled'; —— 若 o.status 没索引,orders 表就会被全扫。
真正能防锁表的索引策略
不是“建个索引就行”,而是按执行路径精准覆盖:
- 驱动表的
WHERE字段必须有索引(如orders.status) - 被驱动表的
JOIN字段必须有索引(如users.id,主键天然满足) - 复合条件优先用联合索引:比如
WHERE o.status = 'cancelled' AND o.created_at > '2025-01-01',建INDEX idx_status_created (status, created_at),顺序不能反 - 避免在索引字段上做任何运算:
YEAR(created_at)、UPPER(name)全部禁止出现在ON或WHERE左侧
线上删数据必须分批 + 验证
一条 DELETE JOIN 删几万行,等于把锁持有时间拉长到秒级,业务写入立刻排队。安全做法是拆成小批次,并确保每批都走索引:
- 先用
SELECT验证逻辑:SELECT COUNT(*) FROM users u JOIN orders o ON u.id = o.user_id WHERE o.status = 'cancelled'; - 改写为带主键范围的分批删:
DELETE u FROM users u JOIN orders o ON u.id = o.user_id WHERE o.status = 'cancelled' AND u.id BETWEEN 10000 AND 20000; - 每次删完检查
ROW_COUNT(),控制LIMIT和间隔(如DO SLEEP(0.05))缓解 IO 压力 - 千万级数据慎用 JOIN 删除,优先考虑先
SELECT id INTO TEMP TABLE,再单表删,更可控
最易被忽略的一点:即使所有字段都有索引,如果统计信息陈旧(ANALYZE TABLE 没跑过),优化器仍可能选错驱动表,导致本该小表驱动大表,结果反过来了——锁表风险藏在看不见的地方。










