JSONB 构建函数(jsonb_build_object / jsonb_agg)
把关系数据"组装成 JSON"是后端最频繁的操作之一:一个订单要带出用户信息与订单明细,一个列表接口要把多行折叠成 JSON 数组。PostgreSQL 把这类需求压缩成了几个函数——jsonb_build_object 拼装单个对象、jsonb_build_array 拼装数组、jsonb_agg 把多行折叠成数组、jsonb_object_agg 把多行折叠成对象。本节从函数视角拆解它们的参数规则、类型转换与嵌套手法。
与「JSONB 聚合与转换」的分工
JSONB 聚合与转换 一章从数据流向出发,系统梳理"行 → JSON"与"JSON → 行"两方向的全部函数;本节聚焦构建方向(值 → JSON、行 → JSON),重点讲 jsonb_build_object 的可变参数与键规则、jsonb_agg 的排序与 NULL 语义、以及用 LATERAL + 嵌套聚合拼装分层 JSON 的工程手法。两节互补,本节不重复展开 jsonb_to_record / jsonb_populate_record 等反向函数。
前置知识
阅读本节前,建议先了解:ARRAY 构造函数与操作
基础概念
构建 JSON 的函数只有两类源头:从当前行的若干值拼,和从多行折叠。记住这条分界,就知道该选哪个函数。
| 概念 | 术语 | 说明 |
|---|---|---|
| 对象构造 | Object Builder | jsonb_build_object,键值对交替传入 |
| 数组构造 | Array Builder | jsonb_build_array,参数列表直接成为数组 |
| 行折叠 | Row Aggregation | jsonb_agg,多行聚合成一个 JSON 数组 |
| 对象聚合 | Object Aggregation | jsonb_object_agg,多行的 key/value 聚合成对象 |
| 可变参数 | VARIADIC "any" | 构造函数的参数类型不限、个数不限 |
| 类型转换 | to_jsonb | 把任意 SQL 值转成 jsonb |
| 记录转 JSON | row_to_json | 把一个复合类型/整行转成 json(非 jsonb) |
| 嵌套聚合 | Nested Aggregation | 在子查询里先聚合,再拼进外层对象 |
| 去空字段 | Null Stripping | jsonb_strip_nulls 递归删除 null 值字段 |
先看一张"来源 → 手段"对照表,这是选型的核心:
| 数据来源 | 想输出的形态 | 该用的函数 |
|---|---|---|
| 一行的几个列 | 对象 | jsonb_build_object('k', v, ...) |
| 一行的几个值 | 数组 | jsonb_build_array(v1, v2, ...) |
| 一行的整行 | 对象 | to_jsonb(t) 或 row_to_json(t) |
| 多行的某一列 | 数组 | jsonb_agg(x) |
| 多行的 key/value 两列 | 对象 | jsonb_object_agg(k, v) |
| 多行 + 每行是对象 | 数组的对象 | jsonb_agg(jsonb_build_object(...)) |
最小示例:
SELECT
jsonb_build_object('id', 1, 'name', '甲') AS obj,
jsonb_build_array(1, 'a', true, NULL) AS arr,
jsonb_agg(x) AS agg_arr
FROM (VALUES (1), (2), (3)) AS t(x); obj | arr | agg_arr
-------------------------------+--------------------------+-------------
{"id": 1, "name": "甲"} | [1, "a", true, null] | [1, 2, 3]
(1 row)注意 jsonb_build_array(1, 'a', true, NULL) 里的 NULL 变成了 JSON 的 null,而不是被丢弃——这是构建函数与聚合函数在 NULL 处理上的第一个分水岭。
语法与用法
-- 值 → JSON
jsonb_build_object(VARIADIC "any") → jsonb -- 键值对交替
jsonb_build_array(VARIADIC "any") → jsonb
to_jsonb(anyelement) → jsonb
row_to_json(record [, boolean]) → json -- 注意返回 json
jsonb_strip_nulls(jsonb) → jsonb -- 递归删 null 字段
-- 行 → JSON(聚合)
jsonb_agg(expression [ORDER BY ...]) → jsonb
jsonb_agg_strict(expression) → jsonb -- PG16+,跳过 NULL
jsonb_object_agg(key, value [ORDER BY ...]) → jsonb
jsonb_object_agg_strict(key, value) → jsonb -- PG16+
jsonb_object_agg_unique(key, value) → jsonb -- PG16+,重复键报错jsonb_build_object 的参数规则
它是 VARIADIC "any",因此参数个数不限、类型不限,但有两条硬规则:
- 参数个数必须是偶数,否则报错;
- 键必须是能转成
text的非 NULL 值,键为NULL直接报错。
-- 错误复现一:奇数个参数
SELECT jsonb_build_object('a');ERROR: argument list must have even number of elements
HINT: The arguments of jsonb_build_object() must consist of alternating keys and values.-- 错误复现二:键为 NULL
SELECT jsonb_build_object(NULL, 1);ERROR: argument 1: key must not be null键可以是任何类型,会被隐式转成 text;数字键会自动变成字符串键:
SELECT
jsonb_build_object('name', '甲', 'age', 30) AS str_keys,
jsonb_build_object(1, 'one', 2, 'two') AS num_keys; str_keys | num_keys
------------------------------------+---------------------
{"age": 30, "name": "甲"} | {"1": "one", "2": "two"}
(1 row)jsonb_build_array 与 to_jsonb 的区别
两者都能产出 JSON,但适用对象不同:
| 维度 | jsonb_build_array(v1, v2) | to_jsonb(v) |
|---|---|---|
| 输入 | 若干独立表达式 | 一个值(可为整行) |
| 输出 | 数组 | 该值的 JSON 表示 |
| 整行支持 | 需逐个列出 | to_jsonb(t) 直接展开为对象 |
| 返回类型 | jsonb | jsonb |
-- 整行转对象:to_jsonb 最省事
SELECT to_jsonb(t) AS row_as_obj
FROM (SELECT 1 AS id, '甲' AS name, ARRAY['a','b'] AS tags) AS t; row_as_obj
--------------------------------------------------
{"id": 1, "name": "甲", "tags": ["a", "b"]}
(1 row)to_jsonb 会把一行里的每一列变成一个键,列名就是键名,嵌套的数组也会被正确转成 JSON 数组——这是拼装接口响应最快的一条路。
-- 对比:手工 build_object 也能做,但要逐列书写
SELECT jsonb_build_object('id', t.id, 'name', t.name, 'tags', to_jsonb(t.tags)) AS manual
FROM (SELECT 1 AS id, '甲' AS name, ARRAY['a','b'] AS tags) AS t;row_to_json 与 to_jsonb 的差别只在返回类型(json vs jsonb),通常应优先选 jsonb,因为它是规范化的、可比较、可索引。
jsonb_agg 的排序与 NULL 语义
jsonb_agg 是聚合函数,接收多行。顺序不保证,必须显式 ORDER BY:
-- 不保证顺序
SELECT jsonb_agg(name) FROM products;
-- 推荐:显式排序
SELECT jsonb_agg(name ORDER BY id) FROM products;NULL 的处理是 jsonb_agg 最需要留意的地方:
| 函数 | 输入为 NULL 时 | 版本 |
|---|---|---|
jsonb_agg(x) | 输出 JSON null(保留位置) | 9.4+ |
jsonb_agg_strict(x) | 跳过该行 | 16+ |
jsonb_agg(x) FILTER (WHERE x IS NOT NULL) | 跳过该行 | 9.4+ |
SELECT
jsonb_agg(x) AS keep_null,
jsonb_agg(x) FILTER (WHERE x IS NOT NULL) AS filter_null,
jsonb_strip_nulls(jsonb_agg(x)) AS stripped
FROM (VALUES ('a'), (NULL), ('c')) AS t(x); keep_null | filter_null | stripped
---------------------+-------------+---------------
["a", null, "c"] | ["a", "c"] | ["a", null, "c"]
(1 row)关键坑点:jsonb_strip_nulls 只删对象里的 null 字段,不删数组里的 null 元素。所以上面第三个结果是 ["a", null, "c"],与 keep_null 完全一致。要去掉数组里的 null,只能用 FILTER(PG16 之前)或 jsonb_agg_strict(PG16+)。
-- PG16+ 的更简洁写法
SELECT jsonb_agg_strict(x) AS strict_agg
FROM (VALUES ('a'), (NULL), ('c')) AS t(x); strict_agg
------------
["a", "c"]
(1 row)jsonb_object_agg 与重复键
jsonb_object_agg(k, v) 把两列折成一个对象。它的两条规则与 jsonb_build_object 一致:键不能为 NULL;但重复键不报错,后者覆盖前者:
SELECT jsonb_object_agg(k, v) AS obj
FROM (VALUES ('a', 1), ('b', 2), ('a', 3)) AS t(k, v); obj
------------------
{"a": 3, "b": 2}
(1 row)上面的输出能看到 'a' 出现了两次,最终只保留最后一个值 3。这是静默的数据丢失,聚合前若不确定键唯一,应先确认或在 PG16+ 用 _unique 变体让重复键直接报错:
-- PG16+:重复键直接抛错,把静默问题变成显式问题
SELECT jsonb_object_agg_unique(k, v)
FROM (VALUES ('a', 1), ('a', 3)) AS t(k, v);ERROR: duplicate JSON object key value详细说明
三种"整行转 JSON"的写法对比
把一行变成 JSON 对象,有 to_jsonb、row_to_json、jsonb_build_object 三条路,选择依据是列是否需要筛选/改名/加工:
| 写法 | 返回 | 列筛选 | 列改名 | 计算列 | 适用场景 |
|---|---|---|---|---|---|
to_jsonb(t) | jsonb | 否(全列) | 否 | 否 | 快速导出整行 |
to_jsonb(t) + 子查询 | jsonb | 是 | 是 | 是 | 推荐的通用做法 |
row_to_json(t) | json | 否 | 否 | 否 | 需要 json 类型时 |
jsonb_build_object(...) | jsonb | 是 | 是 | 是 | 列少、需精确控制 |
推荐范式:先用子查询把列选好、改名、算好,再 to_jsonb 一行包住。既能有 to_jsonb 的简洁,又能任意控制输出内容:
SELECT to_jsonb(u) AS user_json
FROM (
SELECT id, name, email, created_at::date AS reg_date
FROM users
WHERE id = 1
) AS u; user_json
---------------------------------------------------------------------------------
{"id": 1, "name": "甲", "email": "a@b.com", "reg_date": "2026-01-01"}
(1 row)这比手写 jsonb_build_object('id', id, 'name', name, ...) 更不容易漏改字段。
拼装数组:jsonb_build_array 与 jsonb_agg 的边界
同一个"数组"需求,两个函数的适用面完全不同:
| 维度 | jsonb_build_array(a, b, c) | jsonb_agg(x) |
|---|---|---|
| 输入来源 | 当前行的若干表达式 | 多行的一列 |
| 元素个数 | 写死(或 VARIADIC 传入数组) | 由行数决定 |
| 能否排序 | 按书写顺序 | 需 ORDER BY |
| NULL 元素 | 保留 | 保留(_strict 跳过) |
| 典型用途 | 固定的元组,如 [lat, lng] | 变长列表,如"该用户的所有订单号" |
常见错误:想聚合多行却用了 jsonb_build_array,结果只拿到当前一行;或想拼固定元组却用了 jsonb_agg,被迫 GROUP BY。
-- 固定元组:build_array
SELECT jsonb_build_array(lat, lng) AS coord FROM locations;
-- 变长列表:agg
SELECT jsonb_agg(order_no ORDER BY created_at) AS orders
FROM orders WHERE user_id = 1;用 LATERAL + 嵌套聚合拼装分层 JSON
这是本节最实用的工程手法:主表一行对应从表多行时,用 LATERAL 子查询在每行内部做一次聚合,把从表折成数组塞进主对象。
SELECT jsonb_build_object(
'id', u.id,
'name', u.name,
'orders', o.orders
) AS user_with_orders
FROM users u
LEFT JOIN LATERAL (
SELECT jsonb_agg(
jsonb_build_object(
'order_no', od.order_no,
'amount', od.amount,
'items', it.items
) ORDER BY od.created_at
) AS orders
FROM orders od
LEFT JOIN LATERAL (
SELECT jsonb_agg(jsonb_build_object('sku', oi.sku, 'qty', oi.qty) ORDER BY oi.sku) AS items
FROM order_items oi
WHERE oi.order_id = od.id
) AS it ON true
WHERE od.user_id = u.id
) AS o ON true;得到的就是典型的三层嵌套结构:
{"id": 1, "name": "甲", "orders": [
{"order_no": "SO-001", "amount": 299.00,
"items": [{"sku": "A", "qty": 2}, {"sku": "B", "qty": 1}]}
]}为什么必须用 LATERAL:内层子查询需要引用外层当前行的 u.id / od.id,普通 JOIN 子查询无法引用同级外层的列。LATERAL 允许右侧子查询引用左侧的列,实现"每行按需展开"。
为什么用 LEFT JOIN LATERAL ... ON true 而不是逗号连接:LEFT JOIN LATERAL 保证即使从表没有匹配行,主行也会保留,此时 o.orders 为 NULL。若要"没有订单就输出空数组",需在 jsonb_agg 外套一层 coalesce:
'orders', coalesce(o.orders, '[]'::jsonb)嵌套聚合的两种写法:LATERAL 与"先聚合再连接"
LATERAL 不是唯一解。也可以先在各从表上聚合,再 JOIN 主表。两种写法的差别主要在可读性与执行计划:
| 写法 | 可读性 | 执行计划 | 超多行时 |
|---|---|---|---|
LEFT JOIN LATERAL | 嵌套层次清晰 | 每行执行一次聚合 | 主表巨大时可能较慢 |
| 子查询先聚合再 JOIN | 平铺,较冗长 | 各从表各扫一次 | 通常更稳 |
-- 写法二:先聚合,再连接
SELECT jsonb_build_object('id', u.id, 'name', u.name, 'orders', o.orders)
FROM users u
LEFT JOIN (
SELECT user_id, jsonb_agg(order_no ORDER BY created_at) AS orders
FROM orders
GROUP BY user_id
) AS o ON o.user_id = u.id;两种写法结果等价,写法二在从表数据量大时通常更容易被优化成一次扫描 + 哈希连接,写法一在层数多时更易读。实际项目里建议先写 LATERAL(可读),再用 EXPLAIN (ANALYZE, BUFFERS) 验证,必要时改写。
jsonb_strip_nulls 与"空对象"的区别
jsonb_strip_nulls 递归删除对象中值为 null 的字段,但对数组元素无效;更要注意它会把嵌套对象里所有 null 字段一并删掉:
SELECT
jsonb_strip_nulls('{"a": 1, "b": null}'::jsonb) AS simple, -- {"a": 1}
jsonb_strip_nulls('{"a": {"x": null, "y": 2}}'::jsonb) AS nested, -- {"a": {"y": 2}}
jsonb_strip_nulls('{"a": [1, null, 2]}'::jsonb) AS in_array; -- {"a": [1, null, 2]} simple | nested | in_array
----------------+-----------------------+--------------------
{"a": 1} | {"a": {"y": 2}} | {"a": [1, null, 2]}
(1 row)如果业务上"值为 null 就等于该字段不存在",用它是合理的;但如果 null 本身有语义(比如"显式置空"),则不应删除。
构建函数的性能特征
构建 JSON 是 CPU 密集型操作,代价主要来自三处:
| 环节 | 成本来源 | 规避手段 |
|---|---|---|
| 类型转换 | 每个值都要转成 jsonb 内部表示 | 减少不必要的列 |
| 字符串键 | 每次都要建 key 的文本 | 键多时考虑 to_jsonb(子查询) |
| 排序 | jsonb_agg ... ORDER BY 需额外排序 | 若顺序无所谓则省略 ORDER BY |
-- 只选需要的列,而不是 to_jsonb(整行)
SELECT to_jsonb(t) FROM (SELECT id, name FROM users) AS t;
-- 不推荐:把大字段(如 TEXT 正文)也带进 JSON
SELECT to_jsonb(u) FROM users u;与 jsonb_agg 配合的类型转换
jsonb_agg 接管任意类型,numeric 会以数字形式输出,date/timestamp 会以 ISO 字符串输出,text 带引号——这符合直觉,但要留意 numeric 的输出格式:
SELECT jsonb_agg(v ORDER BY v) AS nu
FROM (VALUES (1.50::numeric), (2.00::numeric)) AS t(v); nu
---------------
[1.50, 2.00]
(1 row)numeric 会保留声明的小数位(因为 numeric 自带 scale),而 float8 会输出最短表示。做金额字段时,用 numeric 拼 JSON 能保持精度一致。
实战示例
场景:为电商后台构造订单详情接口的 JSON,包含用户、订单、订单明细三层。
第 1 步:建表
DROP TABLE IF EXISTS order_items;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
order_no TEXT NOT NULL UNIQUE,
amount NUMERIC(10,2) NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE order_items (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders(id),
sku TEXT NOT NULL,
qty INTEGER NOT NULL CHECK (qty > 0),
price NUMERIC(10,2) NOT NULL
);第 2 步:插入数据
INSERT INTO users (name, email) VALUES
('甲', 'a@example.com'),
('乙', 'b@example.com'),
('丙', 'c@example.com'); -- 无订单,用于验证空数组
INSERT INTO orders (user_id, order_no, amount, status, created_at) VALUES
(1, 'SO-001', 299.00, 'paid', '2026-01-01 10:00:00+08'),
(1, 'SO-002', 158.00, 'pending', '2026-01-02 11:00:00+08'),
(2, 'SO-003', 499.00, 'paid', '2026-01-03 09:00:00+08');
INSERT INTO order_items (order_id, sku, qty, price) VALUES
(1, 'SKU-A', 2, 99.00),
(1, 'SKU-B', 1, 101.00),
(2, 'SKU-C', 1, 158.00),
(3, 'SKU-D', 1, 499.00);第 3 步:单层聚合——把明细折成数组
SELECT o.order_no,
jsonb_agg(jsonb_build_object('sku', i.sku, 'qty', i.qty, 'price', i.price)
ORDER BY i.sku) AS items
FROM orders o
JOIN order_items i ON i.order_id = o.id
GROUP BY o.id, o.order_no
ORDER BY o.order_no; order_no | items
----------+--------------------------------------------------------------------------------------------------
SO-001 | [{"qty": 2, "sku": "SKU-A", "price": 99.00}, {"qty": 1, "sku": "SKU-B", "price": 101.00}]
SO-002 | [{"qty": 1, "sku": "SKU-C", "price": 158.00}]
SO-003 | [{"qty": 1, "sku": "SKU-D", "price": 499.00}]
(3 rows)第 4 步:两层嵌套——订单对象内含明细数组
SELECT jsonb_build_object(
'order_no', o.order_no,
'amount', o.amount,
'status', o.status,
'created_at', to_jsonb(o.created_at),
'items', i.items
) AS order_json
FROM orders o
LEFT JOIN LATERAL (
SELECT jsonb_agg(jsonb_build_object('sku', oi.sku, 'qty', oi.qty, 'price', oi.price)
ORDER BY oi.sku) AS items
FROM order_items oi
WHERE oi.order_id = o.id
) AS i ON true
WHERE o.order_no = 'SO-001'; order_json
-----------------------------------------------------------------------------------------------
{"items": [{"qty": 2, "sku": "SKU-A", "price": 99.00}, {"qty": 1, "sku": "SKU-B", "price": 101.00}],
"amount": 299.00, "status": "paid", "order_no": "SO-001", "created_at": "2026-01-01T02:00:00+00:00"}
(1 row)第 5 步:三层嵌套——用户 → 订单数组 → 明细数组
SELECT jsonb_build_object(
'id', u.id,
'name', u.name,
'email', u.email,
'orders', coalesce(o.orders, '[]'::jsonb)
) AS user_json
FROM users u
LEFT JOIN LATERAL (
SELECT jsonb_agg(
jsonb_build_object(
'order_no', od.order_no,
'amount', od.amount,
'status', od.status,
'items', coalesce(it.items, '[]'::jsonb)
) ORDER BY od.created_at
) AS orders
FROM orders od
LEFT JOIN LATERAL (
SELECT jsonb_agg(jsonb_build_object('sku', oi.sku, 'qty', oi.qty) ORDER BY oi.sku) AS items
FROM order_items oi
WHERE oi.order_id = od.id
) AS it ON true
WHERE od.user_id = u.id
) AS o ON true
ORDER BY u.id; id | name | orders
----+------+--------------------------------------------------------------
1 | 甲 | [{"items": [...], "amount": 299.00, "status": "paid", "order_no": "SO-001"},
{"items": [...], "amount": 158.00, "status": "pending", "order_no": "SO-002"}]
2 | 乙 | [{"items": [{"qty": 1, "sku": "SKU-D"}], "amount": 499.00, "status": "paid", ...}]
3 | 丙 | [] -- 无订单,coalesce 兜底为空数组
(3 rows)用户"丙"没有订单,coalesce(o.orders, '[]'::jsonb) 保证了输出是 [] 而不是 null——这是接口设计里非常重要的一致性保证。
第 6 步:用 to_jsonb 快速导出整行
-- 整行导出,省去逐列书写
SELECT to_jsonb(u) AS user_row
FROM (SELECT id, name, email FROM users WHERE id = 1) AS u;第 7 步:清理 null 字段
-- 把值为 null 的字段删掉,让响应更干净
SELECT jsonb_strip_nulls(
jsonb_build_object('id', 1, 'name', '甲', 'phone', NULL, 'remark', NULL)
) AS cleaned; cleaned
----------------------------------
{"id": 1, "name": "甲"}
(1 row)第 8 步:清理
DROP TABLE order_items;
DROP TABLE orders;
DROP TABLE users;注意事项
jsonb_build_object的参数个数必须是偶数,键值交替,漏写一个值会直接报错。
-- 错误
SELECT jsonb_build_object('a', 1, 'b');ERROR: argument list must have even number of elements- 键不能为
NULL,会直接抛错而不是跳过。若键来自数据列,需先用coalesce兜底。
-- 错误
SELECT jsonb_build_object(NULL, 1);
-- 正确:给键兜底(但要警惕兜底后产生的键冲突)
SELECT jsonb_build_object(coalesce(k, 'unknown'), v) FROM t;jsonb_object_agg遇重复键静默覆盖,不报错,容易造成数据丢失;键不唯一时用 PG16+ 的jsonb_object_agg_unique。
-- 静默丢数据
SELECT jsonb_object_agg(k, v) FROM (VALUES ('a',1), ('a',2)) AS t(k, v);
-- {"a": 2}jsonb_strip_nulls不删数组里的 null 元素,只删对象字段。
SELECT jsonb_strip_nulls('{"a": [1, null, 2]}'::jsonb); -- {"a": [1, null, 2]}jsonb_agg不保证顺序,必须显式ORDER BY,否则测试会间歇性失败。
-- 推荐
SELECT jsonb_agg(name ORDER BY id) FROM products;jsonb_agg保留 NULL 输入并转成 JSONnull,要去掉需FILTER或jsonb_agg_strict(PG16+)。
SELECT jsonb_agg(x) FILTER (WHERE x IS NOT NULL) FROM t;row_to_json返回json而非jsonb,json不能比较、不能建 GIN 索引;除非下游要求json,否则一律用to_jsonb。
-- 返回类型不同
SELECT pg_typeof(to_jsonb(1)) AS a, -- jsonb
pg_typeof(row_to_json(ROW(1))) AS b; -- jsonto_jsonb(整行)会带上所有列,包括不该暴露的密码哈希、内部字段,接口层务必用子查询先选列。
-- 危险
SELECT to_jsonb(u) FROM users u;
-- 安全
SELECT to_jsonb(x) FROM (SELECT id, name FROM users) AS x;LATERAL子查询不要忘记ON true,写成逗号连接会丢失无匹配行的主行。
-- 正确:保留无订单的用户
LEFT JOIN LATERAL (...) AS o ON true;
-- 错误:无订单的用户会整行消失
, LATERAL (...) AS o- 嵌套聚合层数越多代价越高,三层以上开始明显地随主表行数线性放大,接口层应考虑拆分为多次查询。
-- 每多一层 LATERAL,就多一次按行的聚合执行
EXPLAIN (ANALYZE, BUFFERS) SELECT jsonb_build_object(...) FROM users u LEFT JOIN LATERAL (...) ON true;jsonb会重排键并去重,与手写json文本的输出顺序不同,不要依赖键顺序做断言。
SELECT jsonb_build_object('b', 1, 'a', 2); -- {"a": 2, "b": 1}- 构建出来的 JSON 不参与索引。若这些 JSON 需要被查询,应把关键字段落成独立列并建索引,JSON 只作响应载体。
-- 需要按 status 查询时,依赖独立列而非 JSON 内部字段
SELECT jsonb_agg(x) FROM orders WHERE status = 'paid';最佳实践
- 先用子查询选列,再
to_jsonb包一层,这是最不易漏字段的写法。
SELECT to_jsonb(u) FROM (SELECT id, name, email FROM users LIMIT 10) AS u;- "多行折叠"一律显式写
ORDER BY,让输出稳定可测。
jsonb_agg(order_no ORDER BY created_at DESC)- 嵌套从表用
LEFT JOIN LATERAL ... ON true+coalesce(..., '[]'::jsonb),"无子行"统一输出空数组而不是 null。
'orders', coalesce(o.orders, '[]'::jsonb)- 区分三处 NULL 语义:构建函数保留 NULL、
jsonb_agg保留 NULL、jsonb_strip_nulls只删对象字段的 NULL。展示层要"干净"就在最外层统一jsonb_strip_nulls。
SELECT jsonb_strip_nulls(final_json) FROM (...);- PG16+ 优先用
_strict/_unique变体,把"跳过 NULL""重复键"从隐式行为变成显式语义。
jsonb_agg_strict(x)
jsonb_object_agg_unique(k, v)- 金额用
numeric而非float,numeric会按 scale 输出,避免浮点尾数进入 JSON。
jsonb_build_object('amount', amount) -- amount 为 numeric(10,2)- 接口层只输出必要字段,绝不
to_jsonb(整行),防止内部字段泄露。
SELECT to_jsonb(x) FROM (SELECT id, name FROM users) AS x;- 聚合层数控制在三层以内,返回给前端时用
jsonb_pretty只在调试环境打印,生产不要用它(额外格式化开销)。
-- 仅调试
SELECT jsonb_pretty(order_json) FROM (...);- 需要按 JSON 内部条件筛选时,先筛行再构造,不要构造完再在应用层过滤。
-- 推荐:先过滤
SELECT jsonb_agg(jsonb_build_object('no', order_no))
FROM orders WHERE status = 'paid';- 用
EXPLAIN (ANALYZE, BUFFERS)验证嵌套聚合,LATERAL写法在行数多时可能退化为逐行扫描,必要时改成"先聚合再 JOIN"。
EXPLAIN (ANALYZE, BUFFERS)
SELECT jsonb_build_object('id', u.id, 'orders', o.orders) FROM users u LEFT JOIN LATERAL (...) ON true;下一节