为什么 SQL Server 将它的 (JSON) 响应拆分为多行?

joe*_*nte 6 sql-server query subquery json

我正在尝试构建一个查询,该查询会生成一个由 SQL Server 生成的 JSON 对象。我发现我可以使用子查询用包含问题列表的 JSON 字符串填充字段(在本例中为问题字段)。

下面是查询:

SELECT
    quizzes.id AS 'id',
    quizzes.name AS 'name',
    quizzes.description AS 'description',
    quizzes.instructions AS 'instructions',
    author.id AS 'author.id',
    author.midas AS 'author.midas',
    author.first_name AS 'author.first_name',
    author.last_name AS 'author.last_name',
    author.email AS 'author.email',
    author.tel AS 'author.tel',
    author.department_name AS 'author.department_name',
    author.created_at AS 'author.created_at',
    author.last_updated AS 'author.last_updated',
    course.id AS 'course.id',
    course.name AS 'course.name',
    course.description AS 'course.description',
    course.crn AS 'course.crn',
    instructor.id AS 'course.instructor.id',
    instructor.midas AS 'course.instructor.midas',
    instructor.first_name AS 'course.instructor.first_name',
    instructor.last_name AS 'course.instructor.last_name',
    instructor.email AS 'course.instructor.email',
    instructor.tel AS 'course.instructor.tel',
    instructor.department_name AS 'course.instructor.department_name',
    instructor.created_at AS 'course.instructor.created_at',
    instructor.last_updated AS 'course.instructor.last_updated',
    course.created_at AS 'course.created_at',
    course.last_updated AS 'course.last_updated',
    
    
    (
        SELECT
            questions.id AS 'id',
            questions.text AS 'text',
            question_types.id AS 'type.id',
            question_types.name AS 'type.name',
            question_types.created_at AS 'type.created_at',
            question_types.description AS 'type.description',
            question_author.id AS 'author.id',
            question_author.midas AS 'author.midas',
            question_author.first_name AS 'author.first_name',
            question_author.last_name AS 'author.last_name',
            question_author.email AS 'author.email',
            question_author.tel AS 'author.tel',
            question_author.department_name AS 'author.department_name',
            question_author.created_at AS 'author.created_at',
            question_author.last_updated AS 'author.last_updated',
            questions.is_graded AS 'is_graded',
            questions.score_value AS 'score_value',
            questions.created_at AS 'created_at',
            questions.last_updated AS 'last_updated'
        FROM
            questions
        LEFT JOIN users AS question_author ON question_author.id = questions.author
        LEFT JOIN question_types ON question_types.id = questions.type
        WHERE
            questions.quiz = quizzes.id FOR JSON PATH, INCLUDE_NULL_VALUES
    ) AS 'questions',
    
    
    quizzes.created_at AS 'created_at',
    quizzes.last_updated AS 'last_updated'
FROM
    quizzes
    LEFT JOIN users AS author ON quizzes.author = author.id
    LEFT JOIN courses AS course ON quizzes.course = course.id
    LEFT JOIN users AS instructor ON course.instructor = instructor.id FOR JSON PATH,
    INCLUDE_NULL_VALUES;
Run Code Online (Sandbox Code Playgroud)

问题是:当我执行此查询时,它以两行响应,其中生成的 JSON 字符串被分成两部分。显然这是不可取的。

经过调查,我发现如果我删除了LEFT JOIN's,那么查询会按它应该的方式响应(只有一行完整的字符串)。


以下是以下行为的示例:

SELECT n = sc1.name FROM sys.syscolumns sc1 FOR JSON AUTO
Run Code Online (Sandbox Code Playgroud)

如上所示,返回了 11 行。

结果

JSON 输出的长度约为 20,000 个字符。

JSON 长度


我正在使用以下版本的 SQL Server

Microsoft SQL Server 2019 - 15.0.4073.23 (X64) 
Developer Edition (64-bit) on Linux (Ubuntu 18.04.5 LTS) <X64>
Run Code Online (Sandbox Code Playgroud)

为什么会这样?我该如何解决?

joe*_*nte 10

经过进一步研究,我从这篇 StackOverflow 帖子中发现 SQL Server 将FOR JSON查询分解为“~2kb 块”。

Sql Server 将 FOR JSON 查询的结果拆分为 ~2KB 块,因此您应该像 MSDN 页面那样连接片段,或者您可以将结果流式传输到某个输出流中。

这意味着每个块只能发送约 2000 个字符。


更新:

Max VernonAndriy M的帮助下,我们找到了一个相当简单的解决方案。

DECLARE @json nvarchar(max);

;WITH src (n) AS
(
    SELECT n = sc1.name
    FROM sys.syscolumns sc1
    FOR JSON AUTO
)
SELECT @json = src.n
FROM src

SELECT @json, LEN(@json);
Run Code Online (Sandbox Code Playgroud)

可以在此处找到导致此问题的聊天记录。

上面的查询返回两列。

  1. 完全组装的 JSON 字符串
  2. 该字符串的长度

理想情况下,您可以用您自己的查询替换两个括号之间的查询。

为什么这样做?

根据微软的文档

SQL Server 对此行集使用预定义的列名,其中一列是 NTEXT 类型——“XML_F52E2B61-18A1-11d1-B105-00805F49916B”——以 UTF-16 编码表示分块的 XML 行集。这需要 API 对 XML 块行集进行特殊处理,以在客户端将其公开为单个 XML 实例。在 ADO.Net 中,需要使用 ExecuteXmlReader,而在 ADO/OLEDB 中,应该使用 ICommandStream 接口。

(虽然以上确实是指XML,但JSON也是如此。)

首先,JSON 和 XML 响应以块的形式返回的原因是出于性能原因:

为了获得最大的 XML [JSON] 发布性能 FOR XML [JSON] 对结果行集进行流式 XML 格式化,并将其输出以小块的形式直接发送到服务器端 TDS 代码,而无需在服务器空间中缓冲整个 XML。块大小为 2033 个 UCS-2 字符。因此,大于 2033 个 UCS-2 字符的 XML 以多行的形式发送到客户端,每行都包含一个 XML 块。

上面的解决方案通过首先将结果设置FOR JSON为变量,然后发送变量的值来规避这一点,这导致 SQL Server 将响应作为一行返回给客户端。

应该注意的是,一些数据库客户端(特别是 SQL Server Management Studio)能够“重建”分块响应,但是如果您使用 PHP ( PDO ) 或在免费试用客户端(例如 TablePlus for Mac)上运行,您将看到原始的、分块的响应。


表现

就性能而言,我没有做过任何广泛的测试,但我可以提供以下来自我所做的有限测试的数据:

使用运行最新操作系统的 MacBook Pro,我发现平均以下查询

SELECT n = sc1.name
    FROM sys.syscolumns sc1
    FOR JSON AUTO
Run Code Online (Sandbox Code Playgroud)

将在大约23,300 秒内处理

虽然查询

DECLARE
    @json nvarchar (max);

;WITH src (n) AS
(
    SELECT n = sc1.name
    FROM sys.syscolumns sc1
    FOR JSON AUTO
)
SELECT @json = src.n
FROM src

SELECT @json, LEN(@json);
Run Code Online (Sandbox Code Playgroud)

平均花费137.8 秒的处理时间。

这似乎与文档所说的直接冲突,所以我不确定这些结果的可信度。但是,可能值得自己对此进行测试。

测试数据结果