使用XML阅读器优化计划


34

从此处执行查询以将死锁事件从默认扩展事件会话中拉出

SELECT CAST (
    REPLACE (
        REPLACE (
            XEventData.XEvent.value ('(data/value)[1]', 'varchar(max)'),
            '<victim-list>', '<deadlock><victim-list>'),
        '<process-list>', '</victim-list><process-list>')
    AS XML) AS DeadlockGraph
FROM (SELECT CAST (target_data AS XML) AS TargetData
    FROM sys.dm_xe_session_targets st
    JOIN sys.dm_xe_sessions s ON s.address = st.event_session_address
    WHERE [name] = 'system_health') AS Data
CROSS APPLY TargetData.nodes ('//RingBufferTarget/event') AS XEventData (XEvent)
    WHERE XEventData.XEvent.value('@name', 'varchar(4000)') = 'xml_deadlock_report';

在我的计算机上完成大约需要20分钟。报告的统计数据是

Table 'Worktable'. Scan count 0, logical reads 68121, physical reads 0, read-ahead reads 0, 
         lob logical reads 25674576, lob physical reads 0, lob read-ahead reads 4332386.

 SQL Server Execution Times:
   CPU time = 1241269 ms,  elapsed time = 1244082 ms.

慢计划XML

平行

如果删除该WHERE子句,它会在不到一秒的时间内完成返回3,782行。

类似地,如果我添加OPTION (MAXDOP 1)到原始查询中,它也加快了速度,现在的统计信息显示大量的lob读取减少。

Table 'Worktable'. Scan count 0, logical reads 15, physical reads 0, read-ahead reads 0,
                lob logical reads 6767, lob physical reads 0, lob read-ahead reads 6076.

 SQL Server Execution Times:
   CPU time = 639 ms,  elapsed time = 693 ms.

更快的计划XML

序列号

所以我的问题是

谁能解释这是怎么回事?为什么原始计划如此灾难性地恶化,并且有任何可靠的方法来避免该问题?

加成:

我还发现,INNER HASH JOIN由于DMV结果太小,更改查询以在某种程度上改善了情况(但是仍然需要3分钟以上),因此我怀疑Join类型本身是负责任的,并且认为必须进行其他更改。统计数据

Table 'Worktable'. Scan count 0, logical reads 30294, physical reads 0, read-ahead reads 0, 
          lob logical reads 10741863, lob physical reads 0, lob read-ahead reads 4361042.

 SQL Server Execution Times:
   CPU time = 200914 ms,  elapsed time = 203614 ms.

(和计划)

填补了扩展事件环缓冲区(以后DATALENGTHXML是4880045个字节,它包含了1448点的事件。)和测试的切口向下原始查询的版本有和没有MAXDOP暗示。

SELECT COUNT(*)
FROM   (SELECT CAST (target_data AS XML) AS TargetData
        FROM   sys.dm_xe_session_targets st
               JOIN sys.dm_xe_sessions s
                 ON s.address = st.event_session_address
        WHERE  [name] = 'system_health') AS Data
       CROSS APPLY TargetData.nodes ('//RingBufferTarget/event') AS XEventData (XEvent)
WHERE  XEventData.XEvent.value('@name', 'varchar(4000)') = 'xml_deadlock_report'

SELECT*
FROM   sys.dm_db_task_space_usage
WHERE  session_id = @@SPID 

得到以下结果

+-------------------------------------+------+----------+
|                                     | Fast |   Slow   |
+-------------------------------------+------+----------+
| internal_objects_alloc_page_count   |  616 |  1761272 |
| internal_objects_dealloc_page_count |  616 |  1761272 |
| elapsed time (ms)                   |  428 |   398481 |
| lob logical reads                   | 8390 | 12784196 |
+-------------------------------------+------+----------+

tempdb分配有明显的不同,显示616页面已分配和释放的速度更快。这与将XML放入变量时使用的页面数量相同。

对于慢速计划,这些页面分配数将达到数百万。dm_db_task_space_usage查询运行时进行轮询表明,它似乎一直在不断分配和取消分配页面,tempdb其中任意一次分配了1,800到3,000个页面。


您可以将WHERE子句移到XQuery表达式中。无需删除逻辑即可使其快速运行:TargetData.nodes ('RingBufferTarget[1]/event[@name = "xml_deadlock_report"]')。也就是说,我对XML的内部知识还不够了解,无法回答您提出的问题。
乔恩·塞格尔

Martin的Paging @SQLPoolBoy Martin ...他建议在这里仔细阅读注释,那里有更有效的建议(它们基于上面代码源文章)。
亚伦·伯特兰

Answers:


36

性能差异的原因在于在执行引擎中如何处理标量表达式。在这种情况下,感兴趣的表达是:

[Expr1000] = CONVERT(xml,DM_XE_SESSION_TARGETS.[target_data],0)

该表达标签定义由计算标量运算符(在串行计划节点11,在并行计划节点13)。计算标量运算符与其他运算符(SQL Server 2005及更高版本)的不同之处在于,它们定义的表达式不一定要在它们出现在可见执行计划中的位置进行求值。可以将评估推迟到以后的运算符需要计算结果之前。

在当前查询中,target_data字符串通常很大,使得从字符串到XML昂贵的转换。在慢速计划中,XML每当需要返回结果的后一个运算符Expr1000反弹时,都会执行转换字符串。

当相关参数(外部引用)改变时,重新绑定发生在嵌套循环的内侧。Expr1000是此执行计划中大多数嵌套循环联接的外部引用。多个XML读取器(流聚合)和启动过滤器多次引用该表达式。根据的大小,XML字符串被转换为的次数XML可以轻松达到数百万。

下面的调用堆栈显示了target_data要转换为字符串的示例XMLConvertStringToXMLForES-其中ES是Expression Service):

启动过滤器

启动筛选器调用堆栈

XML阅读器(内部是TVF Stream)

TVF流调用堆栈

流聚合

流聚合调用堆栈

XML每次重新绑定这些运算符时,都将字符串转换为可解释的嵌套循环计划所产生的性能差异。这与是否使用并行性无关。恰好发生在MAXDOP 1指定提示时优化器选择哈希联接的情况。如果MAXDOP 1, LOOP JOIN指定,则性能与默认的并行计划(优化器选择嵌套循环)一样差。

哈希联接可以提高多少性能,取决于是否Expr1000出现在运算符的内部版本或探测方面。以下查询将表达式定位在探针侧:

SELECT CAST (
    REPLACE (
        REPLACE (
            XEventData.XEvent.value ('(data/value)[1]', 'varchar(max)'),
            '<victim-list>', '<deadlock><victim-list>'),
        '<process-list>', '</victim-list><process-list>')
    AS XML) AS DeadlockGraph
FROM (SELECT CAST (target_data AS XML) AS TargetData
    FROM sys.dm_xe_sessions s
    INNER HASH JOIN sys.dm_xe_session_targets st ON s.address = st.event_session_address
    WHERE [name] = 'system_health') AS Data
CROSS APPLY TargetData.nodes ('//RingBufferTarget/event') AS XEventData (XEvent)
WHERE XEventData.XEvent.value('@name', 'varchar(4000)') = 'xml_deadlock_report';

我已经从问题中显示的版本中颠倒了联接的书面顺序,因为联接提示(INNER HASH JOIN上面)也强制了整个查询的顺序,就像FORCE ORDER已经指定了一样。必须进行反转以确保Expr1000出现在探针侧。执行计划中有趣的部分是:

提示1

使用在探针端定义的表达式,该值被缓存:

散列缓存

Expr1000直到第一个运算符需要该值(上面的堆栈跟踪中的启动过滤器)之后,才进行评估,但是计算的值将被缓存(CValHashCachedSwitch),并由XML Reader和Stream Aggregates重新用于以后的调用。下面的堆栈跟踪显示了XML读取器重用的缓存值的示例。

缓存重用

当强制执行连接顺序以致Expr1000在哈希连接的构建侧出现的定义时,情况就不同了:

SELECT CAST (
    REPLACE (
        REPLACE (
            XEventData.XEvent.value ('(data/value)[1]', 'varchar(max)'),
            '<victim-list>', '<deadlock><victim-list>'),
        '<process-list>', '</victim-list><process-list>')
    AS XML) AS DeadlockGraph
FROM (SELECT CAST (target_data AS XML) AS TargetData
    FROM sys.dm_xe_session_targets st 
    INNER HASH JOIN sys.dm_xe_sessions s ON s.address = st.event_session_address
    WHERE [name] = 'system_health') AS Data
CROSS APPLY TargetData.nodes ('//RingBufferTarget/event') AS XEventData (XEvent)
WHERE XEventData.XEvent.value('@name', 'varchar(4000)') = 'xml_deadlock_report'

杂凑2

哈希联接在开始探测匹配项之前会完全读取其构建输入以构造哈希表。结果,我们必须存储所有值,而不仅仅是从计划的探针端开始处理每个线程的值。因此,哈希联接使用tempdb工作表来存储XML数据,并且Expr1000以后的操作员对结果的每次访问都需要花费大量时间才能到达tempdb

存取缓慢

下面显示了慢速访问路径的更多详细信息:

细节慢

如果强制执行合并联接,则将对输入行进行排序(阻塞操作,就像对哈希联接的构建输入一样),从而产生类似的安排,其中tempdb由于数据的大小,需要通过排序优化的工作表进行缓慢的访问。

由于执行计划中看不到的各种原因,处理大型数据项的计划可能会有问题。使用散列连接(表达式在正确的输入上)不是一个好的解决方案。它依赖于未记录的内部行为,但不能保证下周将以相同的方式工作,或者查询会稍有不同。

所传达的信息是,XML今天进行优化可能是棘手的事情。与XML上面显示的内容相比,在切碎之前将_s 写入变量表或临时表要更可靠。一种方法是:

DECLARE @data xml =
        CONVERT
        (
            xml,
            (
            SELECT TOP (1)
                dxst.target_data
            FROM sys.dm_xe_sessions AS dxs 
            JOIN sys.dm_xe_session_targets AS dxst ON
                dxst.event_session_address = dxs.[address]
            WHERE 
                dxs.name = N'system_health'
                AND dxst.target_name = N'ring_buffer'
            )
        )

SELECT XEventData.XEvent.value('(data/value)[1]', 'varchar(max)')
FROM @data.nodes ('./RingBufferTarget/event[@name eq "xml_deadlock_report"]') AS XEventData (XEvent)
WHERE XEventData.XEvent.value('@name', 'varchar(4000)') = 'xml_deadlock_report';

最后,我只想从下面的评论中添加Martin的非常漂亮的图形:

马丁的图形


很好的解释,谢谢。我也阅读了您关于计算标量的文章,但这里没有将两个和两个放在一起。
马丁·史密斯

3
我昨天尝试进行剖析的过程一定弄乱了(也许弄乱了慢速和快速的痕迹!)。我今天重做了,当然它只是显示您已经说过的话。
马丁·史密斯

2
是的,屏幕截图是Visual Studio 2012分析器中的“调用树视图”报告。我认为方法名称在您的输出中看起来更加清晰,尽管没有@@IEAAXPEA_K出现神秘字符串。
马丁·史密斯

10

那是我最初在这里发布的文章中的代码:

http://www.sqlservercentral.com/articles/deadlock/65658/

如果您阅读了这些注释,将会发现一些其他替代方法,这些替代方法都不会遇到您遇到的性能问题,一种替代方法是使用对该原始查询的修改,另一种替代方法是使用变量在处理XML之前保存XML,这样可以解决问题。更好。(请参阅我在第2页上的评论)来自DMV的XML处理起来可能很慢,就像从DMF解析XML到文件目标一样,通常最好先将数据读入临时表然后进行处理,以更好地实现。与使用.NET或SQLCLR等相比,SQL中的XML速度较慢。


1
谢谢!做到了。一个不带变量的变量需要600毫秒和6341的读取时间,而带变量303 ms3249 lob reads。在2012年,我还需要添加and target_name='ring_buffer'该版本,因为现在似乎有两个目标。不过,我仍在尝试获得20分钟版本在做什么的心理印象。
马丁·史密斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.