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

INSERT … SELECT 里用 NOT EXISTS 防重复最稳妥
直接在 INSERT 里嵌套子查询判断是否存在,比先 SELECT 再 INSERT 更安全——避免并发写入时的竞态条件。核心是用 NOT EXISTS 搭配相关子查询,而不是 NOT IN(后者对 NULL 敏感,容易漏判)。
常见错误是写成:INSERT INTO t (a,b) SELECT 'x','y' WHERE NOT IN (SELECT a FROM t)——这只要表里有任意 a IS NULL,整个条件就返回 FALSE,插入失败。
-
NOT EXISTS不受NULL影响,语义清晰:只要子查询一行都查不到,就执行插入 - 子查询必须关联外层值,例如:
NOT EXISTS (SELECT 1 FROM t WHERE t.a = 'x'),不能只写SELECT 1 FROM t - 注意字段类型匹配,比如
CHAR和VARCHAR比较时可能因尾部空格隐式转换出问题
MySQL 的 INSERT IGNORE 和 ON DUPLICATE KEY UPDATE 不算子查询方案
这两个是 MySQL 特有语法,底层依赖唯一索引,不是标准 SQL 子查询逻辑。它们能防重复,但行为和子查询不同:
-
INSERT IGNORE遇到唯一键冲突就静默跳过,不报错也不影响后续行;但无法区分“真插入”和“被忽略”,也没法做插入后的额外逻辑 -
ON DUPLICATE KEY UPDATE会触发更新,哪怕你只想插入新数据——如果业务上严格要求“只新增、不修改”,它就不适用 - 两者都不支持复杂条件判断(比如“存在同名用户但邮箱不同才允许插入”),而子查询可以自由组合
WHERE
PostgreSQL 用 INSERT ... SELECT + WHERE NOT EXISTS,记得加括号
PostgreSQL 要求 INSERT ... SELECT 的 SELECT 部分必须带括号包裹子查询,否则语法报错。这是和其他数据库明显不同的地方。
正确写法:
INSERT INTO users (name, email) SELECT 'alice', 'alice@example.com' WHERE NOT EXISTS ( SELECT 1 FROM users WHERE name = 'alice' );
- 漏掉外层
SELECT或括号,会报错syntax error at or near "WHERE" - 如果要插入多行,得用
VALUES构造行集再套子查询,不能直接写多个SELECT并列 - 在事务中执行时,
NOT EXISTS查询会加SHARE锁,防止其他事务同时插入相同值——这点比应用层判断更可靠
性能要注意:子查询字段必须走索引
防重复的判断字段(比如 email、username)没索引的话,每次 INSERT 都会全表扫描,高并发下直接拖垮性能。
- 确认执行计划里子查询用了
index lookup,而不是Seq Scan - 复合唯一约束(如
(tenant_id, email))要确保查询条件包含最左前缀,否则索引失效 - 如果判断条件涉及函数(如
LOWER(email)),需要建函数索引,否则子查询无法走索引
真正难的不是写出来,是让这个子查询在百万级数据上依然快——索引设计和执行计划验证,比语法本身花的时间多得多。

















