ON CONFLICT DO UPDATE WHERE 是作用于更新阶段的条件判断,仅决定是否执行SET操作,不控制整个冲突逻辑;它基于EXCLUDED和目标行数据进行比较,需注意NULL处理、索引优化及锁行为影响。
ON CONFLICT DO UPDATE WHERE 是 WHERE 子句,不是触发器条件
很多人误以为 ON CONFLICT DO UPDATE WHERE 中的 WHERE 是用来控制“是否执行整个冲突更新逻辑”的开关,其实它只作用于 DO UPDATE 阶段:只有当该 WHERE 条件为 TRUE 时,才会真正执行更新;否则跳过更新(相当于变成 DO NOTHING),但**不会报错、也不会回滚**。
关键点在于:WHERE 是对目标行(即即将被更新的那行)的约束判断,不是对插入值的预检。它能访问两套别名:EXCLUDED(本次想插入的新数据)和表中已存在的行(默认用表名或显式别名,如 t)。
-
EXCLUDED.col指代本次 INSERT 尝试写入的值 -
t.col或mytable.col指代冲突发生时表中已存在的那行数据 -
WHERE中不能引用未在目标表中定义的列,也不能用子查询(除非是标量子查询且兼容)
常见错误:把业务逻辑条件错放成 ON CONFLICT 的 WHERE
比如想“仅当新值比旧值更新时才覆盖”,却写成:
INSERT INTO events (id, ts, data)
VALUES (1, '2024-01-01', '{"status":"done"}')
ON CONFLICT (id) DO UPDATE
SET ts = EXCLUDED.ts, data = EXCLUDED.data
WHERE EXCLUDED.ts > events.ts;
这看起来合理,但实际会出问题——如果 events.ts 为 NULL,EXCLUDED.ts > NULL 结果是 UNKNOWN,整行 WHERE 判定失败,更新被跳过,容易让人误以为“没生效”。更隐蔽的是,这种写法无法利用索引加速判断,纯靠逐行计算。
正确做法是确保比较安全:
- 用
COALESCE(EXCLUDED.ts, '-infinity'::timestamptz) > COALESCE(events.ts, '-infinity'::timestamptz) - 或提前在应用层过滤,避免无效冲突
- 若需强一致性校验(如版本号递增),建议在
WHERE中加入events.version 并确保version列有索引
UPDATE SET 中不能直接引用 WHERE 条件里的表达式结果
PostgreSQL 不支持像 MySQL 那样在 SET 中复用 WHERE 里的计算逻辑。例如下面写法是非法的:
-- ❌ 错误:cannot reference WHERE clause in SET ON CONFLICT (id) DO UPDATE SET updated_at = NOW(), status = 'merged' WHERE (EXCLUDED.priority > events.priority OR events.priority IS NULL) -- 然后还想在 SET 里用这个判断结果设置另一个字段?不行。
如果需要根据同一条件设置多个字段,只能重复写条件,或改用 CASE: