用 NOT EXISTS 子查询过滤可避免主键冲突,如 INSERT INTO users SELECT * FROM staging_users WHERE NOT EXISTS (SELECT 1 FROM users u WHERE u.phone = s.phone),需确保关联字段有索引。

INSERT INTO SELECT 遇到主键冲突就报错?用子查询绕过重复数据
直接执行 INSERT INTO t1 SELECT * FROM t2 很容易因主键/唯一约束失败。这不是语法问题,而是设计上没做存在性校验。真正安全的做法,是在 SELECT 里嵌套子查询过滤掉目标表已有的记录。
- 典型错误现象:
ERROR 1062 (23000): Duplicate entry 'xxx' for key 'PRIMARY' - 适用场景:从日志表、临时表、ETL中间表批量导入主数据,且目标表已有部分数据
- 核心思路:把
SELECT的结果集和目标表自身做LEFT JOIN或NOT EXISTS判断,只取“不存在”的行 - 性能注意:如果目标表很大,
NOT EXISTS通常比LEFT JOIN ... IS NULL更快,尤其在有索引的关联字段上
用 NOT EXISTS 写条件插入最稳妥
NOT EXISTS 语义清晰、行为确定,是多数生产环境首选。它明确表达“只要目标表里查不到匹配项,就插入”,不会受空值(NULL)干扰,也不依赖连接字段是否可为空。
- 示例(向用户表插入新注册用户,跳过已存在的手机号):
INSERT INTO users (phone, name, created_at) SELECT s.phone, s.name, s.created_at FROM staging_users s WHERE NOT EXISTS ( SELECT 1 FROM users u WHERE u.phone = s.phone );
- 关键点:
SELECT 1是惯用写法,不查实际字段,只判断是否存在;子查询里的u.phone = s.phone必须两边类型一致,否则可能隐式转换导致索引失效 - 容易踩的坑:如果
staging_users.phone含空值,NOT EXISTS仍能正确处理;但若误写成NOT IN (SELECT phone FROM users),只要users.phone有任意一个NULL,整个条件就恒为FALSE,一条都插不进去
INSERT IGNORE 和 ON DUPLICATE KEY UPDATE 不是子查询,别混用
这两个是 MySQL 特有语法,属于“插入时容错”,不是“前置条件过滤”。它们发生在插入执行阶段,而 INSERT INTO SELECT + 子查询是在生成结果集阶段就完成筛选——这是根本区别。
-
INSERT IGNORE会静默跳过冲突行,但无法控制跳过逻辑(比如你想跳过旧数据、保留新数据,它做不到) -
ON DUPLICATE KEY UPDATE本质是“插入或更新”,不是“条件插入”;它触发的是UPDATE操作,会产生额外的 binlog 和锁开销 - 跨数据库不兼容:PostgreSQL 没有
INSERT IGNORE,SQL Server 用MERGE,而子查询方案(NOT EXISTS)在所有标准 SQL 引擎中都可用
WHERE 条件里引用子查询要小心相关性
子查询写在 WHERE 里时,如果引用了外部查询的列(比如上面例子中的 s.phone),就是“相关子查询”,会被外部每一行调用一次。这没问题,但必须确保被关联的字段有索引,否则性能会断崖式下跌。
- 检查方式:对
users表的phone字段加索引 ——CREATE INDEX idx_users_phone ON users(phone); - 如果关联字段没索引,10 万行源数据可能触发 10 万次全表扫描,比单条
INSERT ... VALUES循环还慢 - 另一个常见疏忽:子查询里用了函数包裹字段,比如
WHERE u.phone = UPPER(s.phone),会导致索引失效;应统一清洗好再比较
子查询做条件插入的关键,是把“要不要插”的判断提前到数据拉取阶段,而不是靠数据库引擎事后纠错。最容易被忽略的,其实是索引——没索引的 NOT EXISTS 看似简洁,实际可能让凌晨的导入任务跑成白天。










