перекрестное применение xml-запроса работает экспоненциально хуже по мере роста xml-документа

Что я имею

У меня есть XML-документ переменного размера, который необходимо проанализировать на MSSQL 2008 R2, который выглядит следующим образом:

<data item_id_type="1" cfgid="{4F5BBD5E-72ED-4201-B741-F6C8CC89D8EB}" has_data_event="False">
  <item name="1">
    <field id="{EA032B25-19F1-4C1B-BDDE-3113542D13A5}" type="2">0.506543009706267</field>
    <field id="{71014ACB-571B-4C72-9C9B-05458B11335F}" type="2">-0.79500402346138</field>
    <field id="{740C36E9-1988-413E-A1D5-B3E5B4405B45}" type="2">0.0152649050024924</field>
  </item>
  <item name="2">
    <field id="{EA032B25-19F1-4C1B-BDDE-3113542D13A5}" type="2">0.366096802804087</field>
    <field id="{71014ACB-571B-4C72-9C9B-05458B11335F}" type="2">-0.386642801354842</field>
    <field id="{740C36E9-1988-413E-A1D5-B3E5B4405B45}" type="2">0.031671174184115</field>
  </item>
</data>

.

Что я хочу

Мне нужно преобразовать его в обычный набор данных табличного типа, который выглядит так:

item_name field_id                             field_type  field_value
--------- ------------------------------------ ----------- ---------------
1         EA032B25-19F1-4C1B-BDDE-3113542D13A5 2           0.5065430097062
1         71014ACB-571B-4C72-9C9B-05458B11335F 2           -0.795004023461
1         740C36E9-1988-413E-A1D5-B3E5B4405B45 2           0.0152649050024
2         EA032B25-19F1-4C1B-BDDE-3113542D13A5 2           0.3660968028040
2         71014ACB-571B-4C72-9C9B-05458B11335F 2           -0.386642801354
2         740C36E9-1988-413E-A1D5-B3E5B4405B45 2           0.0316711741841
3         EA032B25-19F1-4C1B-BDDE-3113542D13A5 2           0.8839620369590
3         71014ACB-571B-4C72-9C9B-05458B11335F 2           -0.781459993268
3         740C36E9-1988-413E-A1D5-B3E5B4405B45 2           0.2284423515729

.

Что работает

Этот cross apply запрос создает желаемый результат:

create table #temp (x xml)

insert into #temp (x)
values ('
<data item_id_type="1" cfgid="{4F5BBD5E-72ED-4201-B741-F6C8CC89D8EB}" has_data_event="False">
  <item name="1">
    <field id="{EA032B25-19F1-4C1B-BDDE-3113542D13A5}" type="2">0.506543009706267</field>
    <field id="{71014ACB-571B-4C72-9C9B-05458B11335F}" type="2">-0.79500402346138</field>
    <field id="{740C36E9-1988-413E-A1D5-B3E5B4405B45}" type="2">0.0152649050024924</field>
  </item>
  <item name="2">
    <field id="{EA032B25-19F1-4C1B-BDDE-3113542D13A5}" type="2">0.366096802804087</field>
    <field id="{71014ACB-571B-4C72-9C9B-05458B11335F}" type="2">-0.386642801354842</field>
    <field id="{740C36E9-1988-413E-A1D5-B3E5B4405B45}" type="2">0.031671174184115</field>
  </item>
  <item name="3">
    <field id="{EA032B25-19F1-4C1B-BDDE-3113542D13A5}" type="2">0.883962036959074</field>
    <field id="{71014ACB-571B-4C72-9C9B-05458B11335F}" type="2">-0.781459993268713</field>
    <field id="{740C36E9-1988-413E-A1D5-B3E5B4405B45}" type="2">0.228442351572923</field>
  </item>
</data>
')

select c.value('(../@name)','varchar(5)') as item_name
      ,c.value('(@id)','uniqueidentifier') as field_id
      ,c.value('(@type)','int') as field_type
      ,c.value('(.)','nvarchar(15)') as field_value
from   #temp cross apply
       #temp.x.nodes('/data/item/field') as y(c)

drop table #temp

.

Проблема

Когда в XML несколько сотен (или меньше) элементов <item>, запрос выполняется нормально. Однако при наличии 1000 элементов <item> для завершения возврата строк в SSMS требуется 24 секунды. При наличии 6500 элементов <item> выполнение запроса cross apply занимает около 20 минут. У нас может быть 10-20 000 <item> элементов.

.

Вопросы

Что заставляет запрос cross apply работать так плохо в этом простом XML-документе и работать экспоненциально медленнее по мере роста набора данных?

Есть ли более эффективный способ преобразования XML-документа в табличный набор данных (в SQL)?


person James L.    schedule 13.06.2014    source источник


Ответы (2)


Что заставляет перекрестный запрос работать так плохо в этом простом XML-документе и работать экспоненциально медленнее по мере роста набора данных?

Это использование родительской оси для получения идентификатора атрибута из узла элемента.

Именно эта часть плана запроса является проблематичной.

введите здесь описание изображения

Обратите внимание на 423 строки, выходящие из нижней табличной функции.

Это дает вам добавление еще одного узла элемента с тремя узлами поля.

введите здесь описание изображения

Возвращено 732 строки.

Что, если мы удвоим узлы из первого запроса до 6 узлов элементов?

введите здесь описание изображения

Мы вернулись к колоссальным 1602 строкам.

Цифра 18 в верхней функции — это все узлы поля в вашем XML. У нас есть 6 элементов с тремя полями в каждом элементе. Эти 18 узлов используются во вложенных циклах, соединяющихся с другой функцией, поэтому 18 выполнений, возвращающих 1602 строки, дают, что она возвращает 89 строк за итерацию. Это просто точное количество узлов во всем XML. На самом деле это на один больше, чем все видимые узлы. Я не знаю почему. Вы можете использовать этот запрос для проверки общего количества узлов в вашем XML.

select count(*)
from @XML.nodes('//*, //@*, //*/text()') as T(X)  

Таким образом, алгоритм, используемый SQL Server для получения значения, когда вы используете родительскую ось .. в функции значений, заключается в том, что он сначала находит все узлы, которые вы уничтожаете, 18 в последнем случае. Для каждого из этих узлов он уничтожает и возвращает весь XML-документ и проверяет оператор фильтра для узла, который вам действительно нужен. Вот вам и экспоненциальный рост. Вместо использования родительской оси вы должны использовать одно дополнительное перекрестное применение. Сначала измельчите на предмете, а затем на поле.

select I.X.value('@name', 'varchar(5)') as item_name,
       F.X.value('@id', 'uniqueidentifier') as field_id,
       F.X.value('@type', 'int') as field_type,
       F.X.value('text()[1]', 'nvarchar(15)') as field_value
from #temp as T
  cross apply T.x.nodes('/data/item') as I(X)
  cross apply I.X.nodes('field') as F(X)

Я также изменил способ доступа к текстовому значению поля. Использование . заставит SQL Server искать дочерние узлы для field и объединять эти значения в результате. У вас нет дочерних значений, поэтому результат тот же, но лучше не включать эту часть в план запроса (оператор UDX).

План запроса не имеет проблемы с родительской осью, если вы используете XML-индекс, но вы все равно выиграете от изменения способа получения значения поля.

person Mikael Eriksson    schedule 13.06.2014
comment
Хотел бы я +5. Спасибо за подробное объяснение. Запросы XML теперь имеют гораздо больше смысла. - person James L.; 13.06.2014
comment
Отличная работа! Я использовал OPENROWSET и метод .query() с ужасными результатами производительности. Ваше решение сократило все время обработки до менее минуты! - person Phil Nicholas; 25.11.2014
comment
+1 за совет уничтожать узел за узлом (через cross apply), использовать text() и включать результаты плана выполнения. - person underscore_d; 09.09.2015

Добавление XML-индекса помогло. Теперь 6500 записей, выполнение которых заняло 20 минут, занимает ‹ 4 секунды.

create table #temp (id int primary key, x xml)
create primary xml index idx_x on #temp (x)

insert into #temp (id, x)
values (1, '
<data item_id_type="1" cfgid="{4F5BBD5E-72ED-4201-B741-F6C8CC89D8EB}" has_data_event="False">
  <item name="1">
    <field id="{EA032B25-19F1-4C1B-BDDE-3113542D13A5}" type="2">0.506543009706267</field>
    <field id="{71014ACB-571B-4C72-9C9B-05458B11335F}" type="2">-0.79500402346138</field>
    <field id="{740C36E9-1988-413E-A1D5-B3E5B4405B45}" type="2">0.0152649050024924</field>
  </item>
  <item name="2">
    <field id="{EA032B25-19F1-4C1B-BDDE-3113542D13A5}" type="2">0.366096802804087</field>
    <field id="{71014ACB-571B-4C72-9C9B-05458B11335F}" type="2">-0.386642801354842</field>
    <field id="{740C36E9-1988-413E-A1D5-B3E5B4405B45}" type="2">0.031671174184115</field>
  </item>
  <item name="3">
    <field id="{EA032B25-19F1-4C1B-BDDE-3113542D13A5}" type="2">0.883962036959074</field>
    <field id="{71014ACB-571B-4C72-9C9B-05458B11335F}" type="2">-0.781459993268713</field>
    <field id="{740C36E9-1988-413E-A1D5-B3E5B4405B45}" type="2">0.228442351572923</field>
  </item>
</data>
')

select c.value('(../@name)','varchar(5)') as item_name
      ,c.value('(@id)','uniqueidentifier') as field_id
      ,c.value('(@type)','int') as field_type
      ,c.value('(.)','nvarchar(15)') as field_value
from   #temp cross apply
       #temp.x.nodes('/data/item/field') as y(c)

drop table #temp
person James L.    schedule 13.06.2014
comment
Джеймс, вы можете проверить свои основные имена таблиц и имена индексов в tempdb (т. е. временные) с префиксом, поэтому 1000 пользователей могут иметь одну и ту же таблицу с одним и тем же индексом, и она не будет конфликтовать. По крайней мере, это верно для индексов, отличных от xml;) - person TomTom; 13.06.2014
comment
@TomTom - Ты прав. Помню, в 90-х у меня была проблема с этим. Не могу вспомнить, какая СУБД. Рад узнать, что это больше не проблема! - person James L.; 13.06.2014
comment
@TomTom Ваш комментарий слишком упрощен, поскольку он верен только для #tempTables с префиксом одинарного хэша. Если временная таблица имеет имя ##somethingLikeThis, т. е. имеет префикс из двух хэшей, она наверняка является глобальной. - person underscore_d; 09.09.2015