MySQL的INSERT ... ON DUPLICATE KEY UPDATE依赖唯一索引触发冲突检测,无索引则始终插入;PostgreSQL需显式指定ON CONFLICT目标列并慎用WHERE避免误更新。

MySQL 用 INSERT ... ON DUPLICATE KEY UPDATE 最直接
只要表有唯一索引(UNIQUE 或 PRIMARY KEY),就能触发冲突检测。MySQL 不会真正“先查再插”,而是靠唯一约束报错后自动走更新分支,性能好且原子。
常见错误是没建唯一索引就写 ON DUPLICATE KEY UPDATE,结果永远插入新行——它根本没东西可“duplicate”。
示例:
INSERT INTO users (id, name, email) VALUES (123, 'Alice', 'a@example.com') ON DUPLICATE KEY UPDATE name = VALUES(name), email = VALUES(email);
-
VALUES(col)引用的是本次INSERT语句中该列的值,不是当前行旧值 - 如果想在更新时做条件判断(比如只更新非空值),得用
IF或CASE,不能直接写name = COALESCE(VALUES(name), name)—— 这种写法在部分 MySQL 版本里不生效 - 主键或唯一键必须明确存在,哪怕只是给
email加个UNIQUE约束
PostgreSQL 用 INSERT ... ON CONFLICT DO UPDATE
语法更严谨,必须显式指定冲突目标(比如 ON CONFLICT (email)),不能只写 ON CONFLICT 模糊匹配。
容易踩的坑是漏写 WHERE 条件导致误更新:默认行为是只要冲突就无条件更新整行,但你可能只想在某些条件下才覆盖(例如仅当新数据时间戳更新)。
示例:
INSERT INTO users (id, name, email) VALUES (123, 'Alice', 'a@example.com') ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name, updated_at = NOW() WHERE users.updated_at
-
EXCLUDED是 PostgreSQL 关键字,代表本次想插入但被拦截的那行数据 - 冲突目标必须是已存在的唯一约束或唯一索引字段,不能是普通列
- 如果表没有合适唯一键,又不想改表结构,只能退回到
SELECT ... FOR UPDATE+ 手动判断逻辑,但并发下易出错
SQL Server 用 MERGE 语句,但要注意事务隔离和死锁
MERGE 功能完整,但实际用起来比 MySQL/PG 更重,且对锁行为敏感。一个典型问题是:在高并发场景下,MERGE 可能因内部扫描方式引发意料外的范围锁,导致死锁率上升。
它要求 WHEN MATCHED 和 WHEN NOT MATCHED 分支都显式写出,不能省略任一分支(除非加 AND 1=0 这类 trick)。
示例:
MERGE users AS target USING (SELECT 123 AS id, 'Alice' AS name, 'a@example.com' AS email) AS source ON target.email = source.email WHEN MATCHED THEN UPDATE SET name = source.name, updated_at = GETDATE() WHEN NOT MATCHED THEN INSERT (id, name, email) VALUES (source.id, source.name, source.email);
- 必须确保
ON条件中的列有索引,否则MERGE会走全表扫描,性能崩塌 - SQL Server 2012+ 支持
OUTPUT子句捕获影响行,但OUTPUT里不能引用inserted/deleted的计算列,容易报错 - 某些 ORM(如 Entity Framework)生成的
MERGE语句未加HOLDLOCK,在读已提交隔离级别下可能产生幻读
SQLite 用 INSERT OR REPLACE 或 INSERT OR IGNORE + 显式 UPDATE
INSERT OR REPLACE 看似简单,但它本质是“删+插”:先删掉冲突行,再插入新行。这会导致自增主键跳号、外键级联动作触发、触发器重复执行——不是真正的 UPSERT。
真要保序、保关联、保触发器行为一致,得拆成两步:INSERT OR IGNORE 后立刻用 UPDATE 覆盖,靠 changes() 函数判断是否需要更新。
示例(需在事务中):
BEGIN;
INSERT OR IGNORE INTO users (id, name, email) VALUES (123, 'Alice', 'a@example.com');
UPDATE users SET name = 'Alice', updated_at = datetime('now') WHERE email = 'a@example.com' AND changes() = 0;
COMMIT;
-
changes()返回上一条语句影响行数,INSERT OR IGNORE成功插入时返回 1,忽略时不返回影响,所以changes() = 0表示发生了忽略,该更新 - 必须包在事务里,否则并发时两个线程都看到
changes() = 0就会重复更新 - SQLite 不支持
UPSERT关键字直到 3.24.0(2018年),老版本只能用这套组合拳
不同数据库的“UPSERT”表面相似,底层机制差异很大:MySQL 依赖唯一索引报错重试,PostgreSQL 显式声明冲突路径,SQL Server 用合并语义但锁粒度难控,SQLite 则要么牺牲一致性要么手动补逻辑。选哪种,得先看你的表结构有没有唯一约束,再看并发强度和事务要求——没约束还硬上 ON CONFLICT,只会得到语法错误或静默失败。










