Очень странная производительность с индексом XML

32

Мой вопрос основан на этом: https://stackoverflow.com/q/35575990/5089204

Чтобы дать ответ, я сделал следующий тест-сценарий.

Тестовый сценарий

Сначала я создаю тестовую таблицу и заполняю ее 100 000 строк. Случайное число (от 0 до 1000) должно приводить к ~ 100 строкам для каждого случайного числа. Этот номер помещается в столбец varchar и в качестве значения в ваш XML.

Затем я делаю вызов наподобие OP, где он нужен с .exist () и .nodes () с небольшим преимуществом на секунду, но оба занимают от 5 до 6 секунд. На самом деле, я делаю вызовы дважды: второй раз в порядке замены и с немного измененными параметрами поиска и с «// item» вместо полного пути, чтобы избежать ложных срабатываний через кэшированные результаты или планы.

Затем я создаю индекс XML и делаю те же вызовы

Теперь - что действительно удивило меня! - .nodesс полным путем гораздо медленнее , чем раньше (9 секунд) , но .exist()это до половины секунды, с полным путем даже примерно до 0,10 сек. (хотя .nodes()с коротким путем лучше, но все еще далеко позади .exist())

Вопросов:

Мои собственные тесты показывают вкратце: XML-индексы могут чрезвычайно взорвать базу данных. Они могут чрезвычайно ускорить процесс (см. Редактирование 2), но также могут замедлить ваши запросы. Я хотел бы понять, как они работают ... Когда нужно создавать индекс XML? Почему .nodes()с индексом может быть хуже, чем без? Как можно избежать негативного воздействия?

CREATE TABLE #testTbl(ID INT IDENTITY PRIMARY KEY, SomeData VARCHAR(100),XmlColumn XML);
GO

DECLARE @RndNumber VARCHAR(100)=(SELECT CAST(CAST(RAND()*1000 AS INT) AS VARCHAR(100)));

INSERT INTO #testTbl VALUES('Data_' + @RndNumber,
'<error application="application" host="host" type="exception" message="message" >
  <serverVariables>
    <item name="name1">
      <value string="text" />
    </item>
    <item name="name2">
      <value string="text2" />
    </item>
    <item name="name3">
      <value string="text3" />
    </item>
    <item name="name4">
      <value string="text4" />
    </item>
    <item name="name5">
      <value string="My test ' +  @RndNumber + '" />
    </item>
    <item name="name6">
      <value string="text6" />
    </item>
    <item name="name7">
      <value string="text7" />
    </item>
  </serverVariables>
</error>');

GO 100000

DECLARE @d DATETIME=GETDATE()
SELECT #testTbl.*
FROM #testTbl
CROSS APPLY XmlColumn.nodes('/error/serverVariables/item[@name="name5" and value/@string="My test 600"]') AS a(b);
SELECT CAST(GETDATE()-@d AS TIME) AS NodesFullPath_no_index;
GO

DECLARE @d DATETIME=GETDATE();
SELECT * 
FROM #testTbl
WHERE XmlColumn.exist('/error/serverVariables/item[@name="name5" and value/@string="My test 600"]') = 1;
SELECT CAST(GETDATE()-@d AS TIME) AS ExistFullPath_no_index;
GO

DECLARE @d DATETIME=GETDATE();
SELECT * 
FROM #testTbl
WHERE XmlColumn.exist('//item[@name="name5" and value/@string="My test 500"]') = 1;
SELECT CAST(GETDATE()-@d AS TIME) AS ExistShortPath_no_index;
GO

DECLARE @d DATETIME=GETDATE()
SELECT #testTbl.*
FROM #testTbl
CROSS APPLY XmlColumn.nodes('//item[@name="name5" and value/@string="My test 500"]') AS a(b);
SELECT CAST(GETDATE()-@d AS TIME) AS NodesShortPath_no_index;
GO

CREATE PRIMARY XML INDEX PXML_test_XmlColum1 ON #testTbl(XmlColumn);
CREATE XML INDEX IXML_test_XmlColumn2 ON #testTbl(XmlColumn) USING XML INDEX PXML_test_XmlColum1 FOR PATH;
GO

DECLARE @d DATETIME=GETDATE()
SELECT #testTbl.*
FROM #testTbl
CROSS APPLY XmlColumn.nodes('/error/serverVariables/item[@name="name5" and value/@string="My test 600"]') AS a(b);
SELECT CAST(GETDATE()-@d AS TIME) AS NodesFullPath_with_index;
GO

DECLARE @d DATETIME=GETDATE();
SELECT * 
FROM #testTbl
WHERE XmlColumn.exist('/error/serverVariables/item[@name="name5" and value/@string="My test 600"]') = 1;
SELECT CAST(GETDATE()-@d AS TIME) AS ExistFullPath_with_index;
GO

DECLARE @d DATETIME=GETDATE();
SELECT * 
FROM #testTbl
WHERE XmlColumn.exist('//item[@name="name5" and value/@string="My test 500"]') = 1;
SELECT CAST(GETDATE()-@d AS TIME) AS ExistShortPath_with_index;
GO

DECLARE @d DATETIME=GETDATE()
SELECT #testTbl.*
FROM #testTbl
CROSS APPLY XmlColumn.nodes('//item[@name="name5" and value/@string="My test 500"]') AS a(b);
SELECT CAST(GETDATE()-@d AS TIME) AS NodesShortPath_with_index;
GO

DROP TABLE #testTbl;

РЕДАКТИРОВАТЬ 1 - Результаты

Это один из результатов, когда SQL Server 2012 установлен локально на среднем ноутбуке. В этом тесте я не смог воспроизвести крайне негативное воздействие NodesFullPath_with_index, хотя оно медленнее, чем без индекса ...

NodesFullPath_no_index    6.067
ExistFullPath_no_index    6.223
ExistShortPath_no_index   8.373
NodesShortPath_no_index   6.733

NodesFullPath_with_index  7.247
ExistFullPath_with_index  0.217
ExistShortPath_with_index 0.500
NodesShortPath_with_index 2.410

РЕДАКТИРОВАТЬ 2 Тест с большим XML

В соответствии с предложением TT я использовал вышеупомянутый XML, но скопировал item-nodes, чтобы получить около 450 элементов. Я позволил хит-узлу быть очень высоко в XML (потому что я думаю, что .exist()он остановится при первом попадании, а .nodes()будет продолжаться)

Создание XML-индекса взорвало mdf-файл до ~ 21 ГБ, ~ 18 ГБ, кажется, принадлежит к индексу (!!!)

NodesFullPath_no_index    3min44
ExistFullPath_no_index    3min39
ExistShortPath_no_index   3min49
NodesShortPath_no_index   4min00

NodesFullPath_with_index  8min20
ExistFullPath_with_index  8,5 seconds !!!
ExistShortPath_with_index 1min21
NodesShortPath_with_index 13min41 !!!
Shnugo
источник

Ответы:

33

Конечно, здесь много чего происходит, поэтому нам просто нужно посмотреть, к чему это приведет.

Во-первых, разница во времени между SQL Server 2012 и SQL Server 2014 обусловлена ​​новым оценщиком мощности в SQL Server 2014. Вы можете использовать флаг трассировки в SQL Server 2014 для принудительного запуска старого оценщика, а затем вы увидите то же время характеристики в SQL Server 2014, как в SQL Server 2012.

Сравнение nodes()vs exist()не является справедливым, поскольку они не будут возвращать один и тот же результат, если в одной строке есть более одного сопоставленного элемента в XML. exist()будет возвращать одну строку из базовой таблицы независимо, тогда как nodes()потенциально может дать вам более одной строки, возвращенной для каждой строки в базовой таблице.
Мы знаем данные, но SQL Server не знает и должен составить план запроса, который учитывает это.

Чтобы сделать nodes()запрос эквивалентным exist()запросу, вы можете сделать что-то вроде этого.

SELECT testTbl.*
FROM testTbl
WHERE EXISTS (
             SELECT *
             FROM XmlColumn.nodes('/error/serverVariables/item[@name="name5" and value/@string="My test 600"]') AS a(b)
             )

С таким запросом нет никакой разницы между использованием nodes()или, exist()и это потому, что SQL Server строит практически одинаковый план для двух версий, не использующих индекс, и точно такой же план, когда индекс используется. Это верно как для SQL Server 2012, так и для SQL Server 2014.

Для меня в SQL Server 2012 запросы без индекса XML занимают 6 секунд, используя измененную версию nodes()запроса выше. Нет разницы между использованием полного или короткого пути. При наличии индекса XML версия с полным путем является самой быстрой и занимает 5 мс, а использование короткого пути - около 500 мс. Изучение планов запросов расскажет вам, почему существует различие, но короткая версия заключается в том, что при использовании короткого пути SQL Server выполняет поиск в индексе по короткому пути (с использованием поиска диапазона like) и возвращает 700000 строк перед тем, как отбрасывать строки, которые не совпадают по значению. При использовании полного пути SQL Server может использовать выражение пути непосредственно вместе со значением узла для поиска и возвращает только 105 строк с нуля для работы.

При использовании SQL Server 2014 и новой оценки кардинальности нет разницы в этих запросах при использовании индекса XML. Без использования индекса запросы все равно занимают столько же времени, но это 15 секунд. Очевидно, что здесь нет улучшения при использовании нового материала.

Не уверен, полностью ли я потерял понимание того, о чем на самом деле ваш вопрос, так как я изменил запросы, чтобы они были эквивалентны, но я верю, что теперь это так.

Почему nodes()запрос (оригинальная версия) с XML-индексом значительно медленнее, чем когда индекс не используется?

Ответ таков: оптимизатор плана запросов SQL Server делает что-то плохое, и в нем вводится оператор спула. Я не знаю почему, но хорошая новость заключается в том, что его больше нет с новым оценщиком мощности в SQL Server 2014.
При отсутствии индексов запрос занимает около 7 секунд, независимо от того, какой оценщик мощности используется. С индексом это занимает 15 секунд со старым оценщиком (SQL Server 2012) и около 2 секунд с новым оценщиком (SQL Server 2014).

Примечание. Приведенные выше результаты действительны для ваших данных испытаний. Может быть совсем другая история, чтобы сказать, если вы измените размер, форму или форму XML. Невозможно узнать наверняка без тестирования данных, которые у вас есть в таблицах.

Как работают XML-индексы

XML-индексы в SQL Server реализованы как внутренние таблицы. Первичный индекс XML создает таблицу с первичным ключом базовой таблицы плюс столбец идентификатора узла, всего 12 столбцов. Он будет иметь по одной строке на каждый, element/node/attribute etc.поэтому таблица, конечно, может стать действительно большой в зависимости от размера хранимого XML. Имея первичный индекс XML, SQL Server может использовать первичный ключ внутренней таблицы, чтобы найти узлы и значения XML для каждой строки в базовой таблице.

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

Из CREATE XML INDEX (Transact-SQL) :

VALUE
Создает вторичный XML-индекс для столбцов, где ключевые столбцы (значение узла и путь) первичного XML-индекса.

PATH
Создает вторичный XML-индекс для столбцов, построенных на значениях пути и значениях узлов в первичном XML-индексе. Во вторичном индексе PATH значения пути и узла являются ключевыми столбцами, которые обеспечивают эффективный поиск при поиске путей.

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

Поэтому, когда вы создаете индекс PATH, первый столбец в этом индексе является выражением пути, а второй столбец - значением в этом узле. На самом деле путь хранится в неком сжатом формате и перевернут. То, что он хранится в обратном порядке, делает его полезным при поиске с использованием выражений коротких путей. В вашем случае короткого пути, который вы искали //item/value/@string, //item/@nameи //item. Так как путь сохраняется обратные в столбце, SQL Server может использовать диапазон прятки с , like = '€€€€€€%где €€€€€€путем обратным. Когда вы используете полный путь, нет смысла его использовать, likeпоскольку весь путь закодирован в столбце, и значение также можно использовать в предикате поиска.

Ваши вопросы :

Когда следует создавать индекс XML?

В крайнем случае, если когда-либо. Лучше спроектировать базу данных, чтобы вам не приходилось использовать значения внутри XML для фильтрации в предложении where. Если вы заранее знаете, что вам нужно сделать это, вы можете использовать продвижение свойства, чтобы создать вычисляемый столбец, который вы можете индексировать при необходимости. Начиная с SQL Server 2012 SP1, у вас также есть выборочные индексы XML. Работа за сценой почти такая же, как и с обычными XML-индексами, только вы указываете выражение пути в определении индекса, и индексируются только совпадающие узлы. Таким образом, вы можете сэкономить много места.

Почему .nodes () с индексом может быть хуже, чем без?

Если для таблицы создан индекс XML, SQL Server всегда будет использовать этот индекс (внутренние таблицы) для получения данных. Это решение принимается до того, как оптимизатор скажет, что быстро, а что нет. Входные данные для оптимизатора переписываются с использованием внутренних таблиц, и после этого оптимизатор должен делать все возможное, как при обычном запросе. Когда индекс не используется, вместо него используется пара табличных функций. Суть в том, что вы не можете сказать, что будет быстрее без тестирования.

Как можно избежать негативного воздействия?

тестирование

Микаэль Эрикссон
источник
2
Ваши идеи о разнице .nodes()и .exist()убедительны. Также тот факт, что индекс с full path searchбыстрее, кажется, легко понять. Это будет означать: если вы создаете XML-индекс, вы всегда должны знать о негативном влиянии любого общего XPath ( //или, *или, ..или, [filter]или чего-либо, кроме простого Xpath ...). На самом деле, вы должны использовать только полный путь - довольно
хороший