news 2026/10/31 2:44:38

SQL Server旧版动态SQL拼接JSON分页实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQL Server旧版动态SQL拼接JSON分页实战

简介:一份面向数据库开发、后端接口与报表实现人员的SQL Server技术文档,主题是用T-SQL把关系表查询结果自动组织成JSON,解决手工拼装列表数据、分页返回格式不统一的问题。内容围绕系统视图读取表结构、ROW_NUMBER()分页、动态SQL拼接键值对展开,并说明将JSON写入数据表、供前端接口读取的基本用法,适合需要输出分页JSON或改造轻量接口的开发者参考。压缩包仅含1个docx文件,约30KB,篇幅紧凑,重点放在脚本模板、参数组织与字段转换处理上,便于按需检索和复用。已有1146人学习,读者可从中了解动态SQL生成JSON的实现脉络、分页区间与总数字段的组织方式,以及空值、转义等细节的排查方向。

1. 一个查询把表变成 JSON:这套 T-SQL 脚本适合谁

前端同学扔过来一份字段清单,说接口要返回{"MerbarCode":"A001","MerName":"苹果","rn":"1","total":"137"}这种结构;数据库那边是一台老 SQL Server 实例,FOR JSON语法用不了,改动运维流程又不现实。于是需求变成:在 SQL 层直接把查询结果拼成 JSON 字符串,顺带把分页一起做掉。这份脚本干的就是这件事,把「列名转 key、行转对象、区间裁剪、总数统计」四步压进一段 T-SQL,一次EXEC拿到能直接丢给前端的串。

适合两类人:维护老版本 SQL Server、又要给新前端供数据的开发者;以及想搞明白动态 SQL、系统视图、窗口函数怎么配合起来的中级读者。不适合的情况也说清楚——库已经上到 SQL Server 2016 及以上,直接写FOR JSON PATH,没必要拼字符串。

2. 动态 SQL 拼装骨架:从 SYS.SYSCOLUMNS 拿到列名到 select 语句成型

整段脚本的最有价值的部分在这一层:它不针对某一张表写死字段,而是用系统视图把列名读出来,再拼进一条待执行的 SQL 字符串。理解了这层,后面换表、加字段、改输出格式都是机械操作。

2.1 先建一张能跑通的测试表

后面所有演示都基于这张表,字段特意包含了 NULL 和日期,因为这两个类型是拼接过程里最容易翻车的地方,先埋进去,排查章节才有对照。

IF OBJECT_ID('dbo.Merchandise') IS NOT NULL DROP TABLE dbo.Merchandise; GO CREATE TABLE dbo.Merchandise ( MerbarCode varchar(20) NOT NULL PRIMARY KEY, -- 商品条码,分页排序键 MerName nvarchar(50) NULL, -- 商品名,故意允许 NULL MerPrice decimal(10,2) NULL, -- 价格,验证数值转字符串 MerCreateDate datetime NULL -- 入库时间,验证日期格式 ); GO INSERT INTO dbo.Merchandise VALUES ('A001', N'苹果', 3.50, '2023-01-05 08:30:00'), ('A002', N'香蕉', 2.80, '2023-02-11 09:15:00'), ('A003', NULL, 9.90, '2023-03-20 10:00:00'), ('A004', N'橙子', 5.20, NULL); GO SELECT * FROM dbo.Merchandise;

建完之后先别急着跑原脚本,用SELECT *确认四行数据在,并且MerName和MerCreateDate各有一个 NULL。这一步看着多余,但后面调 JSON 拼接时,没有 NULL 行你就发现不了isnull把空值变成空字符串的问题,等到上线才发现前端拿到的null全变成了"",那种返工更贵。

2.2 用 OBJECT_ID 锁定表:SYS.SYSCOLUMNS 到底返回了什么

脚本里那句WHERE ID = OBJECT_ID(@TableName)是整套动态化的入口。OBJECT_ID在当前数据库上下文里按名字解析对象,返回对象 id;SYS.SYSCOLUMNS里的id列就是同一个东西,name是列名,colid是列在表里的序号。这里有一个很多人没意识到的点:这个查询没有ORDER BY,返回行的顺序是不保证的,而行的顺序决定了 JSON 里字段出现的顺序。

先单独把这段跑一遍,看清楚它到底吐出什么:

-- 只看列名和列序号,确认取到的列和你建表时一致 SELECT c.colid, c.name FROM SYS.SYSCOLUMNS c WHERE c.id = OBJECT_ID('dbo.Merchandise') ORDER BY c.colid;

colid从 1 开始,按建表顺序递增。原脚本没写排序,理论上字段顺序可能乱,实践中在小表上多数时候能保持建表顺序,但这属于运气,不是保证。如果你下游有按顺序消费 JSON 的逻辑(比如导出成 CSV 列位对齐),务必自己补上ORDER BY colid,这行是免费的。

还有个坑要提前说:OBJECT_ID('dbo.Merchandise')解析的是当前数据库,脚本一旦在别的库上下文里执行就会返回 NULL,WHERE ID = NULL一行都取不到,@sql拼出来是半截字符串,最后EXEC报个语法错误,看不出根因。跨库场景必须写成三段式库名.dbo.表名。

2.3 拼字段片段:每一列怎么变成"列名":"值",

核心是下面这段,原文分两次赋值,这里合并成一次读,语义不变。注意里面那层isnull(cast(...))是拼接期的表达式,不是执行期的,它会被原样写进待执行的 SQL 字符串里,等EXEC时才真正对每行求值。

DECLARE @TableName varchar(50) = 'dbo.Merchandise'; DECLARE @sql varchar(3000); -- 待执行的 SQL 串,初始为 NULL DECLARE @OrderBy varchar(20) = 'MerbarCode'; -- 第一行带上前缀,之后每行追加,靠 isnull 实现"首行初始化" SELECT @sql = ISNULL(@sql, 'WITH paging AS ( SELECT REPLACE(''{''+ ') + '''"' + name + '":"'' + ISNULL(CAST(' + name + ' AS varchar), '''') + ''",''' FROM SYS.SYSCOLUMNS WHERE id = OBJECT_ID(@TableName) ORDER BY colid; SELECT @sql AS 拼接中间结果;

跑完看一眼拼接中间结果,你会得到一段形如:

WITH paging AS ( SELECT REPLACE('{'+ '"MerbarCode":"'+ISNULL(CAST(MerbarCode AS varchar),'')+'",' + '"MerName":"'+ISNULL(CAST(MerName AS varchar),'')+'",' + ...

这里有几个容易看漏的细节。'''"'这种四五个引号连在一起的写法,本质是在拼一个单引号、一个双引号、一个双引号、一个单引号,也就是让生成的 SQL 里出现"列名"这样的字面量。数引号是这项工作的日常,别指望一次数对,跑出来打印比对最靠谱。ISNULL(CAST(x AS varchar), '')双保险:先转字符串,再把 NULL 兜成空串,保证拼接表达式不会因为某行某列为 NULL 而整体变 NULL——字符串拼接里只要有一个操作数是 NULL,结果就是 NULL,整行 JSON 直接消失。

提示:CAST(x AS varchar)不写长度时默认 30。nvarchar(200)的备注字段会被无声截断到 30 个字符,只留下半句话。所有CAST都写成varchar(4000)或按列实际长度给足。

2.4 为什么原作者要先 isnull(@sql,'...') 再往后 + 拼接

SELECT @sql = 表达式 FROM ...这个写法在 T-SQL 里是"逐行给变量赋值":查询每命中一行,就用当前行算一遍右边的表达式,把结果赋给@sql,下一行再基于新的@sql继续算。所以第一行时@sql还是 NULL,ISNULL(@sql, '前缀')取出前缀再拼第一个字段片段;从第二行起@sql已有内容,ISNULL直接返回原值,等价于在后面追加。

这套写法很省事,但两种情况下会坑人。一是SELECT @sql = ...一旦加了ORDER BY又混入聚合,赋值顺序会变得难以推断;二是这种"边扫边累加"的写法没有官方顺序保证,所以我在 2.2 里强调补ORDER BY colid——至少要保证列顺序稳定,否则生成的 JSON 字段顺序每次都可能不一样,做 diff 对拍时会怀疑人生。

拼完之后@sql是一段未闭合的字符串,最后那个字段后面还挂着一个多余的逗号。原文用REPLACE(..., ',', '')去处理这个尾巴,思路上是想删掉末尾逗号,但REPLACE是全局替换,会把所有字段之间的分隔逗号一起干掉,拼出来的 JSON 缺分隔符,前端JSON.parse直接抛异常。这是整套脚本里最需要警惕的一处,改法放到第 5 章细讲,稳妥做法是在拼接时就少拼一个逗号,或者用STUFF按位置精确删除首字符。

3. 分页与包装层:ROW_NUMBER、total 字段和 paging 这个 CTE 怎么搭

字段片段只解决了"一行怎么变对象",真正让这段脚本能当接口用的是外面那层WITH paging AS (...):它把原始查询、行号、总数三样东西装进一个临时结果集,再按行号区间裁剪。这一层决定了分页的正确性和性能。

3.1 CTE paging 的结构:sourceTable、rn、total 三件套

把外层补全,拼接产物大致长这样(去掉REPLACE的争议写法,换成更直白的版本):

WITH paging AS ( SELECT '{' + '"MerbarCode":"' + ISNULL(CAST(MerbarCode AS varchar(4000)), '') + '",' + '"MerName":"' + ISNULL(CAST(MerName AS varchar(4000)), '') + '",' + '"MerPrice":"' + ISNULL(CAST(MerPrice AS varchar(4000)), '') + '",' + '"MerCreateDate":"' + ISNULL(CAST(MerCreateDate AS varchar(4000)), '') + '"' AS sourceTable, -- 拼好的单行 JSON 主体 ROW_NUMBER() OVER (ORDER BY MerbarCode) AS rn, -- 全表范围内的行号 (SELECT COUNT(MerbarCode) FROM dbo.Merchandise) AS total -- 总条数,每行重复 FROM dbo.Merchandise ) SELECT * FROM paging WHERE rn BETWEEN 1 AND 20;

sourceTable是每行对应的 JSON 主体,注意最后一个字段后面不要逗号,用+ '"'收尾就干净了,REPLACE那一步可以直接省掉。rn是窗口函数算出来的行号,total是标量子查询,会被算一次然后广播到每一行。三列一起返回,前端拿到之后自己决定是只要sourceTable还是连rn、total一起用。

这里有个设计上的取舍值得说:total放成每行重复的列,前端取第一条的total就行,逻辑简单;代价是结果集里多传了 N 份同样的数字。分页大小 20 的时候无所谓,一次拉 1000 行就有 1000 份冗余了。

3.2 ROW_NUMBER 的 ORDER BY 为什么要落到列名而不是表达式

ORDER BY后面跟的是变量@OrderByColumn拼进去的列名,形如ROW_NUMBER() OVER (ORDER BY MerbarCode)。这里不能写ORDER BY 1之类的位置序号,因为窗口函数的ORDER BY不认位置序号;也不能写别名,因为同一层SELECT里的别名在窗口函数中不可见。

更要紧的是排序键的选择会直接影响分页稳定性。用MerbarCode这种主键或唯一列没问题,一旦换成MerPrice这种有重复值的列,ORDER BY相同值的行在物理上是任意顺序的,翻页时就可能同一行出现两次、另一行一次都不出现。这不是脚本的 bug,是 SQL 的语义如此。

-- 反面示例:价格有重复,翻页可能漏行 ROW_NUMBER() OVER (ORDER BY MerPrice) AS rn -- 稳妥做法:排序键后面补一个唯一列兜底 ROW_NUMBER() OVER (ORDER BY MerPrice, MerbarCode) AS rn

我一般的习惯是,只要能保证排序唯一性,就一定补第二个键,哪怕业务上看起来不需要。翻页漏数据的排查成本极高,因为测试环境数据少、复现不出来,用户那边却一直在丢订单行。

3.3 用 rn between 做区间裁剪,注意首尾行号是闭区间

WHERE rn BETWEEN @CurPageFirstRow AND @CurPageLastRow是闭区间,两端都包含。页码和每页条数的换算:

参数含义公式pageNo=2, pageSize=20 时的值
@CurPageFirstRow当前页第一行的行号(pageNo - 1) * pageSize + 121
@CurPageLastRow当前页最后一行的行号pageNo * pageSize40
@OrderByColumn排序列名由调用方传入,必须是白名单内的列MerbarCode
@TableName查询表名三段式或当前库内对象名dbo.Merchandise

BETWEEN换成>= AND <会变成左闭右开,第 20 条就被排除了,这是很常见的差一错。行号从 1 开始,页大小 20,第 1 页覆盖 1 到 20,第 2 页 21 到 40,不多不少。

边界情况要单独测三个:pageNo=1(首行 = 1,看BETWEEN下界是否被正确包含)、pageNo超过总页数(应返回空结果集而不是报错)、pageSize极大(比如请求pageSize=100000,能把整表拖出来,接口层必须做上限校验)。

3.4 total 子查询的两种写法与代价

原文用(SELECT COUNT(@OrderByColumn) FROM @TableName) AS total做标量子查询。这写法每次执行都要对整表数一遍,不分页也要数。小表无所谓,几十万行以上就能感觉到延迟,因为翻页操作本来只需要扫 20 行,结果被这个 count 拖成全表扫描。

常见的两种改法:

-- 写法 A:窗口聚合,一次扫描同时得到行号和总数 COUNT(*) OVER () AS total -- 写法 B:拆成两条语句,前端按需请求总数 SELECT COUNT_BIG(*) FROM dbo.Merchandise;

写法 A 的好处是省一次扫描,坏处是OVER ()会强制物化整个结果集再做区间裁剪,数据量大时内存压力明显;写法 B 把总数变成可选调用,前端只在页码变化或者过滤条件变化时请求一次,日常翻页根本不用带总数。我在实际项目里默认用 B,接口拆成list和count两个,前端缓存总条数,体验上几乎没差别,数据库那边轻松很多。

注意:COUNT(列名)会跳过该列的 NULL 行,COUNT(*)不会。原文用COUNT(@OrderByColumn),如果排序列允许 NULL,总数会偏小,分页算出来的总页数就少了,最后一页数据取不全。

4. 参数赋值到执行:让脚本真正跑通的三种姿势

前面拼的是字符串,真正让它变成结果集的是最后那句EXEC。这一步决定脚本能不能挂进生产调用链,也决定了你能不能快速定位问题。

4.1 三个参数的算法与边界

参数赋值本身很简单,但算错一个数就整页偏。给一份可以直接抄进去的赋值段:

DECLARE @TableName varchar(50) = 'dbo.Merchandise'; DECLARE @OrderByColumn varchar(20) = 'MerbarCode'; DECLARE @PageNo int = 2; -- 页码,从 1 开始 DECLARE @PageSize int = 20; -- 每页条数 DECLARE @CurPageFirstRow int; DECLARE @CurPageLastRow int; -- 入参兜底:页码小于 1 视为 1,页大小限制在 1~200 IF @PageNo < 1 SET @PageNo = 1; IF @PageSize < 1 OR @PageSize > 200 SET @PageSize = 20; SET @CurPageFirstRow = (@PageNo - 1) * @PageSize + 1; SET @CurPageLastRow = @PageNo * @PageSize; SELECT @CurPageFirstRow AS 首行, @CurPageLastRow AS 末行;

@PageSize的上限校验不是可选项。只要这个值能被外部传进来,攻击者或者写错的前端就能传2147483647,一次把整表拉成 JSON 串,内存和网络双爆。我见过一次线上事故就是分页参数没做上限,某个爬虫把接口当全量导出口用,数据库连接池瞬间打满。

@PageNo为 0 或负数时,@CurPageFirstRow会算出 0 或负数,BETWEEN下界虽然仍能取到第 1 行,但@CurPageLastRow可能小于@CurPageFirstRow,结果直接为空,前端看到空白页却不知道为什么。所以入参归一化要放在公式之前。

4.2 三种执行姿势:SSMS 直接跑、EXEC()、sp_executesql

最直接的验证方式是把拼好的串打印出来,复制到新窗口手跑。这一步看着笨,但每次改拼接逻辑都值得做一遍:

-- 调试期常用:先看,再执行 PRINT @sql; -- 超过 4000 字符会被截断,长脚本用 SELECT @sql -- EXEC (@sql);

EXEC(@sql)是原文的用法,简单直接。它的硬限制是:括号里只能放一个字符串变量或者拼接表达式,且变量若声明为varchar(3000),拼接过程本身就先把结果截断了——表宽一点、字段多一点就超,报错通常是"接近 'xxx' 处有语法错误",看报错完全猜不到是长度问题。改法是把@sql声明成nvarchar(max)。

DECLARE @sql nvarchar(max); -- 别用 varchar(3000),宽表必翻车

sp_executesql是另一种执行方式,支持参数化,把外部传入的值当参数而不是拼进字符串,天然免疫注入:

DECLARE @sql nvarchar(max); DECLARE @First int = 1, @Last int = 20; SET @sql = N'SELECT * FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY MerbarCode) AS rn FROM dbo.Merchandise) t WHERE rn BETWEEN @f AND @l'; EXEC sp_executesql @sql, N'@f int, @l int', @f = @First, @l = @Last;

表名和排序列没法参数化,只能拼字符串,这俩必须走白名单校验。具体的白名单写法放在第 6 章。执行前加一句SET NOCOUNT ON,避免(4 行受影响)这类消息混进结果集,某些驱动会把它们当额外结果集读走,导致前端拿到的 data 是undefined。

4.3 验证输出:把返回串丢给 Python json.loads 做一次对拍

眼睛看 JSON 串是最不可靠的验收方式,逗号少一个、引号多一个都能骗过肉眼。写个十几行的脚本对拍,成本很低:

# pip install pyodbc import json, pyodbc conn = pyodbc.connect( "DRIVER={ODBC Driver 17 for SQL Server};" "SERVER=127.0.0.1;DATABASE=DemoDb;UID=sa;PWD=YourStrongPwd" ) cur = conn.cursor() cur.execute("EXEC dbo.usp_TableToJsonPage @TableName=?, @PageNo=?, @PageSize=?", 'dbo.Merchandise', 1, 20) bad = 0 for row in cur.fetchall(): body = row[0] # sourceTable 列 try: obj = json.loads(body) # 单行 JSON 必须是合法对象 assert isinstance(obj, dict), "顶层必须是 object" except Exception as e: bad += 1 print("解析失败:", e, "原始串:", body) print("失败行数:", bad)

这段脚本的价值在于它是"黑匣子探测器":拼接层出的任何引号、逗号、转义问题,都会在json.loads这一行暴露,而且带原始串,比EXEC报语法错误好定位一百倍。跑通之后再补一条对拍:用FOR JSON PATH生成同一份数据,两边json.loads后比较字典是否相等(字段顺序无关),能一次性验证字段名、值、NULL 处理三件事。

4.4 表名、排序列换成变量后的动态调用

把整段逻辑包进存储过程后,调用就变成一行:

EXEC dbo.usp_TableToJsonPage @TableName = 'dbo.Merchandise', @OrderByColumn = 'MerbarCode', @PageNo = 2, @PageSize = 20;

这里传的@TableName和@OrderByColumn是字符串,会被拼进动态 SQL,所以过程内部必须校验它们是不是合法对象名和白名单列。不校验的话,@TableName = 'Merchandise; DROP TABLE Users--'这种输入能直接执行——虽然varchar(50)长度有限,但足够塞进一条短语句。校验写法在 6.1。

5. 避坑与排查:五条真踩过的记录

这段脚本能在测试库跑通,不代表能在生产跑通。下面五条都是我或者同事在类似拼接方案上真实翻过的车,按"现象 → 原因 → 解决"整理,遇到问题可以先对着扫一遍。

5.1 报「接近 'xxx' 处有语法错误」,但打印出来的 SQL 看着没问题

现象:EXEC直接抛语法错误,把@sql用SELECT打出来,肉眼检查括号引号都对。

原因:@sql声明成varchar(3000),拼接中途被截断,打印出来的其实是前 3000 个字符,看着完整,实际执行的是半截语句。表越宽越容易触发,nvarchar字段名一多,3000 字符根本不够。

解决:把声明改成DECLARE @sql nvarchar(max),EXEC(@sql)支持nvarchar(max)。同时把调试打印改成SELECT DATALENGTH(@sql) AS 字节数, @sql,先看长度再看内容,长度对不上就是截断。

5.2 前端报 JSON 解析失败,返回串中间少逗号

现象:JSON.parse抛Unexpected token,拿到的串长这样{"A":"1""B":"2"},字段之间没有逗号。

原因:原文用REPLACE(拼接结果, ',', '')想删掉末尾多余逗号,但REPLACE是全局替换,把字段间所有分隔逗号一起删了。

解决:别用REPLACE去尾。拼接时最后一个字段直接以+ '"'收尾不补逗号,或者在列表头再补逗号,用STUFF(串, 1, 1, '')删掉第一个字符。如果坚持末尾留逗号,就用LEFT(串, LEN(串) - 1)精确切一个字符,位置明确,不会误伤。

5.3 NULL 变成了空字符串,前端条件判断全错

现象:数据库里MerName明明是 NULL,前端if (row.MerName === null)永远不进分支,页面上显示成空白而不是「未填写」。

原因:拼接表达式里用了ISNULL(CAST(MerName AS varchar), ''),NULL 在拼接期就被替换成空串,JSON 里输出的是"MerName":"",不是null。

解决:区分两种语义。拼接表达式里的ISNULL是为了防止整行变 NULL(必须保留),但值层面的 NULL 要显式输出成字面量null,写法是CASE WHEN MerName IS NULL THEN 'null' ELSE '"' + 转义后的值 + '"' END。升级到 SQL Server 2016+ 的话,FOR JSON PATH, INCLUDE_NULL_VALUES一行搞定。

5.4 字符串里带双引号,整个 JSON 结构被截断

现象:商品名里有一个双引号(比如12"屏),这一行的 JSON 少了一半,后面的字段全丢,json.loads报错位置正好在那个引号。

原因:双引号是 JSON 的字符串定界符,值里出现未转义的双引号会提前闭合字符串。原文只做了CAST和ISNULL,没有任何转义。

解决:拼接时对值做两次替换,反斜杠先转(\→\\),双引号后转("→\"),顺序不能反,否则会把刚转出来的反斜杠再转一次。FOR XML PATH或FOR JSON PATH会自动处理转义,自己拼字符串就必须手工兜。

-- 手拼时必须补的转义,顺序不能换 REPLACE(REPLACE(ISNULL(CAST(MerName AS varchar(4000)), ''), '\', '\\'), '"', '\"')

5.5 只能查当前库,换个库跑就返回空结果集

现象:脚本在 A 库跑得好好的,复制到 B 库对同样的表名执行,EXEC报语法错误,打印出来的 SQL 停在WITH paging AS ( SELECT。

原因:OBJECT_ID('Merchandise')在 B 库上下文里解析不到对象,返回 NULL,WHERE id = NULL一行都没匹配,@sql只有前缀没有字段片段,EXEC当然失败。

解决:表名统一传三段式库名.dbo.表名,或者执行前显式USE 目标库。另外SYS.SYSCOLUMNS是兼容视图,新版本建议换成sys.columns加sys.types,能顺便拿到字段类型做更细的格式化(比如日期统一转yyyy-MM-dd HH:mm:ss),可读性也比连续四五个单引号好得多。

6. 二次开发:把它改成一个能进生产库的通用存储过程

拼串方案的天花板在安全和可维护性,不在功能。要把它放进生产库,我会做三件事:把表名和排序列收进白名单、把执行方式换成sp_executesql、把拼字符串换成FOR XML PATH或FOR JSON PATH。

6.1 白名单 + sp_executesql:把注入面收掉

表名和排序列没法参数化,唯一稳妥的做法是校验。校验不通过直接RAISERROR退出,不要指望调用方自觉。

-- 排序列白名单:只允许真实存在的列,杜绝拼接任意文本 IF NOT EXISTS ( SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(@TableName) AND name = @OrderByColumn ) BEGIN RAISERROR('排序列 %s 不在表 %s 中,拒绝执行', 16, 1, @OrderByColumn, @TableName); RETURN; END -- 表名白名单:只允许 dbo 架构下的普通用户表 IF NOT EXISTS ( SELECT 1 FROM sys.objects WHERE object_id = OBJECT_ID(@TableName) AND type = 'U' AND schema_id = SCHEMA_ID('dbo') ) BEGIN RAISERROR('目标对象不是 dbo 架构下的用户表,拒绝执行', 16, 1); RETURN; END

把这两段放在过程最前面,后面的拼接逻辑就不用再操心来源。行号和页大小走sp_executesql参数,只有表名和列名是拼进去的,而它们已经过系统视图校验,注入面基本收干净了。

6.2 用 FOR XML PATH / FOR JSON PATH 重写拼串

如果版本允许,直接上FOR JSON PATH,转义、NULL、类型格式化全部由引擎处理,代码量能砍掉三分之二:

SELECT MerbarCode, MerName, MerPrice, MerCreateDate FROM dbo.Merchandise ORDER BY MerbarCode OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY FOR JSON PATH, INCLUDE_NULL_VALUES;

注意兼容级别要 130 以上,INCLUDE_NULL_VALUES是让 NULL 输出成null而不是省略字段。OFFSET/FETCH替代ROW_NUMBER做分页,写法更短,性能也更好。

如果版本卡在 2016 之前,用FOR XML PATH手工造 JSON,关键是别踩 2033 字符的坑:FOR XML PATH('')直接返回nvarchar会截断,必须加TYPE再用.value()取出来:

SELECT '[' + STUFF(( SELECT ',' + '{' + '"MerbarCode":"' + ISNULL(MerbarCode, '') + '",' + '"MerName":"' + ISNULL(MerName, '') + '"' + '}' FROM dbo.Merchandise ORDER BY MerbarCode FOR XML PATH(''), TYPE ).value('.', 'nvarchar(max)'), 1, 1, '') + ']';

STUFF(..., 1, 1, '')删掉开头那个多出来的逗号,和 5.2 里说的是同一招。

6.3 上线前的对拍与检查

改完之后不要只测一条数据,用最小但覆盖全的用例跑一轮对拍。我一般固定这几条,缺一条都容易漏:

用例验证点期望结果
第 1 页 20 条首行是否从 1 开始返回 20 行,rn 从 1 起
页码超过总页数空结果集而非报错返回 0 行,无异常
pageSize传 999999上限校验是否生效被压回 200 或直接拒绝
含 NULL 的行null字面量是否正确字段值等于 PythonNone
含双引号的值转义是否完整json.loads通过,内容一致

最后一列是过渡到二次开发前必须走的动作:每次改完,先跑一遍对拍脚本再提交。

我吃过一次亏:改完拼接逻辑只在 SSMS 里瞟了一眼输出,觉得「长得像 JSON」,直接合并上线,结果某个带引号的门店名当天晚上把整个列表接口打挂了。从那以后我每次动这类拼串代码,都强制走一遍json.loads全量对拍,哪怕只改了一个字符串。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 2:44:24

LeetCode解题必看:提示信息的高效利用技巧

1. LeetCode解题前查看提示的重要性在LeetCode平台上刷题时&#xff0c;很多开发者会直接跳入代码编写环节&#xff0c;却忽略了题目描述区域提供的宝贵提示信息。这些提示往往包含了解决该问题的关键思路、边界条件说明以及可能的时间复杂度要求。根据我多年算法竞赛和面试准备…

作者头像 李华
网站建设 2026/10/31 2:43:47

企业AI落地九大痛点与实战解决方案

1. 企业AI落地现状与挑战2023年大模型技术爆发后&#xff0c;超过76%的中国企业启动了AI项目&#xff0c;但实际投产率不足23%。某咨询机构调研显示&#xff0c;AI项目失败的主因并非技术瓶颈&#xff0c;而是落地过程中的认知偏差和实施误区。新手开发者常陷入"技术万能论…

作者头像 李华
网站建设 2026/10/31 2:42:54

轨迹预测技术十年演进:从传统方法到多模态融合

1. 轨迹预测技术十年发展全景&#xff08;2015-2025&#xff09;十年前我刚接触轨迹预测时&#xff0c;行业还在用简单的线性回归分析移动轨迹。如今这个领域已经发展到需要专门学习图神经网络和时空注意力机制的程度。这十年间&#xff0c;从自动驾驶到城市管理&#xff0c;轨…

作者头像 李华
网站建设 2026/10/31 2:42:43

云端AI推理中的模型量化技术与实践指南

1. 云端推理中的模型量化技术概述在当今AI应用爆炸式增长的时代&#xff0c;云端推理服务面临着前所未有的性能挑战。模型量化技术作为解决这一痛点的关键方案&#xff0c;正在重塑云端AI服务的成本效益比。简单来说&#xff0c;模型量化就是通过降低神经网络中权重和激活值的数…

作者头像 李华
网站建设 2026/10/31 2:42:18

PDB符号结构查看与内核偏移基线比对实践

简介&#xff1a;SymbolTypeViewer_1_0_0_6 最新版是一款面向内核符号与结构分析场景的轻量工具包&#xff0c;适合需要研究 Windows 符号文件、查看 PDB 类型结构或进行逆向与调试辅助的开发者使用。压缩包共 8 个文件&#xff0c;以可执行程序、ClickOnce 应用部署文件为主&a…

作者头像 李华
网站建设 2026/10/31 2:42:15

SymbolTypeViewer 1.0.0.6 内核符号路径配置与排错

简介&#xff1a;SymbolTypeViewer 是一款面向内核研究、逆向工程与安全分析人员的符号与类型查看工具&#xff0c;重点解决符号文件获取繁琐、结构体成员关系难以直观梳理的问题。工具支持简便下载符号文件&#xff0c;以树形结构逐层展开类型及其成员&#xff0c;并标记结构体…

作者头像 李华