INSERT INTO ... SELECT 不锁整表但会因REPEATABLE READ下全表扫描触发共享next-key锁而等效锁表;根本原因是SELECT部分对所有扫描行及间隙加锁且全程持有,无索引时尤为严重;加索引是首要解法,分批执行可拆长锁为短锁。

INSERT INTO ... SELECT 本身不“锁整张表”,但会在源表上加共享 next-key lock,一旦扫描范围大、没走索引、隔离级别是 REPEATABLE READ(默认),就会等效锁表——这是真实瓶颈,不是误判。
为什么源表会被长时间锁定?
关键不在 INSERT,而在 SELECT 部分的加锁逻辑:
-
REPEATABLE READ下,InnoDB 对所有被扫描到的行 + 间隙加共享next-key lock,防止幻读 - 如果
WHERE条件没走索引,就会全表扫描 → 锁住源表几乎所有行和间隙 - 锁持续整个语句执行期,不是逐行释放;哪怕只插入 1 行,扫描了 100 万行,就锁了 100 万行+间隙
-
innodb_autoinc_lock_mode = 1(默认)时,目标表有自增主键,还会额外持有一个较重的 AUTO-INC 表级锁
加索引是最直接有效的解法
索引不是“可选优化”,而是锁范围控制的前提。没索引,其他所有手段都是兜底。
- 确保
SELECT中的WHERE、JOIN、ORDER BY字段都有合适索引,特别是组合条件要匹配最左前缀 - 用
EXPLAIN验证是否真的走了索引:type应为ref或range,不是ALL - 对高频迁移场景(如按时间归档),在时间字段上建索引后,再加
FORCE INDEX显式指定,避免优化器误选 - 注意:索引字段类型要和查询条件一致,比如
created_at = '2023-01-01'是字符串,但字段是DATETIME,隐式转换会导致索引失效
分批执行比单次大操作更可控
即使加了索引,单次扫描 50 万行仍可能阻塞业务。分批的核心价值是把长锁拆成多个短锁。
- 不要用
LIMIT直接套在原语句里(MySQL 不支持INSERT ... SELECT ... LIMIT的标准写法),改用游标或应用层分页 - 推荐方式:先查出最小/最大 ID 或时间戳边界,再用
BETWEEN或>= AND 分段 - 示例(安全分批):
INSERT INTO target SELECT * FROM source WHERE create_time BETWEEN '2023-01-01' AND '2023-01-02';
- 每次执行后加
SLEEP(0.1)(应用层控制),避免瞬间压垮主库 - 注意:分批不能解决无索引问题,只是降低单次影响;必须配合索引使用
临时表能解耦锁时机,但不减少锁总量
CREATE TEMPORARY TABLE ... AS SELECT 不是“绕过锁”,而是把“锁源表”和“写目标表”两个动作物理分离。
- 第一步创建临时表时,依然会对源表加
next-key lock,但它是单次、短时行为,锁释放快 - 后续从临时表插入目标表,只锁目标表,不影响源表并发读写
- 适合定时任务场景:凌晨跑一次
CREATE TEMPORARY TABLE tmp AS SELECT ...,白天再分批INSERT INTO target SELECT ... FROM tmp - 陷阱:
TEMPORARY TABLE只在当前会话可见,不能跨连接;若用连接池,需确保复用同一连接,否则查不到临时表
真正容易被忽略的是:锁问题常被误判为“慢查询”,但 SHOW ENGINE INNODB STATUS\G 里看到的 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: 才是根源线索——它暴露的是谁在等、等什么锁,而不是花了多少秒。


















