Оптимизация планов с помощью 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предложение, оно завершается менее чем за секунду, возвращая 3782 строки.

Точно так же, если я добавлю OPTION (MAXDOP 1)к исходному запросу, который тоже ускоряет процесс, статистика теперь показывает значительно меньшее количество операций чтения лобов.

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некоторой степени улучшает ситуацию (но это все еще занимает> 3 минуты), так как результаты DMV настолько малы, что я сомневаюсь, что сам тип 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.

(И план)

После заполнения расширенного кольцевого буфера событий ( DATALENGTHиз них XMLбыло 4 880 045 байт, и он содержал 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где в любое время выделяется от 1800 до 3000 страниц.

Мартин Смит
источник
Вы можете переместить WHEREпредложение в выражение XQuery; логика не должна быть удалена, чтобы все шло быстро TargetData.nodes ('RingBufferTarget[1]/event[@name = "xml_deadlock_report"]'). Тем не менее, я недостаточно хорошо знаю внутренности XML, чтобы ответить на поставленный вами вопрос.
Джон Зигель
Пейджинг @SQLPoolBoy для вас Мартин ... он предложил просмотреть здесь комментарии, где у него есть более эффективные предложения (они основаны на исходной статье для кода выше ).
Аарон Бертран

Ответы:

36

Причина разницы в производительности заключается в том, как скалярные выражения обрабатываются в механизме выполнения. В этом случае выражение интереса:

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

Эта метка выражения определяется оператором Compute Scalar (узел 11 в последовательном плане, узел 13 в параллельном плане). Скалярные операторы Compute отличаются от других операторов (начиная с SQL Server 2005) тем, что определяемые ими выражения не обязательно оцениваются в той позиции, в которой они появляются в видимом плане выполнения; оценка может быть отложена до тех пор, пока последующий оператор не потребует результата вычисления.

В данном запросе target_dataстрока обычно велика, что делает преобразование из строки в XMLдорогое. В медленных планах строка для XMLпреобразования выполняется каждый раз, когда более поздний оператор, которому требуется результат, Expr1000является отскоком.

Перепривязка происходит на внутренней стороне соединения вложенных циклов, когда изменяется коррелированный параметр (внешняя ссылка). Expr1000является внешней ссылкой для большинства объединений вложенных циклов в этом плане выполнения. На выражение многократно ссылаются несколько XML-ридеров, как потоковых агрегатов, так и запускающий фильтр. В зависимости от размера XMLчисло, в которое преобразуется строка, XMLможет легко исчисляться миллионами.

Стеки вызовов ниже показывают примеры target_dataпреобразования строки в XML( ConvertStringToXMLForES- где ES - Служба выражений ):

Фильтр запуска

Стек вызовов фильтра запуска

XML Reader (внутренний поток TVF)

Стек вызовов TVF Stream

Поток Агрегат

Поток совокупного стека вызовов

Преобразование строки в 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 и потоковыми агрегатами. Трассировка стека ниже показывает пример повторного использования кэшированного значения XML Reader.

Повторное использование кэша

Когда порядок соединения принудителен так, что определение 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переменную или временную таблицу перед уничтожением является гораздо более надежным решением, чем все, что показано выше. Один из способов сделать это:

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';

Наконец, я просто хочу добавить очень приятную графику Мартина из комментариев ниже:

Графика Мартина

Пол Уайт говорит, что GoFundMonica
источник
Отличное объяснение, спасибо. Я тоже читал вашу статью о вычислимых скалярах, но не собираю здесь два и два.
Мартин Смит
3
Должно быть, я что-то напутал с моей попыткой вчерашнего профилирования (возможно, запутал медленные и быстрые следы!). Я переделал это сегодня и, конечно, это просто показывает, что вы уже сказали.
Мартин Смит
2
Да, снимок экрана - это отчет о просмотре дерева вызовов из профилировщика Visual Studio 2012 . Я думаю, что имена методов выглядят намного более ясными в вашем выводе, хотя и без загадочных строк, таких как @@IEAAXPEA_Kпоявление.
Мартин Смит
10

Вот код из моей статьи, первоначально размещенной здесь:

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

Если вы прочтете комментарии, вы найдете пару альтернатив, у которых нет проблем с производительностью, с которыми вы столкнулись, одна использует модификацию этого исходного запроса, а другая - переменную для хранения XML перед его обработкой, которая сработает. лучше. (см. мои комментарии на стр. 2) XML из DMV может быть медленным для обработки, так же как и анализ XML из DMF для целевого файла, который часто лучше выполнить, сначала прочитав данные во временную таблицу, а затем обработав ее. XML в SQL медленнее по сравнению с использованием таких вещей, как .NET или SQLCLR.

Джонатан Кехайяс
источник
1
Благодарность! Это добилось цели. Тот, у которого нет переменной, принимает 600 мс, а 6341 читает, а с переменной 303 msи 3249 lob reads. В 2012 году мне нужно было также добавить and target_name='ring_buffer'эту версию, так как сейчас есть две цели. Я все еще пытаюсь понять, что именно он делает в 20-минутной версии.
Мартин Смит