直接 INSERT INTO t2 SELECT * FROM t1 易阻塞业务,因在 REPEATABLE READ 下对 t1 扫描部分执行当前读并加 next-key 锁;若无索引则全表扫描,锁住整个主键区间,导致其他事务更新任意行均被阻塞。

能高效,但默认写法大概率会卡住线上服务——关键不在“能不能”,而在“怎么锁”和“怎么切”。
为什么直接 INSERT INTO t2 SELECT * FROM t1 容易阻塞业务
InnoDB 在 REPEATABLE READ 隔离级别下对 SELECT 部分走“当前读”,会根据扫描范围加 next-key lock。没索引?全表扫描 → 锁整个主键索引区间 → 其他事务更新任意行都可能被堵住。
- 用
EXPLAIN SELECT * FROM t1 WHERE ...看type是否为ALL:是,立刻加索引 - 检查
Extra是否有Using temporary或Using filesort:有,说明 JOIN 或 ORDER BY 没走索引,得调 - 目标表如果有大量二级索引,插入前可考虑
ALTER TABLE t2 DISABLE KEYS(仅 MyISAM 有效),InnoDB 不支持,别白试
分批迁移必须避开 LIMIT 子查询这个坑
MySQL 5.7 及更早版本不支持 INSERT INTO t2 SELECT * FROM (SELECT * FROM t1 LIMIT 1000) AS tmp 这种写法,报错 This version of MySQL doesn’t yet support ‘LIMIT & IN/ALL/ANY/SOME subquery’。
- 正确做法是用主键范围切分:
WHERE id BETWEEN 1000001 AND 1010000 - 避免
LIMIT 1000 OFFSET 1000000:OFFSET 越大越慢,底层仍要扫描前面所有行 - 若主键不连续或有删除空洞,改用时间字段(如
created_at)+ 索引分片,但需确保该字段单调递增且有索引
主键冲突时别指望 INSERT IGNORE 一劳永逸
INSERT IGNORE INTO t2 SELECT ... FROM t1 看似省事,但 MySQL 8.0.19+ 对 IGNORE 的语义收紧了:部分唯一键冲突不再静默跳过,而是转成 warning 并继续执行;而整条语句仍是原子的,遇到约束违反仍可能回滚。
- 更可控的是
ON DUPLICATE KEY UPDATE id = id(空更新),它明确跳过冲突行且不报错 - 若需记录哪些行被跳过,必须拆成两步:
SELECT t1.* FROM t1 LEFT JOIN t2 ON t1.id = t2.id WHERE t2.id IS NULL,再插 - 提前用
SELECT COUNT(*) FROM t1 WHERE id IN (SELECT id FROM t2)评估冲突比例,决定是否先清洗源数据
binlog_format = STATEMENT 下执行失败怎么办
常见报错 ERROR 1786: Statement is not safe to log in statement format,本质是 NOW()、UUID()、子查询带 LIMIT 等非确定性操作,在语句级复制下无法保证主从一致。
- 生产环境首选方案:
SET SESSION binlog_format = 'ROW',执行完再切回去(ROW 格式能精确记录每一行变更) - 临时绕过:把
NOW()换成应用层传入的固定时间戳,或用SYSUTCDATE()(注意时区) -
SET SESSION sql_log_bin = 0仅限测试库,生产禁用——它直接不记 binlog,主从彻底断开
真正难的不是写出那条 SQL,而是判断哪段 WHERE 条件会让 InnoDB 锁住三万行而不是三百行;分批的边界值选错一个,后续批次就全偏移。锁、索引、binlog 三者咬合紧密,动一发而牵全身。


















