首页/数据/SQL 速查/Upsert:ON DUPLICATE KEY UPDATE 与 ON CONFLICT
变更与事务52 条

Upsert:ON DUPLICATE KEY UPDATE 与 ON CONFLICT

“存在则更新、不存在则插入”:MySQL 用 ON DUPLICATE KEY UPDATE,PostgreSQL 用 ON CONFLICT,是幂等写入的基础写法。

SQL 速查数据更新于 2026-10-13

Upsert 是什么

「存在则更新、不存在则插入」这类操作统称 upsert。MySQL 用 INSERT … ON DUPLICATE KEY UPDATE,PostgreSQL 用 INSERT … ON CONFLICT (列) DO UPDATE SET。判断「已存在」的依据不同:MySQL 依赖表上的主键或唯一索引,只要任一唯一键冲突就转到更新分支;PostgreSQL 必须显式写出冲突目标列,只在该唯一约束冲突时触发。用 upsert 能避免「先查再写」的竞态窗口,并发场景下比应用层判断可靠得多。

基本信息

MySQL 语法INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)
MySQL 8.0.20+推荐新写法:INSERT … AS new ON DUPLICATE KEY UPDATE col = new.col
PostgreSQL 语法INSERT … ON CONFLICT (唯一列) DO UPDATE SET col = EXCLUDED.col
冲突判定MySQL 看任意唯一键;PostgreSQL 看显式指定的冲突目标
只跳过不更新MySQL 用 INSERT IGNORE;PostgreSQL 用 ON CONFLICT DO NOTHING
副作用MySQL 冲突时也会消耗一次自增编号,产生编号空洞

典型使用场景

  • 配置项写入:有则更新、无则创建,一条语句完成
  • 计数器累加:ON DUPLICATE KEY UPDATE cnt = cnt + 1
  • 数据同步:把上游全量数据幂等地写入本地表
  • 任务幂等重试:重跑时不产生重复行
  • 汇总表回写:按维度键 upsert 统计结果
  • 并发写入唯一业务键:比先 SELECT 再 INSERT 更可靠

示例

示例
-- MySQL:冲突时更新
INSERT INTO user_points (user_id, points, updated_at)
VALUES (100, 10, NOW())
ON DUPLICATE KEY UPDATE
    points     = points + VALUES(points),   # 累加而不是覆盖
    updated_at = NOW();

-- PostgreSQL:显式指定冲突列
INSERT INTO user_points (user_id, points, updated_at)
VALUES (100, 10, now())
ON CONFLICT (user_id) DO UPDATE
SET points     = user_points.points + EXCLUDED.points,
    updated_at = now();

只跳过不更新

只跳过不更新
-- MySQL
INSERT IGNORE INTO user_points (user_id, points) VALUES (100, 10);

-- PostgreSQL
INSERT INTO user_points (user_id, points) VALUES (100, 10)
ON CONFLICT (user_id) DO NOTHING;

-- 批量 upsert:多行 VALUES 同样适用
INSERT INTO user_points (user_id, points) VALUES (100, 1), (101, 2)
ON CONFLICT (user_id) DO UPDATE SET points = user_points.points + EXCLUDED.points;

两种 upsert 对照

对比项MySQLPostgreSQL
语法ON DUPLICATE KEY UPDATEON CONFLICT … DO UPDATE
冲突判定表上任意唯一键或主键显式指定的冲突目标列
引用新值VALUES(col),8.0.20+ 可用 AS 别名EXCLUDED.col
只跳过INSERT IGNOREON CONFLICT DO NOTHING
自增列冲突也消耗自增编号序列同样会被推进
并发行为单语句原子,优于先查后写单语句原子,优于先查后写
MySQL 8.0.20 起 VALUES(col) 写法被标记为不推荐,改用 INSERT … AS new ON DUPLICATE KEY UPDATE col = new.col,可读性更好也避免歧义。
MySQL 的 upsert 以表上任意唯一键为冲突依据,一张表有多个唯一索引时,可能因为另一个不相关的唯一键冲突而走进更新分支,造成意料之外的覆盖。
MySQL 的 upsert 在冲突时会消耗一次自增编号,长期运行会产生编号空洞;业务上依赖连续编号时不能靠自增列。
PostgreSQL 的冲突目标可以是唯一索引、唯一约束或排除约束,写 ON CONFLICT ON CONSTRAINT 约束名 也能表达,视版本与需求选择。

实践要点

  • 先确认冲突依据的那一列确实有唯一约束,否则 upsert 永远不会触发更新分支
  • 更新分支里要写清是累加还是覆盖,两者业务含义完全不同
  • 大批量 upsert 控制每批行数,避免单条语句过大
  • upsert 的返回值也要关注:MySQL 的 affected rows 在插入与更新时数值不同
  • 同步场景配合事务使用,保证一批数据要么全进要么全不进
  • 把 upsert 当作幂等写入的基础,任务重试就不用额外去重

常见误用

  • 表上没有对应的唯一约束,upsert 退化为普通插入,重复数据照样写进去
  • MySQL 表上有多个唯一键,任意一个冲突都会触发更新分支,误覆盖数据
  • 更新分支写成覆盖而不是累加(或反之),统计值逐渐偏离真实
  • 批量 upsert 行数过多,单条语句过大,失败后整批回滚
  • upsert 与普通 INSERT 混在同一段逻辑里,冲突处理行为不一致
  • 依赖自增编号的连续性,而冲突会消耗自增值造成编号空洞
用 upsert 之前先核对冲突判定列上确实有唯一约束,这是最容易漏掉的一步:没有唯一约束时语句会退化成普通插入,重复数据照写不误。
upsert 的更新分支只影响发生冲突的那一行,未冲突的行按普通插入处理;一批数据里两种路径可能同时出现,导入后要核对总数与预期一致。

易错点

  • 表上没有对应唯一约束,upsert 退化成普通插入,重复数据照样写进去
  • MySQL 表上有多个唯一键时,不相关的键冲突也会触发更新分支,误覆盖数据
  • 把累加写成覆盖,或者相反,统计值越跑越偏且难以察觉
  • 用 VALUES(col) 时不留意已废弃写法,升级版本后行为有变
  • 在大批量导入里逐条 upsert,性能远不如一次多行 upsert

数据来源与参考

  • MySQL 8.0 Reference Manual · INSERT ... ON DUPLICATE KEY UPDATE
  • PostgreSQL Documentation · INSERT
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌