必须用数据库原生UPSERT语义解决主键冲突,MySQL用INSERT...ON DUPLICATE KEY UPDATE(依赖主键或唯一索引),PostgreSQL用INSERT...ON CONFLICT(须显式指定冲突目标),禁用REPLACE INTO等非原子操作,避免并发丢数据或语义失真。

主键冲突时不能靠先查再插来解决,必须用数据库原生 UPSERT 语义,否则高并发下必丢数据或报错。
MySQL 必须用 INSERT ... ON DUPLICATE KEY UPDATE,且表要有唯一约束
这个语法只在触发主键(PRIMARY KEY)或任意 UNIQUE 索引冲突时才转为更新,没建索引就完全不生效。常见错误是建了普通索引却误以为能触发。
- 执行前务必确认:用
SHOW CREATE TABLE users;查看是否有UNIQUE KEY或PRIMARY KEY -
VALUES(col)不是函数调用,它直接取本次INSERT中该字段的原始值,比如VALUES(email)就等于你写在VALUES()里的那个邮箱字符串 - 如果表有多个唯一索引(比如
id主键 +email唯一),任一命中都会触发更新,但无法控制“由哪个索引触发” - 自增 ID 会跳号:即使最终执行的是更新,
AUTO_INCREMENT计数器仍会加一
PostgreSQL 要显式指定冲突目标,用 ON CONFLICT (col) 或 ON CONFLICT ON CONSTRAINT
PostgreSQL 不接受模糊匹配,漏写冲突目标会直接报错:ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification。
- 查主键名:用
\d table_name,主键约束名通常是table_name_pkey - 按字段冲突:写
ON CONFLICT (id);按复合唯一索引冲突:写ON CONFLICT (tenant_id, external_id) - 更新时用
EXCLUDED.col引用本次想插入的值,不是用VALUES()—— 后者在 PostgreSQL 里不合法 -
DO UPDATE SET后可加WHERE,但条件不成立时整行会被静默跳过(既没插也没更),容易误判成功
别把 REPLACE INTO 当 UPSERT,它本质是删+插
REPLACE INTO 在 MySQL 和 SQLite 中都存在,但它不是原子更新,而是先 DELETE 再 INSERT,会破坏外键引用、触发 BEFORE/AFTER DELETE 触发器,还可能让自增 ID 猛增。
- SQLite 的
INSERT OR REPLACE同样走删插路径,若表有ON DELETE CASCADE外键,关联行会被连带删除 - MySQL 的
REPLACE INTO影响行数可能是 2(新插)或 3(删+插),而ON DUPLICATE KEY UPDATE是 1(插)或 2(更) - 如果业务依赖触发器做审计或同步,
REPLACE会导致 DELETE 日志和 INSERT 日志各记一次,语义失真
应用层写 SELECT + INSERT/UPDATE 是并发雷区
哪怕加了事务,SELECT 和后续 INSERT 之间存在时间窗口,两个请求同时判断“不存在”,然后都去 INSERT,必然触发主键冲突或丢失更新。
- 唯一可靠的方案是把判断和写入压进一条原生命令,交给数据库引擎原子执行
- ORM 如 MyBatis 或 SQLAlchemy 需确认是否生成了原生 UPSERT 语句,而不是拼出两段 SQL
- 批量写入时,
ON DUPLICATE KEY UPDATE支持多行VALUES,但所有行共用一套更新逻辑,无法按每行不同条件分支处理
真正难处理的从来不是语法本身,而是搞清「冲突到底由哪个索引定义」以及「更新时要不要保留原字段默认值或 NULL」——这两点一旦错,数据就 quietly 错了。











