SQL Server里用CROSS APPLY解析JSON字段的常见写法
SQL Server从2016版本开始原生支持JSON,这一点想必很多人已经接触过了。不过有个细节特别容易卡壳:想把JSON数组展开成行,OPENJSON()必须和CROSS APPLY搭配着用——它俩就像锁和钥匙,缺一不可。直接在WHERE或SELECT里调JSON_VALUE()?那只能拿到单值,数组根本“炸”不开。
举个最常见的场景:订单表orders里有个details列,存的是类似[{"item":"A","qty":2},{"item":"B","qty":1}]这样的JSON数组。想把每项明细变成独立的一行,同时保留主表ID,怎么办?
标准写法就是下面这样:
SELECT o.id, j.item, j.qtyFROM orders oCROSS APPLY OPENJSON(o.details)WITH ( item NVARCHAR(50) '$.item', qty INT '$.qty') AS j;
这里有两个关键点:第一,OPENJSON()是表值函数,必须用CROSS APPLY才能绑定到左侧的每行;第二,WITH子句用来定义输出的列名、类型以及JSON路径,路径以$开头,代表当前数组元素。
为什么不能用INNER JOIN替代CROSS APPLY?
这个问题经常被人问起。原因其实很简单:INNER JOIN要求右侧是一个完整的、可以独立执行的表或子查询,但OPENJSON()严重依赖左侧当前行的details值——它不是静态数据源,每次调用都绑定到不同的行。硬要用JOIN,要么直接语法错误,要么报类似Invalid use of a side-effecting operator 'OPENJSON' within a function的错(如果包在函数里)。
实际操作中容易踩的几个坑:
CROSS APPLY遇到NULL或无效JSON时,整行会被过滤掉——这跟INNER JOIN的语义一样。如果想保留主表那些JSON为空的记录,得换成OUTER APPLY。OPENJSON()默认只解析最顶层的数组。如果JSON是个对象,比如{"items":[...]},就需要先用JSON_QUERY()把数组提出来:CROSS APPLY OPENJSON(JSON_QUERY(o.data, '$.items'))。- 如果不写
WITH子句,OPENJSON()会返回key、value、type三列,而且所有值都是NVARCHAR(MAX)。后续做类型转换?那可就容易出错了。
CROSS APPLY + OPENJSON()的性能影响点
JSON解析是出了名的CPU密集型操作,尤其当字段里嵌套又多、文本又长的时候。执行计划里会冒出一个Table-valued function算子,而且这种场景下索引基本帮不上忙——除非你提前建了计算列再加索引。
几个实操建议:
- 尽量避免在
WHERE里直接对已展开的JSON字段做过滤,比如WHERE j.item = 'X'。这会导致全表先解析一遍再筛,效率极低。更好的做法是先利用JSON_VALUE()做粗筛:WHERE JSON_VALUE(o.details, '$[0].item') = 'X'。 - 如果某个JSON字段经常被查询,可以考虑建一个持久化计算列:
ALTER TABLE orders ADD item_name AS JSON_VALUE(details, '$[0].item');,然后给它加上索引。这样查询就能走索引了。 OPENJSON()不支持参数化路径,比如'$[' + @idx + '].item'这种写法行不通。硬要拼接字符串?风险太大,还有SQL注入的可能,不推荐。
PostgreSQL 或 MySQL 用户别硬套这个模式
这套写法是SQL Server独有的。PostgreSQL里对应的机制是LATERAL,MySQL 8.0+用的是JSON_TABLE(),虽然功能类似,但语法细节和底层行为差异很大。
举个例子对比一下:
-- PostgreSQLSELECT o.id, j.item, j.qtyFROM orders o,LATERAL json_to_recordset(o.details) AS j(item TEXT, qty INT);
-- MySQL 8.0+SELECT o.id, j.item, j.qtyFROM orders oJOIN JSON_TABLE(o.details, '$[*]' COLUMNS (item VARCHAR(50) PATH '$.item', qty INT PATH '$.qty')) AS j;
跨数据库做迁移时,CROSS APPLY + OPENJSON这整套逻辑几乎都要重写,别想着脚本能通用。
说到底,JSON字段关联真正让人头疼的从来不是语法本身,而是schema变更后WITH子句漏改了、空值处理不一致、还有那个永远没人测过的超长JSON边界情况。这些坑踩过一次,下次就记住了。
