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

MySQL 中用 INSERT ... SELECT + NOT EXISTS 避免重复插入
直接用 INSERT ... SELECT 搭配 NOT EXISTS 是最可控的方式,尤其适合需要校验多字段组合、或 WHERE 条件较复杂的情况。它不依赖唯一索引,逻辑显式,调试也方便。
常见错误是把 NOT EXISTS 写成子查询却漏掉关联条件,导致全表被排除或误插。
- 必须确保子查询中用
WHERE关联外部待插入值,例如WHERE t.key = VALUES(key)(MySQL 8.0.19+)或显式写出变量 - 如果目标表有自增主键,
SELECT列表里不要写该列,否则可能触发主键冲突 - 注意
VALUES()函数仅在 MySQL 8.0.19+ 支持;低版本需用临时变量或拼接参数
INSERT INTO users (id, name, email) SELECT 123, 'Alice', 'alice@example.com' WHERE NOT EXISTS ( SELECT 1 FROM users WHERE email = 'alice@example.com' );
PostgreSQL 用 INSERT ... ON CONFLICT 实现“不存在则插入”
PostgreSQL 的 ON CONFLICT 是原子操作,性能好、语义清晰,但前提是目标字段(或组合)上已建 UNIQUE 约束或索引。没这个前提,它会直接报错 there is no unique or exclusion constraint。
- 不能只靠业务层判断“是否存在”再决定是否插入——并发下必然出现竞态
-
DO NOTHING和DO UPDATE选哪个,取决于你是否允许覆盖已有记录;多数“仅不存在时插入”场景用DO NOTHING - 若冲突键是组合(如
(tenant_id, code)),ON CONFLICT子句必须精确匹配该索引定义
INSERT INTO products (sku, name, price)
VALUES ('SKU-001', 'Widget', 29.99)
ON CONFLICT (sku) DO NOTHING;
SQL Server 用 MERGE 语句处理存在性判断
MERGE 是 SQL Server 原生支持的“查存改插”一体化语法,但写错容易引发意外更新或死锁。它不是简单的“if not exists then insert”,而是基于 WHEN NOT MATCHED THEN INSERT 分支执行,必须配对 USING 和 ON。
-
ON条件必须能明确区分“匹配”与“不匹配”,避免因 NULL 导致逻辑失效(NULL = NULL 不成立,需用IS NULL显式判断) - 即使只想要插入,也必须写完整
MERGE结构,包括WHEN MATCHED分支(哪怕只写THEN DO NOTHING) - 高并发下若未加合适提示(如
WITH (HOLDLOCK)),仍可能因索引查找间隙导致重复插入
MERGE users AS target USING (SELECT 'bob@example.com' AS email) AS source ON target.email = source.email WHEN NOT MATCHED THEN INSERT (email, created_at) VALUES (source.email, GETDATE());
跨数据库通用但低效的做法:先查后插(不推荐)
用 SELECT COUNT(*) 或 SELECT 1 判断再 INSERT,看似直观,但在并发环境下几乎必然失败。这不是“写法问题”,而是事务隔离机制决定的:两次语句之间存在时间窗口,其他连接可能已完成插入。
- 即使加了
BEGIN TRANSACTION,默认隔离级别(如 READ COMMITTED)也无法阻止幻读 - 想强行用这种方式,必须升到
SERIALIZABLE或加应用层分布式锁,代价远超收益 - ORM 框架(如 Django ORM 的
get_or_create()、SQLAlchemy 的merge())底层其实也是调用各数据库的原生 upsert 机制,而非先查后插
真正要注意的,是约束定义是否和你的“不存在”逻辑一致——比如你认为“邮箱唯一”,但表里没建 UNIQUE(email),那所有数据库的 upsert 语法都会失效或行为异常。











