在PostgreSQL中实现"存在则更新、不存在则插入"的需求,最常见的方案就是INSERT ... ON CONFLICT DO UPDATE,也就是常说的upsert。但实际业务里往往还有一个隐含要求:并不是每次冲突都需要更新,比如只有当新数据的版本号更高、价格变化超过阈值或者状态发生特定改变时才执行更新,否则保留旧数据不动。这时就需要在DO UPDATE后面追加WHERE子句,实现条件更新。本文将详细讲解这一写法的语法细节、引用方式与常见陷阱。

一、ON CONFLICT DO UPDATE 的基础语法
ON CONFLICT子句必须紧跟在INSERT语句的VALUES之后,由两部分组成:冲突目标和冲突动作。冲突目标用来告诉PostgreSQL依据哪个唯一约束或唯一索引判断冲突,冲突动作则决定冲突发生时执行什么操作。
基础形式如下:
INSERT INTO products (id, name, price, stock)
VALUES (1, '键盘', 199.00, 50)
ON CONFLICT (id)
DO UPDATE SET name = EXCLUDED.name,
price = EXCLUDED.price,
stock = EXCLUDED.stock;
这里的EXCLUDED是一个特殊的行变量,代表本想插入但因为冲突而未能插入的那一行。SET子句中写price = EXCLUDED.price,含义是用新数据的值覆盖表中的旧值。如果不加任何条件,只要主键冲突就无条件覆盖,这在某些场景下是危险的,比如可能把数据库中较新的数据用较旧的快照覆盖掉,造成数据回退。
二、WHERE 条件更新的写法与原理解析
DO UPDATE支持可选的WHERE子句,其语义是:当冲突发生时,先计算WHERE表达式,结果为真才执行UPDATE,为假或为空则这条记录被直接跳过,效果类似于DO NOTHING,语句不会报错。这是实现幂等同步、防止旧数据覆盖新数据的关键手段。
WHERE条件中可以引用两行数据:一是表中已存在的旧行,直接用表名或其别名引用列,例如products.stock;二是准备插入的新行,通过EXCLUDED.列名引用。推荐在表名后写别名,例如INSERT INTO products AS p ... DO UPDATE ... WHERE p.stock < EXCLUDED.stock,这样语句更加清晰,避免旧列名与EXCLUDED产生混淆。
下面是一个完整示例:只有当新库存大于旧库存时才更新:
-- 先建表并插入一条旧数据
CREATE TABLE products (
id INT PRIMARY KEY,
name TEXT NOT NULL,
price NUMERIC(10,2),
stock INT
);
INSERT INTO products VALUES (1, '机械键盘', 249.00, 100);
-- 条件更新:仅当新库存更大时才覆盖
INSERT INTO products AS p (id, name, price, stock)
VALUES (1, '机械键盘', 249.00, 80)
ON CONFLICT (id)
DO UPDATE SET name = EXCLUDED.name,
price = EXCLUDED.price,
stock = EXCLUDED.stock
WHERE p.stock < EXCLUDED.stock;
由于表中id=1的旧记录stock为100,新值只有80,WHERE条件不满足,执行后表数据不会有任何变化。这一点在数据同步、消息重复消费等场景中尤其重要:重复消息往往携带相同或过期的数据,条件更新可以保证只有更新的数据才能写入成功。
WHERE子句还可以配合RETURNING判断语句实际是否更新了行。如果WHERE为假,RETURNING不会返回该行,据此可以在应用层感知跳过事件:
INSERT INTO products AS p (id, name, price, stock) VALUES (1, '机械键盘', 249.00, 120) ON CONFLICT (id) DO UPDATE SET stock = EXCLUDED.stock WHERE p.stock < EXCLUDED.stock RETURNING p.id, p.stock;
三、常见陷阱与实战注意事项
第一个陷阱是冲突目标必须与唯一约束匹配。如果表上有多个唯一索引,ON CONFLICT后面必须明确写出列名,或者使用ON CONFLICT ON CONSTRAINT 约束名的形式,否则PostgreSQL无法判断依据哪个约束,会直接报错提示没有匹配的唯一约束。另外,当冲突目标是部分唯一索引时,必须使用ON CONFLICT (列) WHERE 索引条件的形式,让冲突目标与索引定义完全一致,这是官方文档明确要求的写法。
第二个陷阱是容易混淆两处WHERE的位置。INSERT语句本身不允许带WHERE,如果把条件直接写在INSERT后面会直接语法错误;DO UPDATE后面的WHERE才是控制是否更新的条件;而DO NOTHING后面则不能跟任何WHERE。还有一个易错点是把业务分支逻辑放进SET子句,其实SET中可以使用CASE表达式决定某列更新成什么值,而WHERE决定整行更不更新,二者语义完全不同,要根据业务准确选择。
第三点是并发行为。ON CONFLICT DO UPDATE在并发场景下是原子的,同一行并发冲突时后到的语句会等待先前的插入或更新完成后重试,不会抛出唯一约束异常,这是它比"先查再插"方案更可靠的原因。建议在WHERE中只使用简单的列比较表达式,避免调用自定义函数,保证条件是确定性的,这样在分区表等高级特性下也能获得稳定的执行计划。
总结一下,ON CONFLICT DO UPDATE WHERE通过冲突目标、更新动作、执行条件三段式结构,把upsert从无条件覆盖升级为可控的数据合并操作。掌握EXCLUDED与旧行别名的引用方式,注意冲突目标与唯一约束的匹配规则,就能写出安全、幂等且高效的数据同步SQL。
PostgreSQLON CONFLICTupsert修改时间:2026-09-02 20:43:28