做SQL Server开发和运维的朋友,前些年应该都有过这种经历:业务方甩过来一份接口返回的JSON数据,让你“帮忙存一下”,或者要你把应用日志里的JSON塞进数据库做分析。那时候要么拆成十几个表慢慢导入,要么干脆用Text字段整段存储,查询里面某个属性全靠LIKE碰运气。SQL Server从2016版本开始内置了整套JSON处理函数,这章笔记就专门把这块能力完整梳理一遍:从最基础的数据类型认知、常用函数拆解,到一个能直接参照的订单场景实战,再加上性能优化思路和平时最容易踩的坑,让后台开发、DBA、数据分析师都能拿起来就用。
1. SQL Server处理JSON的整体设计思路
1.1 SQL Server并没有JSON类型,为什么却能处理JSON
很多从MySQL转过来的朋友会先入为主地认为SQL Server也应该有个JSON字段类型。实际上SQL Server从2016开始采取了一个完全不同的方案:JSON以NVARCHAR字符串形式存储,靠内置函数去解析、校验、查询和生成。官方之所以这么设计,核心考虑是文档型数据在关系型数据库里本质上就是“带结构的字符串”,用通用字符串类型存储,语法上和现有T-SQL完全兼容,对存储引擎、索引机制都不用做大手术。
这个设计带来的直接好处是使用门槛低:你不需要学习一套全新的字段类型规则,建表时写NVARCHAR(MAX)就行,往里面塞JSON字符串就行。代价就是没有MySQL那种原生JSON类型提供的自动校验、自动格式化、专用操作符,所以所有的校验和解析工作都要手动调用函数完成。实际项目里我一般会在写入前用ISJSON做一次检查,避免脏数据进去之后查询时到处报错。
1.2 SQL Server JSON能力全景:函数、操作符与适用场景
把SQL Server的JSON相关能力列一张全景表,后面的细节展开就有坐标系了。核心就是5个函数加一个子句:
| 函数/子句 | 作用 | 典型场景 |
|---|---|---|
| ISJSON | 判断字符串是否为合法JSON | 数据入库时校验、清洗历史脏数据 |
| JSON_VALUE | 从JSON中提取一个标量值(字符串、数字、布尔) | 读取某个字段的值,配合WHERE过滤 |
| JSON_QUERY | 从JSON中提取对象或数组 | 取出整个子对象、数组,或者把子JSON再次交给应用解析 |
| JSON_MODIFY | 修改JSON中的某个值并返回新JSON字符串 | 更新配置项、修改订单扩展信息 |
| OPENJSON | 把JSON数组或键值对解析成行集(表) | 拆解数组做统计、关联查询、导入数据 |
| FOR JSON | 把查询结果集转成JSON文本 | 接口输出、生成配置文档、导出数据 |
这6个能力基本覆盖了JSON的“写入前校验、读取、修改、结构化拆分、聚合生成”全链路。后面所有实操内容,都是围绕这6个能力展开的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心函数细节解析与实操要点
2.1 JSON_VALUE和JSON_QUERY怎么选,别再凭感觉
JSON_VALUE和JSON_QUERY是初学者最容易搞混的一对。官方定义很简单:JSON_VALUE返回标量值,JSON_QUERY返回完整的JSON片段(对象或数组)。但在实操里,很多人经常遇到“JSON_VALUE取数组取不到”“JSON_QUERY取普通字符串返回NULL”这类问题,本质就是没搞懂这两个函数的边界。
我自己总结了一个判断方法:先看你想要的目标数据在JSON文本里是什么形态。如果是键值对里的值,并且这个值是一个普通字符串、数字、真假值,用JSON_VALUE。如果你要取的是一个大括号对象或者中括号数组,用JSON_QUERY。举个直观的例子,数据是{"name":"张三","address":{"city":"上海"},"tags":["VIP","老客户"]},取name用JSON_VALUE,取address整个对象和tags整个数组必须用JSON_QUERY。用JSON_VALUE去取address或者tags只会得到NULL。
JSON_VALUE还有一个常见的坑:它返回的是NVARCHAR类型,如果字段值很长,默认截断长度跟字符串长度有关,超过一定长度会返回NULL。每次我在用JSON_VALUE取大段文本前都会加个LEN判断,或者提前用ISJSON验证一下格式。
2.2 JSON_MODIFY修改JSON的注意事项
JSON_MODIFY是更新JSON字段的主力函数,参数格式是JSON_MODIFY(表达式, 路径, 新值)。最典型的一个坑是:新值如果是个字符串,函数会自动把值序列化成JSON字符串,也就是说会在值两边加引号。你想往JSON数组里追加一个对象,直接写JSON_MODIFY(@json, '$.tags', '[{"id":1}]'),最终得到的是一个带转义引号的字符串,而不是真正的数组。解决办法是新值必须用JSON_QUERY包裹,让它以原始JSON片段身份参与拼接。
JSON_MODIFY一次只能修改一个路径。想在同一个JSON里同时改多个字段,要么写多条UPDATE语句,要么嵌套调用。我写得比较多的是SET句式里嵌套了两三层JSON_MODIFY一条语句搞定。路径支持数组下标,例如$.items[0].name可以修改数组第一个元素的对象属性。但是注意,如果数组元素的下标越界,JSON_MODIFY不会自动创建数组元素,这点在动态拼接时很容易出问题,建议修改前先检查数组长度。
2.3 OPENJSON解析数组和键值对
OPENJSON是JSON转表格的核心函数。它有两种用法:不指定列结构时,它会把JSON数组里的每个元素解析成一行,默认返回三个字段:key、value、type;指定WITH子句时,你可以明确JSON字段到表格列的映射关系。
我平时用得最多的是带WITH子句的写法。假设有一串订单明细JSON数组,每个元素包含商品名、单价、数量,只需要这样写:
sql复制SELECT *
FROM OPENJSON(@orderItems)
WITH (
productName NVARCHAR(50) '$.productName',
price DECIMAL(10,2) '$.price',
quantity INT '$.quantity'
);
OPENJSON配合CROSS APPLY可以横向展开一个字段里的JSON数组,这是SQL Server里做“行转列”分析最顺手的方式。后面3.2节会用一个完整订单表来演示。需要注意,WITH子句里的路径如果写错,字段不会报错,而是返回NULL,这类问题排查起来比较隐蔽,所以我习惯在写之前先把JSON样本放到在线校验工具里确认路径。
3. 完整实操:从建表到JSON查询与生成
3.1 场景设计:一张带JSON扩展字段的订单表
为了让这套笔记可以直接落地,我设计了一个电商订单场景。订单主表保存固定字段:订单号、用户ID、订单状态、下单时间,然后预留一个JSON字段保存灵活多变的扩展信息,包括收货地址、商品明细、买家备注。这样设计的好处是,订单在不同业务阶段可能追加不同的扩展字段,不会因为需求变动频繁改表结构。
建表语句如下:
sql复制CREATE TABLE dbo.Orders (
OrderID INT IDENTITY(1,1) PRIMARY KEY,
OrderNo NVARCHAR(32) NOT NULL,
UserID INT NOT NULL,
Status TINYINT NOT NULL DEFAULT 0,
CreateTime DATETIME NOT NULL DEFAULT GETDATE(),
ExtInfo NVARCHAR(MAX) NULL,
CONSTRAINT UQ_OrderNo UNIQUE (OrderNo)
);
插入几条测试数据时可以在ExtInfo里放完整的JSON字符串,比如:
json复制{
"address": {
"province": "上海市",
"city": "上海市",
"detail": "浦东新区某路某号"
},
"items": [
{ "productId": 1001, "productName": "无线鼠标", "price": 89.00, "quantity": 2 },
{ "productId": 1002, "productName": "机械键盘", "price": 399.00, "quantity": 1 }
],
"remark": "工作日白天配送",
"vip": true
}
这个结构兼顾了对象、数组、标量、布尔值,后面演示的所有函数都能用上。
3.2 查询JSON字段里的标量值、判断存在、拆分统计
先看最基础的查询:从ExtInfo里取出省、市、备注,判断用户是否为VIP。JSON_VALUE配上$路径语法很快就能搞定:
sql复制SELECT
OrderNo,
JSON_VALUE(ExtInfo, '$.address.province') AS Province,
JSON_VALUE(ExtInfo, '$.address.city') AS City,
JSON_VALUE(ExtInfo, '$.remark') AS Remark,
JSON_VALUE(ExtInfo, '$.vip') AS IsVIP
FROM dbo.Orders
WHERE JSON_VALUE(ExtInfo, '$.vip') = 'true';
注意JSON_VALUE返回的布尔值会变成字符串'true'/'false',比较的时候别写成数字1/0。接着做存在性判断,比如统计有备注的订单数:
sql复制SELECT COUNT(*)
FROM dbo.Orders
WHERE JSON_VALUE(ExtInfo, '$.remark') IS NOT NULL;
接下来是拆解items数组做统计。这个场景在日常报表里很常见:把JSON数组拆成行,再按商品分组汇总销量和销售额。用CROSS APPLY OPENJSON实现:
sql复制SELECT
item.productName,
SUM(item.quantity) AS TotalQuantity,
SUM(item.price * item.quantity) AS TotalAmount
FROM dbo.Orders o
CROSS APPLY OPENJSON(o.ExtInfo, '$.items')
WITH (
productId INT '$.productId',
productName NVARCHAR(50) '$.productName',
price DECIMAL(10,2) '$.price',
quantity INT '$.quantity'
) AS item
GROUP BY item.productName
ORDER BY TotalAmount DESC;
这段SQL的关键点是OPENJSON的第二个参数$.items指定了要解析的数组路径,WITH子句定义了列结构。实际跑下来,性能在数据量不大的场景下完全够用,但如果Orders表有几十万行,每条记录里的items都要解析一遍,还是要靠后面的计算列索引来优化。
3.3 用FOR JSON生成接口或导出用的JSON
查询出JSON是一方面,有时候还要把关系表数据生成JSON返回给前端。FOR JSON PATH是最常用的生成方式。比如把订单主表和扩展信息组合成一个完整的订单对象:
sql复制SELECT
OrderNo,
UserID,
Status,
CreateTime,
JSON_QUERY(ExtInfo) AS ExtInfo
FROM dbo.Orders
WHERE OrderNo = 'SO2025010001'
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER;
这里有一个关键点:如果直接用Extended字段名,FOR JSON会把ExtInfo里的JSON字符串再转义一次,前端拿到就变成带反斜杠的字符串了。用JSON_QUERY包裹后,SQL Server会把它当成已经格式化好的JSON片段嵌入,这样输出才是有效的嵌套JSON。这个细节我当时调了很久才明白,属于文档里不一定会强调但实际必踩的坑。
FOR JSON AUTO也值得说一句。AUTO模式会根据JOIN关系自动推断嵌套结构,适合直接从多表关联查询生成JSON,但缺点是嵌套层级不易控制。PATH模式给了你完全控制字段路径的权利,比如'$.address.province' AS Province可以把平铺字段嵌套到对象里。接口交付时我基本都用PATH模式。
3.4 修改JSON字段内容:更新、追加、删除
订单状态流转时经常要往ExtInfo里追加操作记录。比如用户申请售后,需要加一个afterSale字段。一次性更新可以使用JSON_MODIFY:
sql复制UPDATE dbo.Orders
SET ExtInfo = JSON_MODIFY(ExtInfo, '$.afterSale', JSON_QUERY('{"reason":"质量问题","status":"pending"}'))
WHERE OrderNo = 'SO2025010001';
删除字段的办法比较隐蔽:JSON_MODIFY的路径值传NULL即可,例如JSON_MODIFY(ExtInfo, '$.remark', NULL)会移除remark字段。追加数组元素时要先检查数组是否存在,如果原JSON没有tags字段,直接写了$.tags[0]会失败,我一般会先用JSON_QUERY(ExtInfo, '$.tags')判断,不存在就先初始化一个空数组。
多条修改可以用嵌套JSON_MODIFY,比如同时追加两个字段:
sql复制SET ExtInfo = JSON_MODIFY(
JSON_MODIFY(ExtInfo, '$.afterSale', JSON_QUERY('{"reason":"质量问题"}')),
'$.handler', 'ops_user_01'
);
嵌套太深可读性会下降,但它是单语句更新多个字段最简洁的方式。如果修改逻辑很复杂,也可以在应用层读出JSON、改好再整段写回,两种方案各有取舍,我个人偏向应用层做复杂变更,SQL层只做简单的单点修改。
4. SQL Server 2022带来的JSON新特性与老版本兼容
4.1 JSON_OBJECT和JSON_ARRAY让构造JSON更方便
SQL Server 2022终于补上了两个语法糖:JSON_OBJECT和JSON_ARRAY。之前想构造一个简单的JSON对象,必须手写字符串或者用FOR JSON再处理,有了这两个函数可以直接在T-SQL里声明式地构造:
sql复制SELECT
JSON_OBJECT('orderNo': OrderNo, 'user': UserID) AS OrderInfo,
JSON_ARRAY(OrderNo, UserID) AS OrderArray
FROM dbo.Orders
WHERE OrderID = 1;
好处是自动处理引号转义、NULL值排除(可通过ABSENT ON NULL控制),不会再出现手工拼字符串漏转义导致JSON格式错误的问题。2022还新增了JSON_PATH函数,用来检查JSON路径是否存在对应值,底层会走更高效的路径查找。
4.2 把JSON字符串转成表格的另一个入口:OPENJSON的增强
以往OPENJSON只能解析JSON字符串,2022版本后新增了可以指定行过滤的能力,但核心用法大致不变。真正值得关注的是JSON数据约束的增强:你可以在建表时给NVARCHAR字段加CHECK约束,直接校验它必须是合法JSON。注意这个能力2016就有,只是很多人没主动用:
sql复制ALTER TABLE dbo.Orders
ADD CONSTRAINT CK_Orders_ExtInfo_IsJSON CHECK (ISJSON(ExtInfo) = 1);
有了这个约束,应用层漏传格式错误JSON时,数据库会在写入阶段就把脏数据拦下来,比应用层自己校验靠谱得多。
4.3 生产环境还是2016/2019,老版本怎么办
规划功能时不能假设所有环境都是最新版。我接触过的生产环境里,SQL Server 2016、2017、2019依然占绝大多数。好消息是,上面章节里所有核心功能(ISJSON、JSON_VALUE、JSON_QUERY、JSON_MODIFY、OPENJSON、FOR JSON)在2016版本就已经全部存在,2017/2019只是修bug和提升性能。所以只要目标实例不低于2016,这套方案都可以原样使用。
如果遇到SQL Server 2014甚至更老的版本,那确实没有内置JSON函数。我当时的替代方案是写CLR聚合函数或者直接在应用层解析。这里我的建议始终是:尽早推动升级到2016以上,为了一个JSON功能单独引入CLR,运维成本和稳定性风险都不划算。
5. 常见问题与排查技巧实录
5.1 JSON_VALUE返回NULL的常见原因对照表
JSON查询返回NULL的排查在开发期非常频繁。我把工作中遇到的情况整理成了速查表,基本能覆盖90%的问题:
| 现象 | 常见原因 | 排查方法 |
|---|---|---|
| JSON_VALUE取对象或数组返回NULL | 函数用错,应该用JSON_QUERY | 看目标值是{}或[]就换JSON_QUERY |
| 路径写错,返回NULL | $路径少写点,路径大小写不匹配 |
用在线JSONPath工具验证路径 |
| 目标值本身是JSON null | 数据里就是null值 | 调整业务逻辑,区分NULL和字符串'null' |
| JSON字符串有BOM或前后空格 | 入库前没清干净 | 用LTRIM(RTRIM(...))或REPLACE去BOM |
| 字段太长被截断 | JSON_VALUE返回类型限制 | 换成JSON_QUERY或改用NVARCHAR(MAX)转换 |
JSON_VALUE对字符串长度确实有限制,网上很多帖子说返回NULL是长度问题,实测下来大文本字段干脆用JSON_QUERY取出完整文本再在应用层处理,反而省心。
5.2 路径语法报错:$.和$[别混用
JSON路径是新手报错重灾区。路径以$开头,对象属性用点,数组下标用方括号。例如解析{"items":[{"name":"a"}]}里的第一个商品名,正确路径是$.items[0].name。很多人会写成$.items[0].name以外的变体,比如items[0].name少了$,或者$.items.[0].name多了一个点,都会导致解析失败或者返回NULL。还有一点容易忽视:属性名里有特殊字符(空格、减号、中文),路径里要用双引号包起来,比如$."user-name"。这个语法细节不做一次项目基本记不住,建议遇到路径解析失败时先检查属性名和特殊字符。
5.3 JSON中的number超范围怎么处理
热搜词里有人问“json中number超范围了,怎么处理”,这个问题在SQL Server场景里很典型。JSON本身对数字的范围没有定义,但SQL Server里的DECIMAL、BIGINT、FLOAT有明确精度限制。比如应用层把Java的Long型雪花ID序列化成JSON,数字长度超过15位,SQL Server用FLOAT解析就会丢精度。解决方案有两种:
第一种,在OPENJSON的WITH子句里把对应列定义为NVARCHAR,先把数字当字符串取出,再显式转换。第二种,在应用层序列化时就把长整数转成字符串。我更推荐第二种,因为一旦在SQL侧转字符串,后续做范围比较、聚合计算又要再CAST一次,效率不高。真正处理高精度数值时还要注意,JSON_VALUE返回的是NVARCHAR,用它跟DECIMAL比较时SQL Server会隐式转换,这个转换可能产生舍入误差,建议统一用CAST包裹。
5.4 OPENJSON解析性能差,怎么优化
OPENJSON写起来很爽,但如果不加控制,在大表上做全表扫描再逐行解析,性能会非常难看。一个容易忽略的要点:JSON解析函数和普通函数一样,直接写在WHERE条件里会导致索引失效。比如WHERE JSON_VALUE(ExtInfo, '$.vip') = 'true',就算在ExtInfo上有索引也用不上,执行计划大概率是全表扫描。
我的处理方案是新建一个计算列,把需要过滤的JSON字段提取出来,然后在这个计算列上建索引:
sql复制ALTER TABLE dbo.Orders
ADD IsVIP AS CAST(JSON_VALUE(ExtInfo, '$.vip') AS BIT) PERSISTED;
CREATE INDEX IX_Orders_IsVIP ON dbo.Orders(IsVIP);
这样查询条件直接走计算列,性能提升非常明显。如果是全文检索需求,SQL Server的全文索引不支持JSON字段内部结构,只能用计算列或拆表方案。日常多条件组合过滤时,我建议优先把高频查询的JSON字段都提升为持久化计算列,这比依赖函数索引(SQL Server本身就不支持函数索引)靠谱得多。
6. 性能与最佳实践:什么时候用JSON字段,什么时候拆表
6.1 关系模型和JSON模型的边界怎么划
JSON不是万能的,在SQL Server里更不能因为它好用就放弃关系建模。我给自己定的判断标准很简单:属性是否需要参与JOIN、WHERE过滤、GROUP BY、聚合计算?如果需要,就设置为正式列;如果只是偶尔读取展示、或者结构经常变化,才放进JSON字段。换句话说,JSON字段适合放“低频查询的高灵活度半结构化数据”,不适合放核心业务字段。
这里举两个反面案例。第一个,把用户手机号、邮箱作为JSON键值存入ExtInfo,结果查询时每个条件都要JSON_VALUE,SQL又丑又慢。第二个,把订单金额这类参与统计的字段也塞进JSON,结果报表上要SUM(CAST(JSON_VALUE(...) AS DECIMAL)),白白增加解析开销。正确做法是:业务核心字段用正式列,扩展属性用JSON。这个边界线越清晰,后期维护越省心。
6.2 JSON数据的写入和更新成本
JSON字段的更新机制要特别注意:SQL Server的UPDATE是针对整行数据,JSON_MODIFY看起来是修改一个字段,实际SQL Server会把整个JSON字符串读出来、修改后再整体写回去。这意味着JSON字段越大,更新成本越高。如果JSON里有大量日志类、流水类数据,每次只追加一小段,性能和存储消耗都会慢慢变大。
我的一般建议是:把高频追加的日志型数据从JSON字段里拆出去,单独建流水表,JSON只保留低频变化的配置型数据。如果确实要在一个字段里持续追加数组,可以考虑定期归档旧数据,或者把数组拆到子表再关联,避免JSON体无限膨胀。
6.3 索引策略:计算列、持久化与覆盖索引组合
除计算列索引外,还可以用包含列的方式优化查询回表。例如经常按省份统计订单,可以在计算列Province上建索引,同时把需要展示的OrderNo、CreateTime作为包含列:
sql复制ALTER TABLE dbo.Orders
ADD Province AS JSON_VALUE(ExtInfo, '$.address.province') PERSISTED;
CREATE INDEX IX_Orders_Province ON dbo.Orders(Province)
INCLUDE (OrderNo, CreateTime);
这样按省聚合时避免回表,扫描成本更低。注意计算列必须是持久化(PERSISTED)才能在计算列上建索引,如果计算列涉及的函数本身不是确定性的,还得小心是否允许持久化。JSON_VALUE是确定性函数,只要路径是常量就可以持久化,这点通常没问题。
6.4 我实际用JSON字段踩过的三个坑
第一个坑是直接拿JSON字段做模糊查询。业务方说要搜订单备注里包含“加急”的记录,我最初用LIKE '%加急%',结果因为JSON里还有别的字段也包含“加急”,误命中了很多无关订单。后来改成JSON_VALUE(ExtInfo, '$.remark') LIKE '%加急%',准确率才正常。
第二个坑是JSON字段排序。ORDER BY JSON_VALUE(ExtInfo, '$.price')时,因为JSON_VALUE返回的是NVARCHAR,排序结果按字符串排序,10会排在9前面。解决办法是CAST成DECIMAL再排序,或者用计算列。
第三个坑是NULL和JSON null的区分。业务系统往JSON里写"vip": null,JSON_VALUE提取出来是字符串"null",不是SQL的NULL,导致IS NULL判断失效。后来统一约定:JSON里不存在的字段和值为null的字段,在业务语义上必须区分清楚,否则查询条件会有莫名其妙的漏数据。
6.5 后续扩展方向参考
这套笔记讲到这个深度,已经可以解决日常开发里绝大多数JSON处理需求。后续如果你想继续深化,建议从三个方向扩展:一是把计算列索引和索引视图结合起来,在复杂统计场景获得更好性能;二是研究JSON数据与其他数据源的互操作,比如把SQL Server查询结果转成JSON后直接喂给下游Java、Python服务;三是关注每年SQL Server新版本对JSON能力的增强,2022版的JSON_OBJECT、JSON_ARRAY、JSON_PATH和更严格的JSON约束确实让代码简洁了不少。
我在实际项目里的体会是:JSON支持不是为了让SQL Server变成一个文档数据库,而是给关系模型一个缓冲地带,用它承接多变的需求,同时保住核心数据的关系完整性和查询性能。用对了地方,它能让开发效率提升一个档次;用错了地方,它也会成为性能黑洞。重点就是掌握每个函数的边界、理解路径语法、控制好数据校验和索引优化,这套方法在任何版本的SQL Server上都能发挥价值。
最后再分享一个小技巧:开发阶段一定要建立一套样本JSON数据并用ISJSON校验过再写代码,这样能帮你隔离问题,也不会把“应用层拼错JSON”和“SQL函数用法错误”这类问题混在一起。先构造最小化复现,再逐步加字段,比一上来就搞全字段大JSON要高效得多。
