正确做法是先用jsonb_object_agg(key, value)构建单行对象,再用jsonb_agg()聚合成数组;需处理NULL键、类型显式转换、多层嵌套须用CTE或子查询隔离、慎用jsonb_pretty()、LEFT JOIN空组需COALESCE补位。

用 jsonb_agg() 和 jsonb_object_agg() 组合嵌套结构
PostgreSQL 16 的 jsonb_agg() 默认把行转成 JSONB 数组,但容易忽略它不保留字段名——除非你显式构造对象。常见错误是直接 SELECT jsonb_agg(t.*) FROM table t,结果里字段顺序不可靠,且无法按需筛选键。
正确做法是先用 jsonb_object_agg(key, value) 构建单行对象,再用 jsonb_agg() 聚合成数组:
SELECT jsonb_agg(
jsonb_object_agg(
'name', name ||
'score', score
)
) AS users_summary
FROM users;
注意:jsonb_object_agg() 要求 key 是标量(如 text),不能是 NULL;若 key 可能为空,得提前 FILTER (WHERE key IS NOT NULL) 或用 COALESCE(key, 'unknown')。
避免 jsonb_build_object() 中的类型隐式转换陷阱
jsonb_build_object() 看似更直观,但参数必须成对出现(key, value),且 value 若为表达式,类型不匹配会静默失败或报错。例如 jsonb_build_object('count', COUNT(*), 'avg', AVG(price)) 在某些分组下可能因 AVG() 返回 numeric 而 count 是 int8,虽不报错,但后续用 ->> 提取时可能因类型预期不符出问题。
建议统一 cast 关键字段:
- 数值类用
::text或::double precision显式声明精度 - 时间类务必用
to_jsonb(created_at)而非直接拼接字符串,避免时区丢失 - 布尔值别依赖自动转换,写成
'is_active', is_active::text::jsonb更可控
在 GROUP BY 报表中嵌套多层 jsonb_agg() 需手动控制层级
想生成「每个部门 → 员工列表 → 每个员工含订单汇总」这类三层结构?jsonb_agg() 不支持自动递归嵌套。常见错误是试图在一个 SELECT 里堆叠多个 jsonb_agg(),结果要么聚合错层,要么因缺少子查询隔离而笛卡尔膨胀。
必须拆成子查询或 CTE:
WITH dept_orders AS (
SELECT
d.name AS dept_name,
jsonb_agg(
jsonb_build_object(
'user_id', u.id,
'orders', (
SELECT jsonb_agg(
jsonb_build_object('id', o.id, 'total', o.total)
)
FROM orders o WHERE o.user_id = u.id
)
)
) AS users_data
FROM departments d
JOIN users u ON u.dept_id = d.id
GROUP BY d.name
)
SELECT jsonb_object_agg(dept_name, users_data) FROM dept_orders;
这里的关键是:内层 SELECT ... jsonb_agg(...) 必须用括号包住,否则会被当成普通列参与外层 GROUP BY,导致语法错误或逻辑错乱。
性能敏感场景下慎用 jsonb_pretty() 和深度嵌套
jsonb_pretty() 仅用于调试输出,它强制格式化、增加空格换行,会显著拖慢大结果集的序列化。线上报表接口若返回 MB 级 JSONB,应禁用它。
另外,PostgreSQL 16 对深度超过 ~10 层的嵌套 JSONB 解析效率明显下降,尤其配合 #>> 或 @? 进行路径查询时。如果报表结构天然很深,考虑是否真需要全量嵌套——有时用关联 ID + 客户端组装,比服务端拼死 JSONB 更快也更易缓存。
真正难处理的不是语法,而是当 jsonb_agg() 遇上 LEFT JOIN 后的空组:空数组会变成 [],但空对象(如 jsonb_object_agg() 在无行时返回 NULL)需用 COALESCE(..., '{}'::jsonb) 补位,否则前端解析可能崩。这点极易被漏测。











