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 对照
| 对比项 | MySQL | PostgreSQL |
|---|---|---|
| 语法 | ON DUPLICATE KEY UPDATE | ON CONFLICT … DO UPDATE |
| 冲突判定 | 表上任意唯一键或主键 | 显式指定的冲突目标列 |
| 引用新值 | VALUES(col),8.0.20+ 可用 AS 别名 | EXCLUDED.col |
| 只跳过 | INSERT IGNORE | ON 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 的更新分支只影响发生冲突的那一行,未冲突的行按普通插入处理;一批数据里两种路径可能同时出现,导入后要核对总数与预期一致。