insert into ... select 比多次 insert 快得多,因其仅触发一次计划编译、事务日志写入和锁协商,且服务端批量处理,避免客户端往返与逐行序列化;需严格匹配字段顺序与类型,自增主键勿显式插入,跨库迁移须用反引号(mysql)、双引号(pg)或方括号(sql server)转义标识符,where 中子查询应改写为 join 并确保关联字段有索引,分批处理推荐用 last_processed_id 而非单纯时间范围,以防重复或遗漏。

INSERT INTO ... SELECT 为什么比多次 INSERT 快得多
因为单次语句触发一次计划编译、一次事务日志写入、一次锁粒度协商;而循环插入每条都重复这些开销,尤其在千万级数据迁移时,性能差距可达 10 倍以上。INSERT INTO t2 SELECT * FROM t1 WHERE status = 'active' 这类写法本质是服务端批量拉取+批量写入,绕过了客户端网络往返和驱动层逐行序列化。
- 确保
t2的字段顺序、类型与SELECT列严格匹配,否则报错Column count doesn't match value count - 如果目标表有自增主键,
SELECT中不要显式提供该列值(除非你真想覆盖),否则可能触发Duplicate entry - MySQL 8.0+ 支持
INSERT IGNORE INTO或ON DUPLICATE KEY UPDATE处理冲突,但注意后者会触发行锁升级
跨库迁移时表名/字段名带特殊字符怎么写
反引号不是可选项,是必须项。哪怕只是字段含下划线或大小写混用,在 MySQL 中不加 ` 就可能解析失败;PostgreSQL 要求双引号 " ";SQL Server 用方括号 [ ]。别依赖“好像没报错”,线上环境大小写敏感策略、SQL 模式(如 ANSI_QUOTES)一变就崩。
- MySQL 示例:
INSERT INTO `user_log_2024` SELECT `user_id`, `action_time`, `payload` FROM `legacy`.`user_event` WHERE `type` = 'login' - PostgreSQL 示例:
INSERT INTO "orders_new" SELECT "id", "created_at", "total" FROM "old_schema"."orders_old" - 别把数据库名拼进字段名里,
db.table.col在SELECT子句中无效,只在FROM或JOIN中合法
WHERE 条件里用子查询导致超慢甚至卡死
常见写法 INSERT INTO t2 SELECT * FROM t1 WHERE id IN (SELECT id FROM t3) 在 MySQL 5.7 及更早版本默认走嵌套循环,t1 表每行都执行一次子查询——100 万行就是 100 万次独立查询。换成 JOIN 写法,优化器才能走哈希连接或索引合并。
- 改写为:
INSERT INTO t2 SELECT t1.* FROM t1 INNER JOIN t3 ON t1.id = t3.id - 确认
t3.id有索引,否则JOIN也慢;EXPLAIN看type列是不是ref或eq_ref - 如果子查询结果集很小(CREATE TEMPORARY TABLE tmp_ids AS SELECT id FROM t3,再
JOIN tmp_ids,避免重复计算
迁移中途失败,怎么安全续跑不重复不漏
靠人工记最大 ID 或时间戳最直接,但前提是源表有单调递增的标识字段且无删改。没有的话,得加中间状态表记录已处理批次,否则重试等于双写,主键冲突或唯一约束报错是大概率事件。
- 推荐方案:用
WHERE id > ?+ 应用层维护一个last_processed_id变量,每次成功插入后更新它 - 避免用
WHERE created_at >= '2024-01-01'续跑——源表该字段可能有重复值或空值,导致某批漏掉或多插 - 如果必须按时间范围切分,加
AND id > ?作为第二条件兜底,双重保险
真正麻烦的从来不是语法对不对,而是你假设“源数据静止”那一刻起,就已经埋了数据不一致的伏笔。










