json字段不能直接group by,必须先展开成行再分组;mysql 8.0+用json_table展开,postgresql用jsonb_array_elements()配合lateral,均需确保类型匹配、空值过滤及正确分组维度。

直接回答:JSON字段不能直接GROUP BY,必须先展开成行,再按原始主键或提取字段分组;否则聚合会跨记录混算,结果完全不可信。
MySQL 8.0+ 用 JSON_TABLE 展开后 GROUP BY
MySQL 没有类似 PostgreSQL 的 set-returning 函数,JSON_TABLE() 是唯一可靠方案。它把 JSON 数组“虚拟建表”,但必须显式声明路径和列结构。
- 路径写
'$[*]'才能遍历全部元素,写'$[0]'或省略路径会漏数据 -
COLUMNS中类型必须匹配实际值,比如数组元素是字符串就用TEXT PATH '$',是数字就用INT PATH '$.qty' - 字段存的是字符串(如
"[\"a\",\"b\"]")而非 JSON 类型,JSON_TABLE()会静默返回空——得先CAST(items AS JSON)或确保 DDL 定义为JSON - NULL 或非法 JSON 默认跳过整行,加
LEFT JOIN+COALESCE更稳妥
示例(统计每个订单里各商品数量总和):
SELECT o.id, jt.item_id, SUM(jt.qty) AS total_qty
FROM orders o
LEFT JOIN JSON_TABLE(
o.items,
'$[*]' COLUMNS (
item_id INT PATH '$.id',
qty INT PATH '$.qty'
)
) AS jt ON TRUE
WHERE JSON_VALID(o.items) AND JSON_LENGTH(o.items) > 0
GROUP BY o.id, jt.item_id;
PostgreSQL 用 jsonb_array_elements() 配合 LATERAL
jsonb_array_elements() 是最常用方式,但它只接受 jsonb 类型,且对 NULL 或空数组无容忍——不提前过滤会导致整行丢失。
- 字段是
json类型?必须先转:items::jsonb - 空数组
[]和NULL都要过滤:WHERE items IS NOT NULL AND jsonb_typeof(items) = 'array' AND jsonb_array_length(items) > 0 - 提取字段用
->>'key'(字符串)或(elem->>'qty')::int(转数值),别漏类型转换 - 展开后必须保留原始主键(如
orders.id)用于外层GROUP BY,否则聚合跨订单
示例(统计每单中各状态出现次数):
SELECT o.id, elem->>'status' AS status, COUNT(*) AS cnt FROM orders o, LATERAL jsonb_array_elements(o.items) AS elem WHERE o.items IS NOT NULL AND jsonb_typeof(o.items) = 'array' AND jsonb_array_length(o.items) > 0 GROUP BY o.id, elem->>'status';
聚合前必须确认:你到底想按什么分组?
这是最容易被跳过的逻辑起点。JSON 字段展开后,行数膨胀,分组维度错位会导致结果彻底失真。
- 想“按订单汇总”?
GROUP BY orders.id—— 这是绝大多数场景的正确选择 - 想“按商品 ID 统计全表销量”?那就要
GROUP BY elem->>'id',但得确保所有订单都展开且没丢行 - 原始记录还有其他关键字段(如
order_time、user_id)?它们要么进GROUP BY,要么用MIN()/ANY_VALUE()包裹,否则 PG 会报错 - MySQL 中若需保留非聚合字段又不想写满
GROUP BY,得开sql_mode允许ONLY_FULL_GROUP_BY以外的行为,但不推荐
真正麻烦的不是函数怎么写,而是展开后数据与业务语义的对齐——比如一个订单里两个相同商品对象,是该去重还是累加?这得看业务规则,SQL 本身不会替你判断。











