降低表值函数的成本 - 查询计划中的 XML 阅读器 - 如何?

Posted

技术标签:

【中文标题】降低表值函数的成本 - 查询计划中的 XML 阅读器 - 如何?【英文标题】:Reduce cost for Table Valued Function - XML Reader in query plan - how? 【发布时间】:2019-08-01 10:08:37 【问题描述】:

我有以下 SQL Server 查询:

with cte as 
(SELECT DISTINCT refrecid
FROM         docuref
WHERE     ACTUALCOMPANYID = 'an' and REFTABLEID='78' and typeid='Note')
SELECT     docuref.REFRECID,  Notes = STUFF
                            ((SELECT     CHAR(13) + CHAR(10) + cast([NOTES] AS nvarchar(max))
                                FROM         DOCUREF
                                WHERE     REFRECID = Cte.refrecid AND ACTUALCOMPANYID = 'an' FOR XML PATH(''), TYPE ).value('.', 'nvarchar(max)'), 1, 2, '')
 FROM         Cte INNER JOIN
                        DOCUREF ON cte.REFRECID= docuref.REFRECID
 WHERE     DOCUREF.ACTUALCOMPANYID = 'an' and docuref.REFTABLEID='78' and docuref.typeid='Note'
 GROUP BY docuref.REFRECID,cte.refrecid

Docuref 表包含大约 40,000 行。我正在尝试将 Notes 列合并到一个 RefrecID 相同的记录中,

例如,如果我有以下内容:

Refrecid    Recid     Notes
1000        2000      Notes1
1000        2001      Notes2
1000        2002      Notes3

我最终会得到:

Refrecid    Notes
1000        Notes1
            Notes2
            Notes3

但是,这个查询运行大约需要 2 分钟,所以需要大大减少,所以只需要几秒钟。我查看了实际执行计划,成本最高的项目是“表值函数 - XML 阅读器”,成本为 91%。实际执行计划见下文:

https://www.brentozar.com/pastetheplan/?id=ByJMhcb7B

有没有更好的方法来做我正在做的事情?

编辑: 因此,基于@Shnugo 的 cmets,我使用 Temp 表进行查询,查询运行时间从 2 分钟下降到 3 秒。我现在使用的查询是:

IF OBJECT_ID('tempdb..#TempTable') Is Not null
Drop Table #TempTable
;
WITH grouped AS
(
    SELECT  dr.refrecid AS REFRECID
    FROM    docuref dr
    WHERE   dr.ACTUALCOMPANYID = 'ansa' 
        and dr.REFTABLEID='78' 
        and dr.typeid='Note'
    GROUP BY dr.refrecid
)

select * into #TempTable from grouped

SELECT gr.REFRECID
    ,STUFF(
    (
        SELECT CHAR(13) + CHAR(10) + cast(dr2.[NOTES] AS nvarchar(max))
        FROM DOCUREF dr2
        WHERE dr2.REFRECID = gr.refrecid 
            AND dr2.ACTUALCOMPANYID = 'ansa' 
        FOR XML PATH(''), TYPE 
    ).value('.', 'nvarchar(max)'), 1, 2, '') AS Notes
FROM #TempTable gr;

【问题讨论】:

【参考方案1】:

这只是一个快速的镜头,但它可能会有所帮助:

阅读 Aaron Bertrand 的这篇文章:"Performance Surprises and Assumptions : GROUP BY vs. DISTINCT"

更改此 cte 代码

SELECT DISTINCT refrecid
FROM         docuref
WHERE     ACTUALCOMPANYID = 'an' and REFTABLEID='78' and typeid='Note'

到这里

SELECT refrecid
FROM   docuref
WHERE  ACTUALCOMPANYID = 'an' and REFTABLEID='78' and typeid='Note'
GROUP BY refrecid

使用DISTINCT 引擎将返回所有行并过滤最终输出。使用 GROUP BY 将返回一个 refrecid 并继续此操作。您的执行计划可能会告诉您(估计行数和实际行数),如果集合减少到单个 ID beforeafter 您正在调用STUFF(sub select with XML)...

提示:最好的办法是升级到 v2017 并使用 STRING_AGG()。

更新:我认为这可以更简单...

试试看:

SELECT  dr.refrecid AS REFRECID
       ,STUFF(
       (
           SELECT CHAR(13) + CHAR(10) + cast(dr2.[NOTES] AS nvarchar(max))
           FROM DOCUREF dr2
           WHERE dr2.REFRECID = dr.refrecid --- this was cte.refrecid
             AND dr2.ACTUALCOMPANYID = 'an' 
           FOR XML PATH(''), TYPE 
       ).value('.', 'nvarchar(max)'), 1, 2, '') AS Notes
FROM    docuref dr
WHERE   dr.ACTUALCOMPANYID = 'an' 
    and dr.REFTABLEID='78' 
    and dr.typeid='Note'
GROUP BY dr.refrecid;

或者这个 - 但我希望这是一样的......

WITH grouped AS
(
    SELECT  dr.refrecid AS REFRECID
    FROM    docuref dr
    WHERE   dr.ACTUALCOMPANYID = 'an' 
        and dr.REFTABLEID='78' 
        and dr.typeid='Note'
    GROUP BY dr.refrecid
)
SELECT gr.REFRECID
        ,STUFF(
        (
            SELECT CHAR(13) + CHAR(10) + cast(dr2.[NOTES] AS nvarchar(max))
            FROM DOCUREF dr2
            WHERE dr2.REFRECID = gr.refrecid 
                AND dr2.ACTUALCOMPANYID = 'an' 
            FOR XML PATH(''), TYPE 
        ).value('.', 'nvarchar(max)'), 1, 2, '') AS Notes
FROm grouped gr;

更新 2

根据您的评论,我认为您的解决方案是使用两步方法:

    使用过滤后的 ID 创建临时表 对这个精简列表采取代价高昂的行动。

很高兴知道:查询优化器不会像我们想象的那样处理查询。这不是一个程序化的逐步过程。我们告诉引擎我们想要什么(结果集的正式描述),然后引擎决定如何做到最好。

在这种情况下,我很确定,编译器没有重新识别,STUFF() 中的东西会很昂贵。假设STUFF() 是为每一行完成的之前 WHEREGROUP BY 减少了行。

CTE 与表不同(尽管我们可以在查询中以相同的方式使用它)。特别是对于 CTE,我喜欢 WITH(ForceFirst) 或类似的提示,它会告诉编译器在查询的其余部分 before 创建 CTE 的集合。

在这种情况下,最好使用程序方法(one-step-after-the-other)强制执行顺序。

【讨论】:

第一个 Group by 建议仍然需要大约 2 分钟才能执行,因此没有任何变化。 第二条语句执行耗时 1 分 45 秒 第三条语句执行耗时 1 分 45 秒 所以与原始语句没有太大变化。感谢您的建议! @Naz 没有STUFF(SELECT...) 有多快?它返回多少行?表中有多少行? refrecid、actualcompanyid、reftableid 和 typeid 上是否有索引?您可以分两步尝试:首先将不同的 id 读入一个临时表,然后从那里继续... @Naz 还有一个问题:您是否检查了估计和实际行数的执行计划? 所以我尝试不使用 STUFF(SELECT...) 并在不到一秒的时间内返回 13k 行。表中的总行数为 40k。 RefcompanyID/TypeID 上有一个非唯一的非聚集索引。此外,RefcompanyID/ReftableID/RefrecID 上的聚集索引。【参考方案2】:

为节点路径指定singleton text node ((./text())[1])。这将有助于优化查询计划中的 XML 表值函数,这可以为更大的结果提供显着的改进。

with cte as 
(SELECT DISTINCT refrecid
FROM         docuref
WHERE     ACTUALCOMPANYID = 'an' and REFTABLEID='78' and typeid='Note')
SELECT     docuref.REFRECID,  Notes = STUFF
                            ((SELECT     CHAR(13) + CHAR(10) + cast([NOTES] AS nvarchar(max))
                                FROM         DOCUREF
                                WHERE     REFRECID = Cte.refrecid AND ACTUALCOMPANYID = 'an' FOR XML PATH(''), TYPE ).value('(./text())[1]', 'nvarchar(max)'), 1, 2, '')
 FROM         Cte INNER JOIN
                        DOCUREF ON cte.REFRECID= docuref.REFRECID
 WHERE     DOCUREF.ACTUALCOMPANYID = 'an' and docuref.REFTABLEID='78' and docuref.typeid='Note'
 GROUP BY docuref.REFRECID,cte.refrecid

【讨论】:

它把时间缩短了5秒,但还是太长了。所以我刚刚将查询处理的行数从 40,000 减少到大约 1,500。这是我可以将性能降低到 10 秒以下的唯一方法。感谢您的帮助! @Naz,将您的实际执行计划上传到brentozar.com/pastetheplan,并将链接添加到您的问题中。 我已经添加了计划链接,没有意识到我可以这样做,谢谢你的提示! 该计划显示索引扫描操作符触及了 695M 行,因为由于嵌套循环,它被执行了 12965 次。您可以尝试@Shnugo 建议的重构以及我建议的更改,看看您是否有更好的计划。如果没有,请为您的表和索引添加 DDL 以获得更多帮助。 我会认为 XML 阅读器是问题所在,因为它具有大部分查询成本 - 91%?我会试试 Shnugo 的建议

以上是关于降低表值函数的成本 - 查询计划中的 XML 阅读器 - 如何?的主要内容,如果未能解决你的问题,请参考以下文章

SQL Server:表值函数与存储过程

SQL Server 中的 SQL 查询优化

SQL 中内联表值函数的性能影响

如何降低通过链接服务器执行的查询的成本

从表值函数返回显式 Open XML 结果集

选择与表值函数连接中的 Oracle 标量函数