SELECT ... JOIN 本身不锁表,但可能锁行、间隙或索引页;是否加锁取决于隔离级别、是否有锁提示(如FOR UPDATE)、JOIN字段是否走索引及执行路径。

SELECT ... JOIN 会锁表吗?取决于隔离级别和访问路径
不会自动锁整张表,但可能锁住被扫描或命中的索引页、行甚至间隙。MySQL 默认 REPEATABLE READ 下,SELECT ... JOIN 本身不加锁——除非用了 FOR UPDATE 或 LOCK IN SHARE MODE;但如果是可重复读下执行 UPDATE/DELETE 带 JOIN,InnoDB 很可能对驱动表和被驱动表都加临键锁(next-key lock),尤其当关联字段没走索引时。
- 没索引的 JOIN 条件 → 全表扫描 → 大概率锁全表对应的所有聚簇索引页
- 有覆盖索引但没主键参与 → 可能只锁二级索引页,但若回表查主键,仍会锁主键记录
- READ COMMITTED 隔离级别下,普通 SELECT 不加锁,UPDATE/DELETE 的锁范围更小(只锁匹配行,不锁间隙)
JOIN 查询引发死锁的典型场景与定位方法
死锁常发生在两个事务以不同顺序访问相同表集,比如事务 A 先查 orders 再查 order_items,事务 B 反过来先查 order_items 再查 orders,且都带 FOR UPDATE。InnoDB 死锁检测会报错 Deadlock found when trying to get lock,但不会告诉你哪条 SQL 触发的。
- 用
SHOW ENGINE INNODB STATUS\G查最近死锁详情,重点关注TRANSACTION和WAITING FOR THIS LOCK TO BE GRANTED段 - 确保所有涉及写操作的 JOIN,驱动表顺序统一(例如始终按主表→子表顺序)
- 避免在事务里跨多个 JOIN 执行多次
SELECT ... FOR UPDATE,尽量合并为单条带锁查询
LEFT JOIN 在高并发更新下的隐式锁风险
LEFT JOIN 本身不加锁,但如果你在事务中用它查出数据后,紧接着对右表(NULL 行对应的那张表)做 INSERT/UPDATE,就可能触发唯一索引冲突检查锁,或因插入位置触发间隙锁。更隐蔽的是:优化器可能把 LEFT JOIN 改写成 INNER JOIN(比如 WHERE 条件里对右表字段非空判断),导致实际加锁范围远超预期。
- 检查执行计划,确认
type是ref还是ALL;Extra是否含Using join buffer(意味着可能全表扫描右表) - 避免在
WHERE子句中写t2.status IS NOT NULL这类条件,它会让LEFT JOIN实际退化为INNER JOIN - 如果只是为判空,改用
EXISTS或单独查右表,逻辑更清晰、锁更可控
如何用 EXPLAIN + INFORMATION_SCHEMA 查锁影响范围
光看 SQL 写法没法判断锁行为,必须结合执行路径和索引结构。重点不是“有没有 JOIN”,而是“哪些索引被用上”“是否回表”“是否触发唯一约束检查”。
- 运行
EXPLAIN FORMAT=JSON,关注key(实际用的索引)、rows(预估扫描行数)、filtered(过滤率),低过滤率 + 高 rows = 锁风险高 - 查
INFORMATION_SCHEMA.INNODB_TRX和INNODB_LOCK_WAITS,实时观察谁在等锁、等哪条记录 - 对关键 JOIN 字段,务必建立联合索引(如
ON o.id = oi.order_id,就在order_items(order_id)上建索引;若还有WHERE status = ?,则建(order_id, status))
锁的粒度从来不由 JOIN 语法决定,而由存储引擎怎么走索引、事务怎么读写、隔离级别怎么解释“一致性”共同决定。最容易被忽略的,是那些没报错、没死锁、但让其他事务莫名卡住几秒的“半锁”状态——它往往藏在看似安全的 SELECT ... LEFT JOIN 后面,等着你在凌晨三点收到告警。










