upsert中jsonb_concat(excluded.data, data)会导致旧值被丢弃,因excluded.data为左操作数;正确做法是用jsonb_set精准更新路径或data || excluded.data浅合并,且须注意jsonb_concat自pg14引入、无深合并能力。

UPSERT时用jsonb_concat合并JSON字段会覆盖整个值
直接在ON CONFLICT ... DO UPDATE里写SET data = jsonb_concat(excluded.data, data),结果不是“把新字段补进旧JSON”,而是把excluded.data(即INSERT那一行的data)当左操作数——这意味着旧值被整体丢弃,只保留新值里有的键。这是jsonb_concat的语义决定的:jsonb_concat(a, b)返回的是a和b的并集,但a中同名键会被b覆盖,而excluded.data是你要插入的新数据,它天然“在左边”。
正确做法:用jsonb_set逐层更新或||运算符做浅合并
如果目标是“保留原JSON所有字段,仅用新字段覆盖/新增指定路径”,优先用jsonb_set。例如想更新data->'user'->'name'且不碰其他字段:
INSERT INTO users (id, data) VALUES (1, '{"user": {"name": "Alice", "age": 30}}')
ON CONFLICT (id) DO UPDATE
SET data = jsonb_set(
users.data,
'{user,name}',
excluded.data#>'{user,name}',
true
);
若需合并整个对象(比如data->'settings'下所有键),用||更安全:
-
excluded.data->'settings'必须是非空JSONB,否则||会返回NULL - 写成
users.data || jsonb_build_object('settings', excluded.data->'settings')可避免全量覆盖 - 注意
||对顶层键是浅合并:同名键被右侧覆盖,嵌套对象不会递归合并
常见错误:误把jsonb_concat当“深合并”工具
PostgreSQL没有内置深合并函数。有人试过jsonb_concat(data, excluded.data),发现excluded.data里没出现的键全丢了——因为jsonb_concat第一个参数是“基底”,第二个是“覆盖层”,而excluded.*代表的是INSERT语句里的值,不是原始表里的值。真正要保留原值,必须显式引用users.data(即目标表当前行)作为左操作数:
- 错:
jsonb_concat(excluded.data, users.data)→ 新值覆盖旧值 - 对:
jsonb_concat(users.data, excluded.data)→ 旧值为基底,新值覆盖同名键 - 但即使这样,仍是浅合并,嵌套对象整块替换,不是逐字段合并
性能与兼容性提醒
jsonb_concat从PostgreSQL 14才引入,低版本只能用||替代。另外,频繁在UPSERT中做JSONB运算会拖慢执行,尤其当data字段很大时——每次更新都要解析、重组整个JSONB树。如果只是改一两个字段,jsonb_set比||或jsonb_concat快得多,因为它只触碰指定路径。
真正难处理的是“动态路径更新+条件合并”,这时就得提前在应用层拼好完整JSONB,或者用PL/pgSQL写自定义函数——数据库层不擅长运行时决定合并策略。











