为什么在 SQL Server 2016 上使用 XML 的函数编译时间长?

Ril*_*jor 7 xml sql-server optimization functions sql-server-2016

我们有内联函数,可以将数据收集到 XML 中,将派生的 XML 传递给其他函数,然后将其切碎并将其重组为字符串。

(你的“你不应该在 T-SQL 中做那种事情”是另一天的讨论。)

多年来,这在 2005 和 2008 R2 中一直运行良好。我们现在正在升级到 2016 SP1。使用这些函数在我们的生产服务器上运行不到一秒的查询现在在 2016 SP1 中运行得更快。那太好了,但是在 2016 SP1 上编译需要一个小时。严重地:

在此处输入图片说明

生产环境:

在此处输入图片说明

(它们都来自 SQL Sentry Plan Explorer。)

我们已经在 2008 (100) 和 2016 (130) 兼容性级别尝试了数据库(但我们还没有使用“传统基数估计”和“查询优化器修复”设置,目前这两个设置都为“关闭”)。我们尝试过使用QUERYTRACEON 9481,似乎没有效果。

同样,这与最终的计划无关,因为它们都在很短的时间内运行。这是制定计划所需的时间。

我们已经设法用一组简化的代码在一定程度上重现了这个问题。调用以下示例中的顶级函数的语句在 SQL Server 2016 (SP1-CU5) 上编译需要 30-60 秒,但在 SQL Server 2008 R2 (SP3) 上编译和运行只需不到 1 秒。

例子

/*

Create and populate table...

*/

CREATE TABLE TestXMLStuff (OrderID int, ProdLength int, ProdWidth int, ProdHeight int);
INSERT INTO TestXMLStuff (OrderID, ProdLength, ProdWidth, ProdHeight) VALUES
    (1, 10, 15, 20),
    (1, 15, 20, 25),
    (2, 20, 25, 30),
    (2, 25, 30, 35),
    (2, 30, 35, 40);
GO

/*

Function which accepts XML, shreds it and reforms it as a string...

*/


CREATE FUNCTION TestCalc
(   
    @T varchar(8000),
    @X xml
)
RETURNS TABLE 
AS
RETURN 
    WITH p AS
    (
        SELECT  
            LF = CHAR(13) + CHAR(10),
            Tab = CHAR(9),
            T = isNull(@T,'')
    ), pid AS
    (
        SELECT
            isNull(ProdInfoXMLTable.ProdInfoXML.query('(/ProdInfo)').value('(.)[1]','varchar(max)'),'') AS ProdInfoText
        FROM        (
                        SELECT
                            ProdInfoXML =
                                (
                                    SELECT
                                        ProdInfo = 
                                            CASE WHEN Products.ProdNum > 1 THEN '--' + p.LF ELSE '' END +
                                            'Product Number: ' + CONVERT(varchar(50),Products.ProdNum) + p.LF +
                                                CASE WHEN Products.ProdLength       = '' THEN '' ELSE p.Tab + 'Length: '                + Products.ProdLength       + p.LF END +
                                                CASE WHEN Products.ProdWidth        = '' THEN '' ELSE p.Tab + 'Width: '                 + Products.ProdHeight       + p.LF END +
                                                CASE WHEN Products.ProdHeight       = '' THEN '' ELSE p.Tab + 'Height: '                + Products.ProdHeight       + p.LF END
                                    FROM        (
                                                    SELECT
                                                        ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS ProdNum,
                                                        isNull(P.X.value('(./Length)[1]','varchar(500)'),'') AS ProdLength,
                                                        isNull(P.X.value('(./Width)[1]','varchar(500)'),'') AS ProdWidth,
                                                        isNull(P.X.value('(./Height)[1]','varchar(500)'),'') AS ProdHeight
                                                    FROM        @x.nodes('/Products/Product') AS P(X)
                                                ) AS Products
                                    CROSS JOIN  p
                                    FOR XML PATH(''), TYPE
                                )
                    ) AS ProdInfoXMLTable
    )
    SELECT
        Final = p.T + p.LF + p.LF + pid.ProdInfoText
    FROM        p
    CROSS JOIN  pid;

GO

/*

Function to create XML in the format required for TestCalc...

*/

CREATE FUNCTION TestGetXML
(   
    @N int
)
RETURNS TABLE 
AS
RETURN 
    WITH p AS
    (
        SELECT  
            N = isNull(@N,0)
    )
    SELECT
        ProdInfoXML =
            (
                SELECT
                    [Length] = ProdData.ProdLength,
                    [Width] = ProdData.ProdWidth,
                    [Height] = ProdData.ProdHeight
                FROM        TestXMLStuff ProdData
                WHERE       ProdData.OrderID = @N
                FOR XML PATH('Product'), ROOT('Products'), TYPE
            );
GO

/*

Function to join the other two functions, gathering the XML and feeding it to the string creator which shreds and reforms it...

*/

CREATE FUNCTION TestGetFromTableUsingFunc
(   
    @N int
)
RETURNS TABLE 
AS
RETURN 
    WITH p AS
    (
        SELECT  
            N = isNull(@N,0)
    )
    SELECT
        FinalResult = 'This is a ' + TestCalcResults.Final
    FROM        p
    CROSS APPLY TestGetXML
                (
                    p.N
                ) AS x
    CROSS APPLY TestCalc
                (
                    'test',
                    x.ProdInfoXML
                ) AS TestCalcResults;
GO

/*

Code to call the function. This is what takes around 60 seconds to compile on our 2016 system but basically no time on the 2008 R2 system.

*/

SELECT      *
FROM        TestXMLStuff
CROSS APPLY TestGetFromTableUsingFunc
            (
                OrderID
            )
OPTION      (RECOMPILE);
GO
Run Code Online (Sandbox Code Playgroud)

生产@@version其中编译是不是一个问题:

Microsoft SQL Server 2008 R2 (SP3) - 10.50.6000.34 (X64)
Run Code Online (Sandbox Code Playgroud)

测试@@version编译需要“永远”的地方:

Microsoft SQL Server 2016 (SP1-CU5) (KB4040714) - 13.0.4451.0 (X64) 
Run Code Online (Sandbox Code Playgroud)

问题

  1. 为什么从 2008 R2 到 2016 的编译时间会如此缩短?

  2. 这个答案是否揭示了解决这一困境的简单方法,而无需改变所有这些工作方式?(我希望有神奇的跟踪标志或即将到来的 Microsoft 更新。)

(如果这是 SQL Server 2017,我将使用 JSON 来收集和传递数据,这似乎更快且开销更低,然后我将使用 JSON 函数来切碎并STRING_AGG重新转换为文本。但唉,现在还没有可用的。)

根据Joe Obbish的提示,我使用以下代码收集了使用跟踪标志 8675时的结果:

DBCC TRACEON(3604)
SELECT      *
FROM        TestXMLStuff
CROSS APPLY TestGetFromTableUsingFunc
            (
                OrderID
            )
OPTION      (RECOMPILE, QUERYTRACEON 8675);
DBCC TRACEOFF(3604)
Run Code Online (Sandbox Code Playgroud)

在 2008 R2 实例上,它只用了不到一秒钟的时间,并生成了以下内容:

/*

Create and populate table...

*/

CREATE TABLE TestXMLStuff (OrderID int, ProdLength int, ProdWidth int, ProdHeight int);
INSERT INTO TestXMLStuff (OrderID, ProdLength, ProdWidth, ProdHeight) VALUES
    (1, 10, 15, 20),
    (1, 15, 20, 25),
    (2, 20, 25, 30),
    (2, 25, 30, 35),
    (2, 30, 35, 40);
GO

/*

Function which accepts XML, shreds it and reforms it as a string...

*/


CREATE FUNCTION TestCalc
(   
    @T varchar(8000),
    @X xml
)
RETURNS TABLE 
AS
RETURN 
    WITH p AS
    (
        SELECT  
            LF = CHAR(13) + CHAR(10),
            Tab = CHAR(9),
            T = isNull(@T,'')
    ), pid AS
    (
        SELECT
            isNull(ProdInfoXMLTable.ProdInfoXML.query('(/ProdInfo)').value('(.)[1]','varchar(max)'),'') AS ProdInfoText
        FROM        (
                        SELECT
                            ProdInfoXML =
                                (
                                    SELECT
                                        ProdInfo = 
                                            CASE WHEN Products.ProdNum > 1 THEN '--' + p.LF ELSE '' END +
                                            'Product Number: ' + CONVERT(varchar(50),Products.ProdNum) + p.LF +
                                                CASE WHEN Products.ProdLength       = '' THEN '' ELSE p.Tab + 'Length: '                + Products.ProdLength       + p.LF END +
                                                CASE WHEN Products.ProdWidth        = '' THEN '' ELSE p.Tab + 'Width: '                 + Products.ProdHeight       + p.LF END +
                                                CASE WHEN Products.ProdHeight       = '' THEN '' ELSE p.Tab + 'Height: '                + Products.ProdHeight       + p.LF END
                                    FROM        (
                                                    SELECT
                                                        ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS ProdNum,
                                                        isNull(P.X.value('(./Length)[1]','varchar(500)'),'') AS ProdLength,
                                                        isNull(P.X.value('(./Width)[1]','varchar(500)'),'') AS ProdWidth,
                                                        isNull(P.X.value('(./Height)[1]','varchar(500)'),'') AS ProdHeight
                                                    FROM        @x.nodes('/Products/Product') AS P(X)
                                                ) AS Products
                                    CROSS JOIN  p
                                    FOR XML PATH(''), TYPE
                                )
                    ) AS ProdInfoXMLTable
    )
    SELECT
        Final = p.T + p.LF + p.LF + pid.ProdInfoText
    FROM        p
    CROSS JOIN  pid;

GO

/*

Function to create XML in the format required for TestCalc...

*/

CREATE FUNCTION TestGetXML
(   
    @N int
)
RETURNS TABLE 
AS
RETURN 
    WITH p AS
    (
        SELECT  
            N = isNull(@N,0)
    )
    SELECT
        ProdInfoXML =
            (
                SELECT
                    [Length] = ProdData.ProdLength,
                    [Width] = ProdData.ProdWidth,
                    [Height] = ProdData.ProdHeight
                FROM        TestXMLStuff ProdData
                WHERE       ProdData.OrderID = @N
                FOR XML PATH('Product'), ROOT('Products'), TYPE
            );
GO

/*

Function to join the other two functions, gathering the XML and feeding it to the string creator which shreds and reforms it...

*/

CREATE FUNCTION TestGetFromTableUsingFunc
(   
    @N int
)
RETURNS TABLE 
AS
RETURN 
    WITH p AS
    (
        SELECT  
            N = isNull(@N,0)
    )
    SELECT
        FinalResult = 'This is a ' + TestCalcResults.Final
    FROM        p
    CROSS APPLY TestGetXML
                (
                    p.N
                ) AS x
    CROSS APPLY TestCalc
                (
                    'test',
                    x.ProdInfoXML
                ) AS TestCalcResults;
GO

/*

Code to call the function. This is what takes around 60 seconds to compile on our 2016 system but basically no time on the 2008 R2 system.

*/

SELECT      *
FROM        TestXMLStuff
CROSS APPLY TestGetFromTableUsingFunc
            (
                OrderID
            )
OPTION      (RECOMPILE);
GO
Run Code Online (Sandbox Code Playgroud)

在 2016 SP1-CU5 实例上,花费了 1 分 11 秒并生成了以下内容:

Microsoft SQL Server 2008 R2 (SP3) - 10.50.6000.34 (X64)
Run Code Online (Sandbox Code Playgroud)

虽然还有更多的事情发生,但看起来过去的时间只有 0.018(秒?),比 2008 R2 上的 0.03 少。所以这个编译/优化/执行阶段一定不会花这么长时间。但肯定是有的。


我们最终的 2016 生产实例将具有与当前生产 2008 R2 相同的“硬件”。Test/Dev 2016 实例具有不同的规格,因此这些比较不是苹果对苹果的比较。产品是 78 演出。开发是 16 演出。但是我在另一个 2008 R2 盒子上测试了 20 gig,速度很快。此外,我们正在谈论索引良好的少量数据。并且在编译期间很少 IO 但有很多 CPU。

我可以看到统计数据是命中真实(大)表的真实函数的一个问题,但在我人为的简化示例中,对5个满载整数的行进行一些简单的 XML/文本操作需要 1 分钟以上的时间。我可以更快地输入结果。:) 我相信我们对生产有自动统计,而且我没有看到其他看似与统计相关的性能问题。另一个非生产 2008 R2 开发/测试环境(具有与 2016 开发/测试类似的陈旧生产副本)是 lickety-split。

Joe*_*ish 7

SQL Server 2016 SP1 的 CU7可能会解决您的问题。相关知识库文章似乎是KB 4056955 - FIX:将字符串或二进制数据转换为 XML 的查询需要很长时间才能在 SQL Server 2016 中编译

我在 SQL Server 2016 SP1 CU6 中运行了您的重现代码,并获得了以下编译时间SET STATISTICS TIME ON

SQL Server 解析和编译时间:CPU 时间 = 24437 毫秒,经过时间 = 24600 毫秒。

SQL Server 解析和编译时间:CPU 时间 = 48968 毫秒,已用时间 = 49451 毫秒。

以下是升级到 SP1 CU7 后编译时间的变化:

SQL Server 解析和编译时间:CPU 时间 = 16 毫秒,经过时间 = 16 毫秒。

SQL Server 解析和编译时间:CPU 时间 = 16 毫秒,经过时间 = 17 毫秒。

SQL Server 解析和编译时间:CPU 时间 = 16 毫秒,经过时间 = 17 毫秒。

SQL Server 解析和编译时间:CPU 时间 = 44 毫秒,经过时间 = 44 毫秒。

它大约快 750 倍。