SQL Server 与 JSON 的组合经常被低估。多数项目里,开发拿到第三方接口返回的数据,第一反应是“JSON 也是字符串,丢进 VARCHAR(MAX) 列里存起来总没错”。这个判断只对了一半:存放确实没问题,但当你第二天想按 JSON 里的某个 key 去筛选、统计、关联表数据时,纯字符串方案会让人痛不欲生。真正值得花时间理清楚的,是从 SQL Server 2016 开始内置的那套 JSON 函数,以及它们在各种版本、各种兼容级别下的表现差异。
这篇笔记围绕“SQL Server 里的 JSON”展开,适合正在用 SQL Server 2016 及以上版本、希望把 JSON 当作可查询数据使用的开发。如果你还在维护 SQL Server 2008 R2 这类老环境,建议直接跳到最后一节,那些原生函数在老版本中确实用不了,省得踩完坑再回来骂娘。
1. 原生 JSON 支持从哪里开始:版本门槛与兼容级别
1.1 不是装了新版 SQL Server 就等于拥有全部 JSON 能力
打开 SQL Server Management Studio,输入 OPENJSON 却报“不是内置函数名称”,大多数人第一反应是“我的版本不支持 JSON”。其实还有一类更隐蔽的情况:数据库是从老版本直接还原到新实例上的,兼容级别还停留在 100 或 110,导致新函数虽然在引擎里存在,却没法在这个数据库里正常使用。
先记住一个硬结论:SQL Server 2016 是原生 JSON 支持的分水岭。2008、2008 R2、2012、2014 都没有内置 JSON 解析函数;从 2016 起,ISJSON、JSON_VALUE、JSON_QUERY、JSON_MODIFY、OPENJSON、FOR JSON 这些能力才真正可用。2022 版本又在生成方向补了 JSON_OBJECT 和 JSON_ARRAY 两个函数,写起来会舒服不少。
| SQL Server 版本 | 原生 JSON 读取/修改函数 | FOR JSON | JSON_OBJECT / JSON_ARRAY |
|---|---|---|---|
| 2008 / 2008 R2 / 2012 | 不支持 | 不支持 | 不支持 |
| 2014 | 不支持 | 不支持 | 不支持 |
| 2016 / 2017 | 支持 | 支持 | 不支持 |
| 2019 | 支持 | 支持 | 不支持 |
| 2022 | 支持 | 支持 | 支持 |
这里要特别解释一下 SQL Server 的“兼容级别”。兼容级别是一个数据库级配置,相当于告诉数据库引擎“请用接近哪个历史版本的规则来执行”。很多从 SQL Server 2008 R2 直接附加到 2019 实例上的库,兼容级别仍是 100。数据库物理文件在新实例上跑,但 OPENJSON 这类函数在兼容级别不够时会直接报错。
1.2 遇到“函数不存在”先查兼容级别,别急着怪版本
排查这类问题,我建议按三步走。第一步确认实例版本,第二步看数据库兼容级别,第三步才是怀疑安装包或环境配置。
sql复制-- 查看实例版本
SELECT @@VERSION;
-- 查看当前所有数据库的兼容级别
SELECT name, compatibility_level
FROM sys.databases;
如果发现某个库的兼容级别低于 130,而你确认实例版本是 SQL Server 2016 及以上,直接调整数据库兼容级别即可:
sql复制ALTER DATABASE [你的数据库名]
SET COMPATIBILITY_LEVEL = 130;
130 对应 SQL Server 2016,140 对应 2017,150 对应 2019,160 对应 2022。调整兼容级别不只是为了用 JSON 函数,它还会改变查询优化器的行为模型,属于影响面较大的变更。生产环境必须先做回归测试,别在业务高峰时段直接执行。我见过因为把兼容级别从 100 调到 150 后,某条跑了多年的慢查询执行计划突变,从秒级变成分钟级的情况。所以结论是:JSON 函数需要兼容级别至少 130,但不要为了一个函数盲目调高,先小范围验证再推广。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 校验、取值与查子对象:ISJSON、JSON_VALUE、JSON_QUERY 三件套
2.1 ISJSON 是数据入口的安检闸门
在把值存进 JSON 列之前,最好先用 ISJSON 判断一下。它能返回 1、0 或 NULL:1 表示字符串是合法 JSON,0 表示不是,NULL 表示输入本身就是 NULL。
sql复制SELECT
ISJSON(N'{"name":"张三"}') AS valid_json, -- 1
ISJSON(N'{"name": 张三}') AS invalid_json, -- 0
ISJSON(NULL) AS null_input; -- NULL
这个函数看似简单,用处却很大。你可以在表上直接加 CHECK 约束,从数据库层面挡住不规范 JSON 数据进入业务表:
sql复制CREATE TABLE dbo.ApiLog
(
LogId INT IDENTITY PRIMARY KEY,
Payload NVARCHAR(MAX) NOT NULL,
CONSTRAINT CK_ApiLog_Payload_IsJson CHECK (ISJSON(Payload) = 1)
);
加这个约束不会影响写入性能太多,却能避免后续每个读到 Payload 的地方都重复做 JSON 合法性判断。对于直接对接第三方回调、把原始报文落库的场景,非常推荐加上。
2.2 JSON_VALUE 取标量,JSON_QUERY 取对象或数组
刚接触 SQL Server JSON 的人,最容易犯的错误就是搞不清这两个函数的分工。简单记:JSON_VALUE 用来取标量值,也就是字符串、数字、布尔值这类叶子节点;JSON_QUERY 用来取 JSON 对象或数组,它会把这段子结构原样以 JSON 文本形式返回。
用一个模拟订单数据来演示最直观:
sql复制DECLARE @json NVARCHAR(MAX) = N'
{
"orderId": 10248,
"status": "PAID",
"customer": {
"name": "张三",
"level": "黄金"
},
"items": [
{"sku": "A-100", "price": 58.5},
{"sku": "B-200", "price": 32}
]
}';
SELECT
JSON_VALUE(@json, '$.orderId') AS order_id,
JSON_VALUE(@json, '$.status') AS status,
JSON_VALUE(@json, '$.customer.name') AS customer_name,
JSON_QUERY(@json, '$.customer') AS customer_object,
JSON_QUERY(@json, '$.items') AS items_array;
路径表达式里的 $ 代表 JSON 根节点,$.customer.name 是沿着根节点往下找 customer 对象里的 name 字段。JSON_VALUE 返回的是 NVARCHAR 类型,如果原值是数字,想参与数值运算就需要显式转换:
sql复制SELECT
JSON_VALUE(@json, '$.orderId') AS order_id_text,
CAST(JSON_VALUE(@json, '$.orderId') AS INT) AS order_id_int;
有一个常见的坑:对一个数组字段使用 JSON_VALUE,返回的是 NULL 而不是数组文本。因为数组属于对象/结构类型,JSON_VALUE 只处理标量。想判断某个路径上是否存在数组,或者想取出数组内容继续处理,要用 JSON_QUERY。反过来,如果用 JSON_QUERY 取一个字符串字段,它返回的内容会去掉引号,看起来像是丢了一个引号,实际是在按 JSON 语义解码字符串。把握住“标量用 JSON_VALUE,结构用 JSON_QUERY”这条原则,能避开这类问题。
2.3 路径表达式里的 lax 与 strict:找不到路径时行为完全不同
JSON 路径默认是 lax 模式。lax 模式下路径不存在会返回 NULL,不报错;strict 模式则要求路径必须存在,不存在会直接抛错。例如:
sql复制SELECT JSON_VALUE(@json, 'lax $.customer.phone') AS lax_phone; -- NULL
SELECT JSON_VALUE(@json, 'strict $.customer.phone') AS strict_phone; -- 报错
实际开发中,接口返回结构不稳定时,lax 模式很有用。但 lax 模式也可能把错误吞掉,让你以为某个字段不存在,实际是路径写错。我自己的做法是:在写临时查询和分析脚本时显式写明 lax 或 strict,别依赖默认行为;在存储过程里根据业务需要决定是否要用 strict 让数据异常快速暴露。路径写错的排查也很简单,报错信息“JSON path is not properly formatted”通常说明语法有问题,而“Property cannot be found on the specified JSON path”则说明路径存在格式问题或路径没有匹配到数据。
3. OPENJSON:把 JSON 数组变成行集,这才是核心战斗力
3.1 默认返回 key-value 结构,适合快速浏览
JSON_VALUE 一次只能取一个字段,处理数组里的多条记录非常笨拙。OPENJSON 的价值在于能把 JSON 文档当成“一张临时表”来查。
先看无显式列定义的用法:
sql复制DECLARE @json NVARCHAR(MAX) = N'
{
"orderId": 10248,
"customer": {"name": "张三"},
"items": [
{"sku": "A-100", "price": 58.5},
{"sku": "B-200", "price": 32}
]
}';
SELECT *
FROM OPENJSON(@json, '$.items');
不写 WITH 时,OPENJSON 返回三列:key、value、type。key 是数组里的索引,value 是数组元素序列化后的文本,type 表示数据类型。这种形式适合快速查看数组内有什么,但不适合直接参与业务查询,因为每个元素还是一个未拆解的 JSON 文本,用起来不够爽。
3.2 用 WITH 定义显式列结构,真正进入关系型查询模式
显式定义列结构才是常用姿势:
sql复制SELECT sku, price
FROM OPENJSON(@json, '$.items')
WITH (
sku NVARCHAR(50) '$.sku',
price DECIMAL(10, 2) '$.price'
);
这段 SQL 的含义是:去 @json 的 $.items 数组里遍历每一个元素,把每个元素的 $.sku 映射到 sku 列,类型是 NVARCHAR(50);把 $.price 映射到 price 列,类型是 DECIMAL。WITH 子句里的路径是相对于当前遍历元素而言的,所以不用再写 $.items[0].sku 这种路径。
类型转换也很关键。如果 JSON 里 price 写成了字符串形式“58.5”,映射到 DECIMAL 时 SQL Server 会尝试自动转换,转换失败就会报错。更隐蔽的是:JSON 里某个数字大到超出目标字段精度,也会直接报错。所以显式列定义中的类型要和实际数据格式严格对齐,宁可先在文档层面约定好,也不要依赖碰运气的隐式转换。
3.3 CROSS APPLY 处理“一表带多个 JSON 商品”的经典场景
实际业务中更常见的情况是:主表里每一行都有一个 JSON 数组字段,比如订单表 Orders 里存了 items,需要把每个订单的 JSON 商品明细炸开,和订单主键拼成一张明细结果集。
sql复制SELECT
o.OrderId,
oj.sku,
oj.price
FROM dbo.Orders AS o
CROSS APPLY OPENJSON(o.Payload, '$.items')
WITH (
sku NVARCHAR(50) '$.sku',
price DECIMAL(10, 2) '$.price'
) AS oj
WHERE oj.price > 100;
CROSS APPLY 在这里的作用相当于把 OPENJSON 当作一个表值函数,对 Orders 表的每一行执行一次 JSON 解析。结果就是“主表一行 + 明细多行”的笛卡尔展开。外层再套 WHERE、JOIN、GROUP BY 都不成问题,JSON 数据在查询层已经完成了关系型化。
遇到嵌套层级较深的 JSON 时,比如订单里有 customer,customer 里有 address,只要在 WITH 里继续用路径指到子对象字段即可:
sql复制SELECT
customer_name,
city
FROM OPENJSON(@json, '$.orderInfo')
WITH (
customer_name NVARCHAR(50) '$.customer.name',
city NVARCHAR(50) '$.customer.address.city'
);
OPENJSON 本质上是把 JSON 转成行集的入口,路径负责定位,WITH 负责拉平。能在 JSON 层级里找到的目标字段,几乎都可以通过组合路径在 WITH 中定义出来。如果目标结构里有二级甚至三级数组,那就需要多次 CROSS APPLY,一层层展开。每层展开时要注意别让数据量级爆炸,JSON 里数组套数组又碰上大表,解析消耗会成倍上升。
4. 修改 JSON 字段:JSON_MODIFY 的更新边界与转义陷阱
4.1 修改单字段、追加数组元素、删除老字段
SQL Server 没有“直接 update JSON 对象里某个 key”的 T-SQL 原生语法,靠的是 JSON_MODIFY 来生成一份新的 JSON 文本。它的思路比较朴素:传入原 JSON、路径、新值,返回修改后的完整字符串。
sql复制DECLARE @json NVARCHAR(MAX) = N'
{
"orderId": 10248,
"status": "CREATED",
"customer": {"name": "张三", "level": "黄金"},
"items": []
}';
-- 修改普通标量
SET @json = JSON_MODIFY(@json, '$.status', 'PAID');
-- 修改嵌套对象里的标量
SET @json = JSON_MODIFY(@json, '$.customer.level', '钻石');
-- 向数组追加一个元素
SET @json = JSON_MODIFY(@json, 'append $.items',
JSON_QUERY(N'{"sku": "C-300", "price": 99.9}'));
-- 删除字段:把新值设为 NULL
SET @json = JSON_MODIFY(@json, '$.customer.phone', NULL);
PRINT @json;
这里有几个使用细节必须讲清楚。
第一,追加数组元素时,路径前缀要写 append $.items,而不是 $.items.append。这个位置容易写反,报错倒还好,有时不报错但会生成一个奇怪的新字段,非常坑。
第二,如果追加的是一个对象,不能直接传一个普通字符串。直接写 '{"sku": "C-300"}' 时,SQL Server 会把这个字符串看成一个普通字符串值,插入后会被转义成 "{""sku"":""C-300""}",也就是把 JSON 对象变成了一串带有大量反斜杠和引号的字符串。要让对象原样插入,必须用 JSON_QUERY 包一层:
sql复制JSON_MODIFY(@json, 'append $.items', JSON_QUERY(N'{"sku": "C-300"}'))
JSON_QUERY 在这里起的作用不是“从 JSON 中读取对象”,而是向函数声明“我这段文本就是 JSON,不要当成普通字符串处理”。这是 SQL Server 官方也认可的一种用法,虽然看起来有几分绕。
第三,JSON_MODIFY 只是字符串级别的文本重组。如果 JSON 文档很大,比如几百 KB 的动态配置,每次改一个字段都要重新序列化整个文档,写入开销不可忽视。对高频更新的场景,不建议把整份 JSON 放在一张大表里用 JSON_MODIFY 反复写。它更适合低频配置更新、接口原始报文订正这类场景。
4.2 修改数组内指定项需要用路径定位,但要小心 parse 代价
JSON_MODIFY 支持通过数组下标定位,例如:
sql复制SET @json = JSON_MODIFY(@json, '$.items[0].price', 199);
这种写法能解决一部分“改数组里第一项”的需求,但路径下标一旦写错,结果可能就是 NULL 或原样不变。更麻烦的是,JSON_MODIFY 一次只能改一个值,如果要同时改数组内多个元素,或者按条件删除数组里某一项,就得靠 OPENJSON 拆出来处理后,再重新组 JSON,或者整体重建数组,基本谈不上优雅。
所以我个人的项目约定是:JSON_MODIFY 适合“改标量、追加元素、删某个顶层字段”这类轻盈操作;一旦涉及复杂数组更新,不要硬凑 JSON_MODIFY。宁可把原数组用 OPENJSON 提取出来,在应用层或临时表里处理完,再用 FOR JSON 拼回去。先保证逻辑清晰,再考虑性能。
5. 反向生成 JSON:FOR JSON 与 2022 版的 JSON_OBJECT
5.1 FOR JSON PATH 把查询结果直接打包成嵌套结构
把 JSON 存进 SQL Server 只是单向需求,很多时候还要把表里的关系型数据转成 JSON 返回给前端。SQL Server 2016 起提供了 FOR JSON,可以让查询结果直接序列化成 JSON 文本。
sql复制SELECT
OrderId AS 'order.id',
OrderNo AS 'order.orderNo'
FROM dbo.Orders
WHERE OrderId = 10248
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER;
FOR JSON PATH 的花样在于列别名里的点号。AS 'order.id' 表示把 order 作为第一层对象,id 作为第二层属性。通过多个点的组合,就能在 SQL 层直接拼出想要的嵌套结构,省掉应用层二次组装的代码。
默认情况下 FOR JSON 输出的是一个数组,哪怕是只有一行结果,也会用 [{...}] 包裹。如果接口要求直接返回一个对象而不是数组,就加 WITHOUT_ARRAY_WRAPPER。如果接口要求最外层带一个统一字段名,那就用 ROOT:
sql复制SELECT OrderId, OrderNo
FROM dbo.Orders
WHERE OrderId = 10248
FOR JSON PATH, ROOT('result');
空值处理要额外说明。FOR JSON 默认不输出值为 NULL 的字段。如果接口需要明确返回 NULL 字段,比如告诉调用方“这个订单没有优惠券”,需要加 INCLUDE_NULL_VALUES:
sql复制SELECT OrderId, CouponCode
FROM dbo.Orders
WHERE OrderId = 10248
FOR JSON PATH, INCLUDE_NULL_VALUES;
不加这个选项时,CouponCode 为 NULL 的行里,生成的 JSON 不会出现 CouponCode 这个键。调用方的反序列化逻辑如果对缺失字段不友好,就会出现各类空引用问题。这个细节在联调阶段最容易暴露,往往是前端拿不到字段,后端又觉得 SQL 没问题,两边排查半天才发现是 FOR JSON 默认行为。
5.2 JSON_OBJECT 和 JSON_ARRAY:2022 版新增的“精准拼装”工具
SQL Server 2022 里新增了 JSON_OBJECT 和 JSON_ARRAY,补齐了“生成 JSON”这个方向上的最后一块拼图。它们可以在 SELECT 列表里直接构造一个对象或数组,不再依赖 FOR JSON 对整条查询结果的序列化。
sql复制SELECT
JSON_OBJECT(
'orderId': OrderId,
'orderNo': OrderNo,
'customer': JSON_OBJECT('name': CustomerName)
) AS order_json
FROM dbo.Orders
WHERE OrderId = 10248;
这里需要注意写法:JSON_OBJECT 里键和值之间用冒号分隔,而不是等号。NULL 处理也有 ABSENT ON NULL 和 NULL ON NULL 两种选项:
sql复制SELECT JSON_OBJECT('orderId': 10248, 'coupon': NULL ABSENT ON NULL) AS s1;
SELECT JSON_OBJECT('orderId': 10248, 'coupon': NULL NULL ON NULL) AS s2;
ABSENT ON NULL 表示字段值为 NULL 时不输出该键,NULL ON NULL 表示字段值为 NULL 时仍输出该键并赋值为 null。实际使用中,JSON_OBJECT 更适合对单个列的键名、值做精细控制,避免 FOR JSON PATH 里靠字符串别名拼接造成的转义麻烦。FOR JSON PATH 适合整表或整段查询结果的大范围序列化,两者定位不同,可按场景搭配使用。
6. 给 JSON 查询加上“索引”:计算列与避免无脑扫描
6.1 JSON 字段本质上还是文本,不能绕过索引问题
很多人在 SQL Server 里对 JSON 列做条件查询时,会直接写出这样的 SQL:
sql复制SELECT *
FROM dbo.Orders
WHERE Payload LIKE '%"status":"PAID"%';
这段 SQL 在数据量小的时候感觉还行,一旦表到几十万行,查询性能会迅速恶化。LIKE 无法走常规索引,而且这种字符串匹配非常脆弱:字段间空格、键顺序、转义写法稍有不同,匹配就失效。JSON 值的顺序并不影响语义,{"status":"PAID","amount":100} 和 {"amount":100,"status":"PAID"} 代表同一个意思,但 LIKE 却会因顺序不同给出不同结果。
正确做法是让 SQL Server 把 JSON 里的某个字段提取成一个独立的计算列,再在计算列上建索引。
sql复制ALTER TABLE dbo.Orders
ADD OrderStatus AS JSON_VALUE(Payload, '$.status');
CREATE INDEX IX_Orders_OrderStatus ON dbo.Orders(OrderStatus);
这样,查询条件直接写成:
sql复制SELECT *
FROM dbo.Orders
WHERE OrderStatus = 'PAID';
查询优化器可以走 IX_Orders_OrderStatus 索引,性能与普通列索引没有本质差别。同理,还可以针对订单金额、客户编号等高频查询字段建立多个计算列,目的就是把 JSON 里真正用于检索和筛选的字段“提升”到表结构层面。
不过要注意:计算列索引适合“固定提取少量关键字段”的场景。如果业务里有几十个字段都要做条件查询,建几十个计算列反而会让表结构很难维护。在设计阶段就应该区分哪些 JSON 字段只是展示用,哪些会作为筛选条件,只对后者建立计算列索引。
6.2 持久化计算列与数据类型设计
计算列可以加 PERSISTED 关键字,让 SQL Server 把计算结果物理存储下来。它有两个好处:一是以后查询不再重复计算提取逻辑,二是能在此基础上创建索引。但如果我不指定 PERSISTED,而是直接创建索引,SQL Server 在某些版本下也能正常工作,因为它会按照建立索引时的规则处理计算列。为了减少不确定性,我的习惯是:明确要建索引的计算列,直接写成 PERSISTED:
sql复制ALTER TABLE dbo.Orders
ADD CustomerLevel AS JSON_VALUE(Payload, '$.customer.level') PERSISTED;
CREATE INDEX IX_Orders_CustomerLevel ON dbo.Orders(CustomerLevel);
这里还有一个经常被忽视的坑:JSON_VALUE 返回的类型是 NVARCHAR(MAX)。如果计算列直接取自 JSON_VALUE,类型也是 NVARCHAR(MAX)。NVARCHAR(MAX) 列创建普通索引时会有大小限制,SQL Server 要求索引键列最大允许 900 字节,否则创建索引会报错。实际处理中,如果待索引的 JSON 字段是状态码、编号这类短字符串,建议 CAST 成 NVARCHAR(50) 或 NVARCHAR(100):
sql复制ALTER TABLE dbo.Orders
ADD OrderStatus AS CAST(JSON_VALUE(Payload, '$.status') AS NVARCHAR(50)) PERSISTED;
这条经验是从生产环境里踩出来的。最开始直接用 JSON_VALUE 建索引,结果短数据多了总会碰到错误;后来把类型收紧到业务实际长度,索引才稳定建立起来。如果你在建计算列索引时报错“column is too large to be a key”,优先检查返回类型是不是 NVARCHAR(MAX)。
6.3 小数据集上的 JSON 扫描未必不可接受
话又说回来,索引不是银弹。如果业务表本身只有几万行,每行 Payload 体积也不大,那直接对 JSON 字段做全表扫描配合 OPENJSON,未必慢到不可接受。SQL Server 的 OPENJSON 解析速度尚可,而建立过多计算列索引反而会增加写入成本。判断方式是实测:把典型查询跑一遍,观察 STATISTICS IO 和 STATISTICS TIME。没有数据量级的性能优化都是空谈,先量化,再决定是否建计算列。
7. 关系表与 JSON 字段的边界:不是所有数据都该塞进 JSON
7.1 JSON 适合什么,关系表又适合什么
我在项目里见过把订单主表所有字段都塞进一个 JSON 列、然后天天用 JSON_VALUE 去取值的架构。这种设计虽然能快速上线,但当 JSON 字段开始被 WHERE、JOIN、GROUP BY、ORDER BY 频繁引用时,复杂度会逐渐失控。SQL Server 的 JSON 能力再强,它的定位也是“关系型数据库的 JSON 补充”,而不是让你把整个数据库改造成文档数据库。
比较稳妥的判断标准是看数据的形态稳定性:
- 业务字段关系明确、更新频繁、需要强约束,比如金额、数量、状态、外键,这类数据应该继续使用关系列。
- 结构经常变化、由上游接口直接传入、只做保存和整段展示的数据,比如第三方回调原始报文、用户扩展属性、前端保存的页面配置,这类数据适合用 JSON 列保存。
表结构里如果同时存在大量正式列和一个 JSON 扩展列,查询时正式列走索引,扩展列用于装载那些不常筛选的信息,这种混搭是我在实际项目里比较推荐的方案。它既没有丢掉关系型数据库的强一致性和索引优势,又保留了 JSON 的灵活扩展能力。
7.2 使用 CHECK 约束保证 JSON 列里永远是合法 JSON
只要决定使用 JSON 列,就应当从第一天开始加 CHECK 约束,避免业务程序写出半截 JSON 或把对象误存成字符串:
sql复制ALTER TABLE dbo.Orders
ADD CONSTRAINT CK_Orders_Settings_IsJson
CHECK (ISJSON(ExtendedProps) = 1);
这一步成本极低,但对数据质量的保障非常高。就算应用层已经有 JSON 序列化工具,也无法保证所有历史写入路径都经过同一套校验逻辑。数据库层的约束是最底线的兜底,平时看不到作用,但出问题时能救整个团队一命。
7.3 避免维护同一份数据的“双写”
还有一种隐蔽的反模式:表里既有关联式明细行,又有汇总后的 JSON 列,两边数据其实是同一份业务的两种形态。开发为了查询方便在明细表更新时同步去改 JSON 列,天长日久两端必然出现不一致。
比较可靠的做法是确立单向数据流。明细数据只写在关系表里,需要 JSON 输出时用 FOR JSON 动态生成;或者上游只提供 JSON,入库后通过 OPENJSON 拆成关系表,JSON 原文仅作为历史快照存在。不要在同一套业务里同时维护两个互相依赖数据源。数据只有单一可信来源,后面才不会出乱七八糟的对账问题。
8. 老版本用户怎么办:SQL Server 2008 R2 的 JSON 兼容与迁移思路
8.1 原生函数确实没有,先别在 T-SQL 里硬解析
如果你还在用 SQL Server 2008、2008 R2,看过前面那些函数后大概会有点难受:ISJSON、JSON_VALUE、OPENJSON 这些一个都用不了。网上有人用 REPLACE、CHARINDEX、SUBSTRING 写了一段“T-SQL JSON 解析”,看起来能跑,但维护成本极高,处理多层嵌套、数组、转义时很容易出错。
我不建议在 2008 R2 的存储过程里用字符串函数手写 JSON 解析。JSON 的格式规则比大多数人想象中复杂,引号转义、Unicode 编码、数组长度差别都会让字符串截取类方案瞬间失效。在这个版本下,准确、稳定、可维护的 JSON 解析应该在数据库之外的业务代码层完成。
8.2 更合理的替代路径:客户端解析或升级版本
如果业务接口确实需要把 JSON 保存到 SQL Server 2008 R2,我的建议是让应用层完成 JSON 解析后再落库。比如 C# 中直接操作对象,然后写入正式字段。这样数据库只负责存储关系型数据,不承担 JSON 解析压力,逻辑也清晰。
csharp复制var order = JsonSerializer.Deserialize<OrderModel>(payload);
using var cmd = new SqlCommand(
"INSERT INTO Orders(OrderId, CustomerName, Status) VALUES(@id, @name, @status)",
conn);
cmd.Parameters.AddWithValue("@id", order.OrderId);
cmd.Parameters.AddWithValue("@name", order.Customer.Name);
cmd.Parameters.AddWithValue("@status", order.Status);
cmd.ExecuteNonQuery();
如果不想改业务代码,还有一种做法是把 JSON 原样保存到 NVARCHAR(MAX) 列里,只用于存档和追溯。等后续把数据库迁移到 SQL Server 2016 以上版本,再补加 ISJSON 约束、计算列索引,或者用 OPENJSON 对历史数据进行回补清洗。先把“能用”跑通,再谈“用好”。
8.3 从老版本升上来时的检查顺序
如果团队已经准备把 2008 R2 升级到新版本,JSON 能力升级只是一方面,更重要的是系统性验证。升级前,先在测试环境把数据库还原到新实例,执行 DBCC CHECKDB 确认物理完整性,再用实际业务脚本跑一轮。检查顺序可以参考:
- 确认实例版本和数据库兼容级别,至少把目标库的兼容级别调整到 130 以上。
- 检查所有使用 JSON 的场景能否用原生函数改写,废弃原先在应用层手工拼 JSON 再入库的逻辑。
- 对原来直接以 NVARCHAR(MAX) 保存 JSON 的表,评估是否需要加 ISJSON 约束。
- 对高频查询的 JSON 字段,设计计算列并测试索引效果。
- 跑一轮典型业务的 IO、CPU 对比,确认升级后性能没有回退。
我能理解很多企业因为历史包袱一直守着一个 2008 R2 实例,但 SQL Server 的 JSON 原生支持确实是 2016 版本之后才能好好用起来的功能。如果业务长期依赖 JSON 数据处理,继续停在老版本要付出的维护成本会越来越高。换个角度想:那些 2022 版才新增的 JSON_OBJECT 与 JSON_ARRAY,也在倒逼整个行业把数据交互方式往更规范的方向上迁移。
对我个人而言,SQL Server 处理 JSON 这件事,真正重要的不是背熟每个函数名,而是建立一套判断逻辑:什么时候把 JSON 拆开变成关系数据,什么时候保存整个 JSON 原文,什么时候给它加计算列索引。这个边界想清楚了,JSON 在 SQL Server 里就不再是“存进去容易、查出来难”的鸡肋,而是扩展表结构、承接上游多变数据的一把好手。
