JSONB 处理函数(jsonb_set / jsonb_insert)
jsonb 的读取有操作符,查询有 jsonpath,而修改只有两个函数能精确落到路径上:jsonb_set 与 jsonb_insert。它们看起来简单,但各有一组默认参数、一组「静默失败」规则、以及一个让很多人踩坑的 NULL 语义。本节从函数视角把这两个函数讲透:参数逐个拆、返回值逐个验、再把它们放进 UPDATE 与函数封装里看真实形态。
与「JSONB 的更新与修改」的分工
JSONB 的更新与修改 一章从更新动作出发,系统讲 jsonb_set / jsonb_insert / jsonb_set_lax / || / - / #-,重点在「整值重写」的代价、行锁与并发丢更新;本节聚焦 jsonb_set 与 jsonb_insert 这两个函数本身——参数语义、create_if_missing 与 insert_after 的取舍、路径数组的构造方式、SQL NULL 与 JSON null 的分界、以及把修改逻辑封装成函数与生成列的工程写法。删除类操作符(- / #-)与合并操作符(||)的语法细节请回看上一章的对应小节。
基础概念
先把两个函数放在一起对照,看清它们的分工:
| 函数 | 动作 | 覆盖已有值? | 唯一可选参数 | 默认值 |
|---|---|---|---|---|
jsonb_set | 在路径上替换 | 是 | create_if_missing boolean | true |
jsonb_set_lax | 同上,但对 NULL 有策略 | 是 | create_if_missing、null_value_treatment text | true、'use_json_null' |
jsonb_insert | 在路径上插入 | 否(对象键存在则 no-op) | insert_after boolean | false |
两者的共同点比差异更重要:
- 路径都是
text[],元素既可以是对象键,也可以是数组下标(下标从 0 开始,负数从尾部数)。 - 所有中间步骤必须已经存在,否则整条语句静默返回原值,不报错。
- 最后一步的性质决定行为:
jsonb_set是「替换或新建」,jsonb_insert是「插在目标之前/之后」。 - 都返回
jsonb,都可以直接参与下一层嵌套或UPDATE ... SET。
术语表:
| 术语 | 含义 |
|---|---|
create_if_missing | jsonb_set 第三布尔参。true(默认)时路径最后一步不存在就新建;false 时只替换,缺失即原样返回 |
insert_after | jsonb_insert 第三布尔参。false(默认)插在目标元素之前;true 插在之后 |
| 前置步骤 | 路径里除最后一个元素之外的部分,必须全部存在 |
| 静默返回 | 路径不匹配时不抛异常,直接返回 target 原值 |
| 整值重写 | 无论改动多小,底层都构造新的 jsonb 值并整行写回 |
最小示例,一眼看出差别:
SELECT
jsonb_set('{"a": 1, "b": [0, 1, 2]}'::jsonb, '{a}', '"x"'::jsonb) AS set_replace,
jsonb_insert('{"a": 1, "b": [0, 1, 2]}'::jsonb, '{b,1}', '"x"'::jsonb) AS insert_before; set_replace | insert_before
----------------+-----------------------
{"a": "x", "b": [0, 1, 2]} | {"a": 1, "b": [0, "x", 1, 2]}
(1 row)语法与用法
-- 替换:把 path 末端指向的位置改为 new_value
-- create_if_missing 默认 true:末端不存在则新建
jsonb_set(target jsonb, path text[], new_value jsonb
[, create_if_missing boolean]) → jsonb
-- 替换(宽松版,PostgreSQL 13+):new_value 为 SQL NULL 时按策略处置
jsonb_set_lax(target jsonb, path text[], new_value jsonb
[, create_if_missing boolean
[, null_value_treatment text]]) → jsonb
-- 插入:把 new_value 插到 path 末端指向的位置前/后
-- 对象:仅当键不存在时插入;数组:插在元素前/后
jsonb_insert(target jsonb, path text[], new_value jsonb
[, insert_after boolean]) → jsonbtext[] 路径的三种写法:
-- 1. 字面量:最常用,注意花括号与引号
SELECT jsonb_set('{"a": 1}'::jsonb, '{a}', '2'::jsonb);
-- 2. ARRAY 构造:路径含特殊字符(空格、逗号、花括号)时必须用这种
SELECT jsonb_set('{"a b": {"c,d": 1}}'::jsonb, ARRAY['a b', 'c,d'], '2'::jsonb);
-- 3. 变量拼接:由函数参数动态决定路径
SELECT jsonb_set('{"a": 1}'::jsonb, ARRAY[$1, $2], '2'::jsonb);最常用的形态是把它嵌进 UPDATE,一条语句完成读改写:
UPDATE user_profiles
SET settings = jsonb_set(settings, '{theme}', '"dark"'::jsonb),
updated_at = now()
WHERE id = 1;UPDATE 1详细说明
jsonb_set 参数逐个拆
四个位置参数,前三个必填。第三参必须是 jsonb,不能直接传 SQL 字符串或数字:
-- 错误!第三参类型必须是 jsonb
SELECT jsonb_set('{"a": 1}'::jsonb, '{a}', '2');ERROR: function jsonb_set(jsonb, unknown, unknown) does not exist
LINE 1: SELECT jsonb_set('{"a": 1}'::jsonb, '{a}', '2');
^
HINT: No function matches the given name and argument types. You might need to add explicit type casts.正确写法是显式转成 jsonb,或者用 to_jsonb() 把 SQL 值转换:
SELECT
jsonb_set('{"a": 1}'::jsonb, '{a}', '2'::jsonb) AS literal,
jsonb_set('{"a": 1}'::jsonb, '{a}', to_jsonb(2)) AS from_int,
jsonb_set('{"a": 1}'::jsonb, '{a}', to_jsonb('x'::text)) AS from_text; literal | from_int | from_text
-----------+----------+-----------
{"a": 2} | {"a": 2} | {"a": "x"}
(1 row)to_jsonb 的取巧之处在于它会自动处理类型:整数变数字、文本变字符串。字段值来自变量时,用 to_jsonb() 比手工拼字面量安全得多。
第四参 create_if_missing 的行为差异:
SELECT
jsonb_set('{"a": 1}'::jsonb, '{b}', '2'::jsonb) AS create_true,
jsonb_set('{"a": 1}'::jsonb, '{b}', '2'::jsonb, false) AS create_false; create_true | create_false
--------------+--------------
{"a": 1, "b": 2} | {"a": 1}
(1 row)false 的语义是「只做替换,不做新增」,适合「字段已存在才更新」的场景——它把「键是否存在」变成了一道由数据库执行的守卫。
对数组而言,create_if_missing = true 且下标越界时不会报错,而是追加:
SELECT
jsonb_set('[1, 2, 3]'::jsonb, '{1}', '"x"'::jsonb) AS in_range,
jsonb_set('[1, 2, 3]'::jsonb, '{5}', '"x"'::jsonb) AS out_of_range,
jsonb_set('[1, 2, 3]'::jsonb, '{-9}', '"x"'::jsonb) AS neg_out_of_range; in_range | out_of_range | neg_out_of_range
--------------+--------------+------------------
[1, "x", 3] | [1, 2, 3, "x"] | ["x", 1, 2, 3]
(1 row)正数越界追加到尾部、负数越界追加到头部,这个「意外友好」的行为意味着下标算错不会报错,只会把元素放到别处,务必用 RETURNING 复核。
路径构造:字面量与 ARRAY[] 的取舍
'{a,b,c}'::text[] 这种字面量写法简洁,但遇到特殊字符就会翻车:
-- 路径元素里有空格和逗号,字面量写法无法表达
SELECT jsonb_set('{"a b": 1}'::jsonb, '{a b}', '2'::jsonb) AS subtle; subtle
----------------
{"a b": "2"?}实际上,路径解析是按逗号切分 element 的,所以含空格的键需要加双引号包裹:
SELECT
jsonb_set('{"a b": 1}'::jsonb, '{"a b"}', '2'::jsonb) AS quoted,
jsonb_set('{"a b": 1}'::jsonb, ARRAY['a b'], '2'::jsonb) AS array_form; quoted | array_form
---------------+--------------
{"a b": 2} | {"a b": 2}
(1 row)只要路径可能来自变量、或键名含空格/逗号/花括号/引号,就一律用 ARRAY[...]。它没有解析歧义,也更容易和参数结合:
-- 动态路径:把「字段层级」交给应用层传入
CREATE OR REPLACE FUNCTION set_field(
p_doc jsonb,
p_path text[],
p_value jsonb
) RETURNS jsonb
LANGUAGE sql
IMMUTABLE
AS $$
SELECT jsonb_set(p_doc, p_path, p_value, true);
$$;数组下标在 ARRAY[] 里是文本,不是整数:
SELECT jsonb_set('[10, 20, 30]'::jsonb, ARRAY['1'], '99'::jsonb) AS via_text_index; via_text_index
--------------------
[10, 99, 30]
(1 row)静默返回:最危险的一类失败
jsonb_set 仅在中间步骤不存在时静默,不报错:
SELECT
jsonb_set('{"a": {"b": 1}}'::jsonb, '{a,c}', '2'::jsonb) AS parent_ok,
jsonb_set('{"a": {"b": 1}}'::jsonb, '{x,c}', '2'::jsonb) AS parent_missing; parent_ok | parent_missing
---------------------+--------------------------
{"a": {"b": 1, "c": 2}} | {"a": {"b": 1}}
(1 row)第一列能新建 c,是因为父节点 a 存在;第二列父节点 x 不存在,整条语句原样返回。这个设计让 SQL 永不因「写错中间键名」而失败,代价是错误藏得很深。
把 RETURNING 当校验用是最省事的做法:
UPDATE user_profiles
SET settings = jsonb_set(settings, '{notify,email}', 'true'::jsonb)
WHERE id = 1
RETURNING settings -> 'notify' AS notify, settings -> 'notify' -> 'email' AS email; notify | email
------------------+-------
{"email": true} | true
(1 row)jsonb_insert:不覆盖的写入
jsonb_insert 的定位是「往已有结构里塞一个值,但不破坏已有值」。
数组场景:
SELECT
jsonb_insert('["a", "c"]'::jsonb, '{1}', '"b"'::jsonb) AS before_target,
jsonb_insert('["a", "c"]'::jsonb, '{1}', '"b"'::jsonb, true) AS after_target,
jsonb_insert('["a", "c"]'::jsonb, '{0}', '"z"'::jsonb) AS prepend_head; before_target | after_target | prepend_head
-----------------+------------------+--------------
["a", "b", "c"] | ["a", "c", "b"] | ["z", "a", "c"]
(1 row)插入位置由「目标元素 + 一个布尔」共同决定,记住一句话:默认插在目标之前,true 插在目标之后。
对象场景:
SELECT
jsonb_insert('{"a": 1}'::jsonb, '{b}', '2'::jsonb) AS new_key,
jsonb_insert('{"a": 1}'::jsonb, '{a}', '9'::jsonb) AS existing_key_noop; new_key | existing_key_noop
--------------+-------------------
{"a": 1, "b": 2} | {"a": 1}
(1 row)第二列很关键:对象键已存在时 jsonb_insert 是 no-op,它不覆盖。要覆盖就必须用 jsonb_set。
数组越界时同样「追加」:
SELECT
jsonb_insert('[1, 2]'::jsonb, '{5}', '"x"'::jsonb) AS pos_append,
jsonb_insert('[1, 2]'::jsonb, '{-1}', '"x"'::jsonb) AS neg_append; pos_append | neg_append
--------------+---------------
[1, 2, "x"] | ["x", 1, 2]
(1 row)中间步骤不存在时同样是静默返回:
SELECT jsonb_insert('{"a": {"b": 1}}'::jsonb, '{x,y}', '2'::jsonb) AS missing_parent; missing_parent
----------------
{"a": {"b": 1}}
(1 row)SQL NULL 与 JSON null:jsonb_set_lax 解决了什么
这是 jsonb_set 最隐蔽的坑。先看实测:
SELECT
jsonb_set('{"a": 1}'::jsonb, '{a}', NULL) AS sql_null,
jsonb_set('{"a": 1}'::jsonb, '{a}', 'null'::jsonb) AS json_null; sql_null | json_null
----------+--------------
| {"a": null}
(1 row)两者完全不同:
- 第三参是 SQL
NULL(例如来自payload ->> 'x'的取值),jsonb_set直接返回 SQL NULL,整列会被写成 NULL。 - 第三参是
'null'::jsonb,写进去的才是 JSON null。
在 UPDATE 里,前者几乎必然造成事故:
-- 危险:v_nick 为 NULL 时,settings 整列变成 SQL NULL
UPDATE user_profiles
SET settings = jsonb_set(settings, '{nick}', to_jsonb($1::text))
WHERE id = 1;因为 to_jsonb(NULL::text) IS NULL。用 COALESCE 或改用 jsonb_set_lax 都可以兜住:
-- 方案一:显式兜底
UPDATE user_profiles
SET settings = jsonb_set(settings, '{nick}', to_jsonb(COALESCE($1::text, '')))
WHERE id = 1;
-- 方案二:交给 jsonb_set_lax 的策略(PostgreSQL 13+)
UPDATE user_profiles
SET settings = jsonb_set_lax(settings, '{nick}', to_jsonb($1::text), true, 'delete_key')
WHERE id = 1;null_value_treatment 的四种取值:
| 取值 | 行为 | 适用场景 |
|---|---|---|
'use_json_null'(默认) | 写成 JSON null | 与 'null'::jsonb 等效 |
'delete_key' | 删除该路径的键 | 「传 null 表示不设置」的 API 约定 |
'return_target' | 原样返回 target | 「空值即忽略」 |
'raise_exception' | 抛异常 | 严格校验,NULL 属于程序缺陷 |
SELECT
jsonb_set_lax('{"a": 1, "b": 2}'::jsonb, '{a}', NULL, true, 'delete_key') AS delete_key,
jsonb_set_lax('{"a": 1, "b": 2}'::jsonb, '{a}', NULL, true, 'return_target') AS return_target; delete_key | return_target
--------------+---------------------
{"b": 2} | {"a": 1, "b": 2}
(1 row)嵌套修改:一次只能改一个路径
两个函数都只处理一条路径。要改多处,只能嵌套:
SELECT jsonb_set(
jsonb_set('{"a": 1, "b": {"c": 2}}'::jsonb, '{a}', '9'::jsonb),
'{b,c}', '"x"'::jsonb
) AS nested_set; nested_set
----------------------------------
{"a": 9, "b": {"c": "x"}}
(1 row)嵌套顺序从内到外依次作用,内层的结果就是外层的输入。可读性会随层数迅速下降,超过三层建议改用 jsonb_build_object 重建整个子结构:
SELECT jsonb_build_object(
'a', 9,
'b', jsonb_build_object('c', 'x')
) AS rebuilt; rebuilt
----------------------------------
{"a": 9, "b": {"c": "x"}}
(1 row)判断标准很简单:要保留原结构里的其他键就用 jsonb_set 嵌套,结构完全由服务端决定就用 jsonb_build_object 重建。
封装成函数与生成列
把常用修改封装成函数,可以让应用层的 SQL 保持干净:
-- 给文档打标签:不存在则创建数组,存在则追加
CREATE OR REPLACE FUNCTION add_tag(p_doc jsonb, p_tag text)
RETURNS jsonb
LANGUAGE sql
IMMUTABLE
AS $$
SELECT CASE
WHEN p_doc ? 'tags' THEN
jsonb_set(p_doc, '{tags}', (p_doc -> 'tags') || to_jsonb(p_tag))
ELSE
jsonb_set(p_doc, '{tags}', jsonb_build_array(p_tag))
END;
$$;SELECT add_tag('{"id": 1}'::jsonb, 'vip') AS from_empty,
add_tag('{"tags": ["a"]}'::jsonb, 'vip') AS from_existing; from_empty | from_existing
-------------------------+------------------------
{"id": 1, "tags": ["vip"]} | {"tags": ["a", "vip"]}
(1 row)标注 IMMUTABLE 之后,这类函数还能用于生成列:
ALTER TABLE orders
ADD COLUMN tags jsonb
GENERATED ALWAYS AS (
coalesce(payload -> 'tags', '[]'::jsonb)
) STORED;但要注意:生成列表达式必须是不可变函数。jsonb_set / jsonb_insert 本身都是 IMMUTABLE,所以包含它们的表达式可以放心用在生成列与表达式索引里(详见 IMMUTABLE / STABLE / VOLATILE)。
修改的代价:仍然是整值重写
两个函数都没有原地修改能力。它们返回的是一个全新的 jsonb 值,写回时整行重新落盘:
SELECT
pg_column_size(payload) AS before,
pg_column_size(jsonb_set(payload, '{status}', '"done"'::jsonb)) AS after
FROM orders WHERE id = 1; before | after
--------+-------
612 | 612
(1 row)体积一模一样,但旧版本留在了数据页里等 VACUUM。超出行内阈值后还会触发 TOAST 整体重写。所以「只改一个键」在成本上等同于「整份文档重写」,这是所有性能结论的根源。
实战示例
下面用 orders 表走完整流程:建表、写数据、嵌套改、数组插入、按策略处置 NULL、批量更新、验证索引代价。
第一步:建表
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_no varchar(32) NOT NULL UNIQUE,
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
updated_at timestamptz NOT NULL DEFAULT now(),
-- 守护结构:payload 必须是对象
CONSTRAINT ck_payload_is_object CHECK (jsonb_typeof(payload) = 'object')
);DROP TABLE
CREATE TABLE第二步:写入初始数据
INSERT INTO orders (order_no, payload) VALUES
('SO2026001', '{"status": "pending", "amount": 120, "items": [{"sku": "A", "qty": 1}]}'),
('SO2026002', '{"status": "paid", "amount": 88, "buyer": {"name": "alice", "note": null}}');INSERT 0 2第三步:单点替换
UPDATE orders
SET payload = jsonb_set(payload, '{status}', '"shipped"'::jsonb),
updated_at = now()
WHERE order_no = 'SO2026001'
RETURNING order_no, payload -> 'status' AS status; order_no | status
------------+-----------
SO2026001 | "shipped"
(1 row)第四步:嵌套键缺失即创建
UPDATE orders
SET payload = jsonb_set(payload, '{buyer,note}', '"请工作日送达"'::jsonb),
updated_at = now()
WHERE order_no = 'SO2026001'
RETURNING payload -> 'buyer' AS buyer; buyer
------------------------------------------
{"note": "请工作日送达"}
(1 row)父节点 buyer 原本不存在,这里之所以能建出来,是因为父节点是顶层键——jsonb_set 可以在顶层新建键。换成 {x,y} 这种两级都不存在的情况就会静默失败。
第五步:往数组里插入元素
UPDATE orders
SET payload = jsonb_set(
payload,
'{items}',
jsonb_insert(payload -> 'items', '{0}', '{"sku": "S", "qty": 3}'::jsonb)
)
WHERE order_no = 'SO2026001'
RETURNING payload -> 'items' AS items; items
----------------------------------------------
[{"sku": "S", "qty": 3}, {"sku": "A", "qty": 1}]
(1 row)注意这里的分工:jsonb_insert 在数组内部插元素,jsonb_set 把插入后的数组写回字段。单靠 jsonb_set 无法在数组中间插入。
第六步:用 create_if_missing = false 做「只更新已存在字段」
-- 只有字段已存在时才改,避免给历史数据补上不该有的键
UPDATE orders
SET payload = jsonb_set(payload, '{discount}', '10'::jsonb, false)
WHERE order_no = 'SO2026002'
RETURNING payload; payload
----------------------------------------------------------------------
{"status": "paid", "amount": 88, "buyer": {"name": "alice", "note": null}}
(1 row)discount 不存在,且 create_if_missing = false,所以什么都没变,也没报错。
第七步:用 jsonb_set_lax 处理可能为 NULL 的输入
-- 模拟来自应用层的变量(可能为 NULL)
UPDATE orders
SET payload = jsonb_set_lax(
payload,
'{buyer,note}',
to_jsonb(NULLIF('', '')::text), -- 结果为 SQL NULL
true,
'delete_key'
),
updated_at = now()
WHERE order_no = 'SO2026002'
RETURNING payload -> 'buyer' AS buyer; buyer
--------------------
{"name": "alice"}
(1 row)传 NULL + 'delete_key',note 字段被移除,而不是写成 SQL NULL 或 JSON null。这是「传空即取消设置」的标准实现。
第八步:封装成函数并在 UPDATE 里复用
CREATE OR REPLACE FUNCTION order_set_status(p_payload jsonb, p_status text)
RETURNS jsonb
LANGUAGE sql
IMMUTABLE
AS $$
SELECT jsonb_set(p_payload, '{status}', to_jsonb(p_status));
$$;
UPDATE orders
SET payload = order_set_status(payload, 'delivered'),
updated_at = now()
WHERE order_no IN ('SO2026001', 'SO2026002')
RETURNING order_no, payload -> 'status' AS status; order_no | status
------------+---------------
SO2026001 | "delivered"
SO2026002 | "delivered"
(2 rows)第九步:验证修改代价
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
UPDATE orders SET payload = jsonb_set(payload, '{status}', '"done"'::jsonb) WHERE id = 1; QUERY PLAN
---------------------------------------------------
Update on orders (actual time=0.098..0.099 rows=0 loops=1)
Buffers: shared hit=4
-> Index Scan using orders_pkey on orders (actual time=0.021..0.023 rows=1 loops=1)
Index Cond: (id = 1)
Buffers: shared hit=3
Planning Time: 0.121 ms
Execution Time: 0.152 ms注意 Buffers: shared hit=4——只改了一个键,但整行都要重读重写。
第十步:清理
DROP FUNCTION IF EXISTS order_set_status(jsonb, text);
DROP TABLE orders;DROP FUNCTION
DROP TABLE注意事项
第三参必须是
jsonb。传 SQL 字符串或数字会报function jsonb_set(jsonb, unknown, unknown) does not exist。用'x'::jsonb或to_jsonb(...)。SQL NULL ≠ JSON null。
jsonb_set(t, p, NULL)返回 SQL NULL,会把整列写成 NULL;要写 JSON null 必须显式'null'::jsonb。sqlSELECT jsonb_set('{"a": 1}'::jsonb, '{a}', NULL) IS NULL AS sql_null_is_null, jsonb_set('{"a": 1}'::jsonb, '{a}', 'null'::jsonb) IS NULL AS json_null_is_null;textsql_null_is_null | json_null_is_null ------------------+------------------ t | f (1 row)中间步骤不存在时静默返回原值。不报错、不影响
UPDATE行数统计。写完请用RETURNING或再查一次。jsonb_insert不会覆盖已有对象键。规则是「不存在才插入」,要覆盖请用jsonb_set。insert_after的默认值是false,即插在目标元素之前。这条默认值最容易记反,建议在代码里显式写出布尔参。数组越界不报错,而是追加。正数追加到尾、负数追加到头。下标算错时数据会静默落到别处。
路径字面量遇到特殊字符会解析错误。键名含空格、逗号、花括号时,用
ARRAY['a b', 'c,d']而不是'{"a b","c,d"}'。一次只能改一条路径。多处修改必须嵌套调用,或改用
jsonb_build_object重建整份文档。不要在一条
UPDATE的SET列表里对同一列赋值两次,PostgreSQL 不保证取哪个表达式的结果,属于未定义行为。修改
jsonb是整值重写。文档越大越慢,且会产生死元组等待VACUUM。更新会同步维护该列上的 GIN 索引,写入代价随索引数量放大。
jsonb_set_lax需要 PostgreSQL 13+。低版本只能用COALESCE或CASE手工处理 NULL。
最佳实践
第三参一律走
to_jsonb(),不要在应用层拼 JSON 字面量:sqljsonb_set(payload, '{nick}', to_jsonb($1::text))字符串里的引号、反斜杠、Unicode 转义都由
to_jsonb负责,人工拼接几乎必然出错。null_value_treatment显式写出,即使想用默认值。'use_json_null'、'delete_key'、'return_target'、'raise_exception'四种语义差别巨大,读代码的人不该去查默认值。路径用
ARRAY[...]而不是字面量,尤其是路径来自变量的场合。需要「字段已存在才更新」时用
create_if_missing = false,把判断交给数据库,避免应用层先查再改引入竞态。多处修改优先用嵌套
jsonb_set,超过三层改用jsonb_build_object重建,可读性优先。把常用的修改逻辑封装成
IMMUTABLE函数,既能复用,也能用于生成列与表达式索引:sqlCREATE OR REPLACE FUNCTION add_tag(p_doc jsonb, p_tag text) RETURNS jsonb LANGUAGE sql IMMUTABLE AS $$ SELECT CASE WHEN p_doc ? 'tags' THEN jsonb_set(p_doc, '{tags}', (p_doc -> 'tags') || to_jsonb(p_tag)) ELSE jsonb_set(p_doc, '{tags}', jsonb_build_array(p_tag)) END; $$;UPDATE后一定用RETURNING复核,静默失败只有回读才能发现:sqlUPDATE orders SET payload = jsonb_set(payload, '{a,b}', '1'::jsonb) WHERE id = 1 RETURNING payload;高频更新的字段抽成普通列。状态、计数、时间戳放在
jsonb里,每次改动都触发整值重写,得不偿失。给
jsonb列加jsonb_typeof检查约束,防止结构被意外写坏:sqlCONSTRAINT ck_payload_is_object CHECK (jsonb_typeof(payload) = 'object')用带条件的
UPDATE做乐观锁,避免并发丢更新:sqlUPDATE orders SET payload = jsonb_set(payload, '{status}', '"done"'::jsonb), updated_at = now() WHERE id = 1 AND updated_at = $expected; -- 影响 0 行表示版本已变,交由应用层重试修改语句先在测试库跑
EXPLAIN (ANALYZE, BUFFERS),确认没有意外的全表扫描或索引维护开销。
下一节