降低表值函数的成本 - 查询计划中的 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 before 或 after 您正在调用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()
是为每一行完成的之前 WHERE
和GROUP 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 阅读器 - 如何?的主要内容,如果未能解决你的问题,请参考以下文章