先查再插存在竞态条件:两个并发请求同时查到“不存在”,接着都执行insert,导致唯一键冲突错误;而on duplicate key update或on conflict是原子操作,可避免该问题。

直接用 INSERT ... ON DUPLICATE KEY UPDATE(MySQL)或 INSERT ... ON CONFLICT(PostgreSQL)最稳妥,比先 SELECT 再 INSERT 更安全、更高效。
为什么不能先查再插?
这是新手最常写的逻辑,但存在竞态条件(race condition):两个并发请求同时查到“不存在”,接着都执行 INSERT,其中一个必然报唯一键冲突错误(如 MySQL 的 1062 Duplicate entry 或 PostgreSQL 的 23505)。这不是代码写得不够严谨的问题,而是数据库层面的固有风险。
常见错误现象包括:
- 应用日志频繁出现
ERROR 1062 (23000): Duplicate entry 'xxx' for key 'PRIMARY' - API 偶发 500,但前端重试后又成功——说明是并发导致的瞬时冲突
MySQL:用 ON DUPLICATE KEY UPDATE 实现“存在则忽略”
前提是表中已有唯一索引(UNIQUE 或 PRIMARY KEY),否则该语法不生效。
实操建议:
- 如果只想插入、不更新,把
UPDATE子句设为无副作用操作,例如UPDATE id = id(MySQL 允许,且不触发实际变更) - 若字段允许
NULL,注意ON DUPLICATE KEY UPDATE仍会执行,哪怕新值也是NULL - 不推荐用
INSERT IGNORE,它会静默吞掉所有错误(包括非唯一键冲突的其他错误,比如数据类型超长)
示例(假设 email 有唯一索引):
INSERT INTO users (email, name, created_at)
VALUES ('alice@example.com', 'Alice', NOW())
ON DUPLICATE KEY UPDATE id = id;
PostgreSQL:用 ON CONFLICT DO NOTHING 或 DO UPDATE
语法更明确,也更安全。必须指定冲突目标(通常是唯一约束名或列名),否则报错 ON CONFLICT requires a conflict specification。
使用场景与要点:
- 查约束名可用:
SELECT conname FROM pg_constraint WHERE conrelid = 'users'::regclass AND contype = 'u'; - 若只关心“不重复插入”,用
ON CONFLICT (email) DO NOTHING即可 - 想更新部分字段(如
updated_at),写成ON CONFLICT (email) DO UPDATE SET updated_at = EXCLUDED.updated_at,其中EXCLUDED指本次被拒绝的那行数据 - 注意:PostgreSQL 不支持对主键以外的表达式索引做
ON CONFLICT,除非显式命名约束
示例:
INSERT INTO users (email, name, created_at)
VALUES ('bob@example.com', 'Bob', NOW())
ON CONFLICT (email) DO NOTHING;
SQLite 和 SQL Server 怎么办?
SQLite 支持 INSERT OR IGNORE 和 INSERT OR REPLACE,但后者会删除旧行再插入(可能触发级联、丢失自增 ID 关联),慎用 REPLACE;优先选 OR IGNORE。
SQL Server 没有原生等价语法,必须用 MERGE 语句——它写法冗长、易出错(比如漏写 WHEN NOT MATCHED THEN INSERT 的括号),且在高并发下需额外加 HOLDLOCK 提示才能避免竞态。简单场景下,不如直接捕获错误码 2627 或 2601 后忽略。
真正容易被忽略的是:所有这些方案都依赖**正确的唯一约束定义**。没建索引,或者索引漏了字段,ON CONFLICT 就完全失效——它不会报错,只是退化成普通插入,然后在运行时崩给你看。











