INSERT … SELECT + WHERE NOT EXISTS 是最稳妥的防重复插入方案,通过关联子查询判断记录是否存在,避免并发竞态且不受NULL影响,但需确保判断字段有索引。

INSERT ... SELECT + WHERE NOT EXISTS 是唯一能真正通过子查询逻辑防重复插入主键的通用方案,不需要依赖唯一索引,也不受 NULL 值干扰 —— 但必须确保子查询中主键字段参与关联判断,且整个语句结构符合目标数据库语法。
MySQL 中 INSERT ... SELECT 防主键重复的写法要点
MySQL 不支持直接在 INSERT INTO t VALUES (...) 后加 WHERE NOT EXISTS,必须转为 SELECT 形式。常见错误是漏掉 FROM dual 或写成无意义的子查询。
- 正确写法必须包含
SELECT和FROM:例如INSERT INTO users (id, name) SELECT 100, 'alice' FROM dual WHERE NOT EXISTS (SELECT 1 FROM users WHERE id = 100) - 不能省略
FROM dual(MySQL 8.0+ 允许省略,但低版本会报错;显式写出更稳妥) - 子查询里必须用外层值做等值判断,比如
WHERE id = 100,不能只写WHERE NOT EXISTS (SELECT 1 FROM users)—— 后者永远为FALSE,整条不执行 - 批量插入多行时,不能并列多个
SELECT,得用VALUES ROW(), ROW()或UNION ALL构造结果集,再套一层WHERE NOT EXISTS判断(性能差,慎用)
PostgreSQL 的括号陷阱和主键判断差异
PostgreSQL 要求 INSERT ... SELECT 的 SELECT 部分必须用括号包裹,否则直接语法报错:syntax error at or near "WHERE"。而且它对主键重复的“防”逻辑更严格 —— NOT EXISTS 只管逻辑存在性,不管是否为主键冲突。
- 必须写成:
INSERT INTO users (id, name) (SELECT 100, 'alice' WHERE NOT EXISTS (SELECT 1 FROM users WHERE id = 100))—— 注意最外层(SELECT ...)的括号 - 如果表有复合主键(如
(user_id, role)),子查询必须同时匹配所有字段:WHERE user_id = 100 AND role = 'admin' - PostgreSQL 不支持
INSERT IGNORE或ON DUPLICATE KEY UPDATE,所以这是唯一标准 SQL 兼容路径
为什么不用 ON DUPLICATE KEY UPDATE?它不也防主键重复吗?
它确实能避免主键冲突报错,但它不是「子查询方案」,也不满足「防止插入」这个前提 —— 它本质是「插入失败就改走更新分支」,业务上等于允许了「覆盖行为」。
-
ON DUPLICATE KEY UPDATE必须依赖表上已有的主键或唯一索引约束,否则无效 - 只要主键值存在,哪怕你只想新增、严禁修改,它也会触发
UPDATE,可能误改时间戳、状态字段等 - 返回影响行数是 1(插入)或 2(先删后插)或 1(更新),无法区分「真新增」和「假新增」,日志/监控难追踪
- 子查询方案中
NOT EXISTS是纯读判断,不改变已有数据,语义干净;而ON DUPLICATE KEY UPDATE在高并发下可能因锁机制导致意外等待或死锁
真正要靠子查询防重复,核心就一条:子查询必须关联外层插入值,并走索引。否则 NOT EXISTS 会退化成全表扫描,批量插入时性能崩盘 —— 这点比语法错误更隐蔽,也更容易被忽略。










