不会锁住整个源表,但会加临键锁;若无索引或WHERE不走索引,则等效“准全表锁”;需分批处理、禁用autocommit并手动提交以避免长事务与binlog膨胀。

大表 INSERT INTO ... SELECT 会锁住源表吗?
MySQL 默认在可重复读(RR)隔离级别下,INSERT INTO ... SELECT 对源表加的是临键锁(Next-Key Lock),不是表锁,但实际效果可能接近全表扫描级锁定——尤其当源表没有合适索引、或 WHERE 条件无法走索引时,InnoDB 会对扫描到的每条记录及其间隙加锁。这意味着并发 SELECT 或 UPDATE 可能被阻塞。
- 如果源表有主键或唯一索引,且
SELECT中的 WHERE 能命中索引范围,锁只会落在匹配的索引区间上 - 如果没走索引(例如
WHERE status LIKE '%draft%'),InnoDB 会遍历聚簇索引,对所有扫描行加锁,等效于“准全表锁” - 在 MySQL 8.0+ 中,若启用了
innodb_locks_unsafe_for_binlog=OFF(默认),该语句仍遵循 RR 的加锁逻辑;设为 ON 可降级为记录锁,但会破坏 binlog 一致性,不推荐
如何避免长事务与 binlog 爆炸?
INSERT INTO ... SELECT 是一个原子语句,整个操作记为单条 binlog event。如果复制百万行,这条 event 就会非常大,导致:
- 主从延迟加剧(从库需重放巨型 event)
-
max_binlog_size失效,单个 binlog 文件可能远超设定值 - 若中途失败,事务回滚代价高,undo log 占用猛增
解决思路是拆分 + 控制事务粒度:
- 用
WHERE id BETWEEN ? AND ?分批次拉取,配合自增主键切片(如每次 10 万行) - 每批单独事务提交,避免长事务拖慢 MVCC 清理
- 设置
innodb_log_file_size足够大(建议 ≥2GB),防止频繁 checkpoint - 关键:禁用 autocommit 后手动
COMMIT,不要依赖隐式提交
SELECT 部分能否加 LOCK IN SHARE MODE 或 FOR UPDATE?
不能,也不该加。
-
INSERT INTO ... SELECT语法中,SELECT子句不允许出现FOR UPDATE或LOCK IN SHARE MODE,MySQL 会直接报错ERROR 1205 (HY000): Deadlock found when trying to get lock或更明确的语法错误 - 即使绕过语法限制(比如先
SELECT ... INTO @var再INSERT),也会失去原子性,且手动加锁容易引发死锁 - 真实需求往往是“防止源数据被改”,这时应确保业务层已规避写入冲突,或改用应用层分页 + 应用控制版本号/时间戳校验
替代方案:用 LOAD DATA 或 pt-archiver 更稳?
LOAD DATA INFILE 和 pt-archiver 是更可控的选择,尤其对超大表:
-
LOAD DATA不经过 SQL 解析层,速度更快,锁等待更短;但需先导出为文本,且目标表不能有外键或触发器干扰 -
pt-archiver默认按主键分块、逐批DELETE+INSERT,支持限流(--limit)、休眠(--sleep)、错误跳过,对线上影响小 - 注意:
pt-archiver的--bulk-insert模式本质仍是多行INSERT,不会变成INSERT ... SELECT,所以不触发源表临键锁
真正容易被忽略的是:即使加了索引、分了批、关了 autocommit,只要源表存在未提交的长事务(比如另一个连接正在跑 UPDATE 无 WHERE),INSERT INTO ... SELECT 仍可能因 read view 太旧而卡住——这不是锁问题,是 MVCC 快照可见性导致的隐式等待。










