Производительность XML-столбца SQL Server - PullRequest
1 голос
/ 07 июня 2019

Преобразование столбцов nText, содержащих XML, в тип данных XML привело к снижению производительности в SQL Server.

В настоящее время я работаю над проектом, в котором столбцы nText использовались для хранения действительного XML.Я успешно перенес эти столбцы в тип данных XML.Однако, согласно SQL Profiler, производительность типа данных XML хуже, чем использование nText или nvarchar (max) для хранения XML.Все, что я прочитал, подразумевает, что это не должно иметь место.

Чтобы проверить это, я создал две таблицы с одинаковыми индексами и т. Д.

Table Name Order1
[id] [int] IDENTITY(1,1) NOT NULL,
[uid] [varchar](36) NOT NULL,
[AffiliateId] [varchar](36) NOT NULL,
[Address] [ntext] NOT NULL,
[CustomProperties] [ntext] NOT NULL,
[OrderNumber] [nvarchar](50) NOT NULL,
...

Table Name Order2
[id] [int] IDENTITY(1,1) NOT NULL,
[uid] [varchar](36) NOT NULL,
[AffiliateId] [varchar](36) NOT NULL,
[Address] [xml] NOT NULL,
[CustomProperties] [xml] NOT NULL,
[OrderNumber] [nvarchar](50) NOT NULL,
...

Затем я скопировал данные, используявыберите / вставьте оператор и перестроите индексы обеих таблиц.Затем я создал скрипт со следующим SQL.

DBCC DROPCLEANBUFFERS
GO
--Part1
Select id, uid, AffiliateId, Address, CustomProperties, OrderNumber from [dbo].[Order1] where uid = 'F96045F8-A2BD-4C02-BECB-6EF22C9E473F'
Select id, uid, AffiliateId, Address, CustomProperties, OrderNumber from [dbo].[Order1] where uid = 'A3B71348-EB68-4600-9550-EC2CF75698F4'
Select id, uid, AffiliateId, Address, CustomProperties, OrderNumber from [dbo].[Order1] where uid = 'CB114D91-F000-4553-8AFE-FC20CF6AD8C0'
Select id, uid, AffiliateId, Address, CustomProperties, OrderNumber from [dbo].[Order1] where uid = '06274E4F-E233-4594-B505-D4BAA3770F0A'

DBCC DROPCLEANBUFFERS
GO
--Part2
Select id, uid, AffiliateId, Address, OrderNumber,  
CAST(CustomProperties AS xml).query('CustomProperty/Key[text()="AgreedToTerms"]/../Value/text()')  as "TermsAgreed" 
from Order1

DBCC DROPCLEANBUFFERS
GO
--Part3
Insert Into Order1 uid, AffiliateId, Address, CustomProperties, OrderNumber
Select NewId(), AffiliateId, Address, CustomProperties, OrderNumber + 'X' from [dbo].[Order1] where uid = 'F96045F8-A2BD-4C02-BECB-6EF22C9E473F'

Insert Into Order1 uid, AffiliateId, Address, CustomProperties, OrderNumber
Select NewId(), AffiliateId, Address, CustomProperties, OrderNumber + 'X' from [dbo].[Order1] where uid = 'A3B71348-EB68-4600-9550-EC2CF75698F4'

Insert Into Order1 uid, AffiliateId, Address, CustomProperties, OrderNumber
Select NewId(), AffiliateId, Address, CustomProperties, OrderNumber + 'X' from [dbo].[Order1] where  uid = 'CB114D91-F000-4553-8AFE-FC20CF6AD8C0'

Insert Into Order1 uid, AffiliateId, Address, CustomProperties, OrderNumber
Select NewId(), AffiliateId, Address, CustomProperties, OrderNumber + 'X' from [dbo].[Order1] where uid = '06274E4F-E233-4594-B505-D4BAA3770F0A'

DBCC DROPCLEANBUFFERS
GO
-- Part4 This updates a .5M row table.
Update [dbo].[Order1] Set CustomProperties = Cast(CustomProperties as NVARCHAR(MAX)) + CAST('' as NVARCHAR(MAX)), Address = Cast(CustomProperties as NVARCHAR(MAX)) + CAST('' as NVARCHAR(MAX))

Средние результаты, полученные с помощью SQL Profiler, следующие: -

NTEXT

+-------+-------------+-------------+-------------+-------------+
| Test  |     CPU     |    Reads    |   Writes    |  Duration   |
+-------+-------------+-------------+-------------+-------------+
| Part1 | 281.3333333 | 129.3333333 |           0 |         933 |
| Part2 | 78421.66667 |     5374306 | 10.66666667 | 47493.66667 |
| Part3 | 281.6666667 |         616 | 27.66666667 | 374.6666667 |
| Part4 | 40312.33333 | 15311252.67 |      320662 |       67010 |
| Total |             |             |             | 115811.3333 |
+-------+-------------+-------------+-------------+-------------+


XML

+-------+-------------+-------------+-------------+-------------+
| Test  |     CPU     |    Reads    |   Writes    |  Duration   |
+-------+-------------+-------------+-------------+-------------+
| Part1 |         282 | 58.33333333 |           0 | 949.3333333 |
| Part2 | 21129.66667 | 180143.3333 |           0 | 76048.66667 |
| Part3 |         297 | 370.3333333 | 14.66666667 |         378 |
| Part4 | 112578.3333 | 8908940.667 | 145703.6667 | 114684.3333 |
| Total |             |             |             | 192060.3333 |
+-------+-------------+-------------+-------------+-------------+

Является ли тестовый скрипт некорректным?Или есть какая-то другая оптимизация, которая должна быть выполнена для столбцов типа данных xml вне https://docs.microsoft.com/en-us/previous-versions/sql/sql-server-2005/administrator/ms345115(v=sql.90)

Я ожидаю, что тип столбца XML будет превосходить ntext.

1 Ответ

3 голосов
/ 07 июня 2019

Так что это может быть не ответ, по крайней мере, не решение, но, надеюсь, поможет понять, что происходит ...

Самая дорогая часть XML - это начальный анализ, помещенный в другиеwords: преобразование между текстовым представлением и техническим хранилищем.

Важно знать: собственный XML хранится не в виде текста, который вы видите, а в виде иерархической таблицы.Это требует очень сложной обработки, когда вы передаете текстовый XML в SQL-сервер.Вызов этого XML для читателя-человека требует обратного процесса.Хранить эту строку в строковом столбце (помните, что NTEXT устарела на протяжении веков) быстрее, чем хранить ее как собственный XML, но вы потеряете много преимуществ.

Итак, для вашего сценария:

Я предполагаю, что вы запустили тот же скрипт, но только что изменили Order1 на Order2.Это правильно?

Часть 1 измеряет простое SELECT.

Чтобы обеспечить читаемое представление, SQL-сервер (или, скорее, SSMS) преобразует любое значение в какой-то текст,Если бы ваши таблицы содержали INT, GUID или DateTime, вы бы не увидели реальный битовый шаблон, не так ли?SSMS будет использовать довольно дорогие действия, чтобы создать что-то читаемое для вас.Дорогая часть - это трансформация.Для строк это не нужно, поэтому NTEXT будет работать быстрее.

Часть 2 измеряет метод .query() (также с точки зрения «как представить результат»).

Использовали ли выCAST( AS XML) с Order2 тоже?Тем не менее, при такой необходимости XML должен быть быстрее, потому что NTEXT придется многократно выполнять тяжелый анализ, в то время как XML уже хранится в формате с запросом ... Но ваш XQuery довольно неоптимален (из-за обратной навигации../Value).Попробуйте это:

 .query('/CustomProperty[Key[text()="AgreedToTerms"]]/Value/text()')

Это будет искать <CustomProperty>, где есть <Key> с заданным содержимым, и будет читать <Value> ниже <CustomProperty> без необходимости ../

Я бы наверняка ожидал, что XML превзойдет NTEXT с CAST здесь ... Самый первый вызов *1038* для совершенно новых таблиц и индексов может возвращать смещенные результаты ...

Часть 3 измеряет вставки

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

Часть 4 измеряет обновления

Это выглядит довольно странно ... Чего вы пытаетесь достичь?Код должен преобразовывать ваши собственные XML-файлы в строки и повторно анализировать их для сохранения в XML.Чтобы сделать то же самое с NTEXT, эти дорогостоящие действия вообще не потребуются ...

Некоторые общие соображения

  1. Если вы получили какой-то XML из за пределами , прочитайте егоиз файла, и вам нужно запросить его только один раз, строковые методы для строковых типов могут быть быстрее, но: если вы хотите хранить XML постоянно, чтобы чаще использовать и манипулировать его значениями, собственный XMLтипа будет намного лучше.
  2. Во многих случаях показатели производительности не измеряют то, что, по вашему мнению, вы делаете ...
  3. Старайтесь создавать свои тесты таким образом, чтобы представление результатов былоне является частью теста (например, выполните INSERT для временной таблицы, остановите часы и выдвиньте выходные данные из временной таблицы)

ОБНОВЛЕНИЕ Еще один тест для "Part 2"

Попробуйте этот тестовый скрипт:

USE master;
GO
CREATE DATABASE testShnugo;
GO
USE testShnugo;
GO
CREATE TABLE dbo.WithString(ID INT,SomeXML NTEXT);
CREATE TABLE dbo.WithXML(ID INT,SomeXML XML);
GO
--insert 100.000 rows to both tables
WITH Tally(Nmbr) AS (SELECT TOP 100000 ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values v1 CROSS JOIN master..spt_values v2)
INSERT INTO dbo.WithXML(ID,SomeXML) 
SELECT Nmbr,(SELECT Nmbr AS [@nmbr],CONCAT('hallo',Nmbr) AS [SomeTest/@FindMe],CONCAT('SomeTestValue',Nmbr) As [SomeTest] FOR XML PATH('row'),ROOT('root'),TYPE)
FROM Tally
--copy everything to the second table
INSERT INTO dbo.WithString(ID,SomeXML) SELECT ID,CAST(SomeXML AS NVARCHAR(MAX)) FROM dbo.WithXML; 
GO
--check the actual content
SELECT * FROM dbo.WithString;
SELECT * FROM dbo.WithXML;
GO

DECLARE @d DATETIME2=SYSUTCDATETIME();
SELECT * FROM dbo.WithString WHERE SomeXML LIKE '%FindMe="hallo333"%'
PRINT 'String-Method LIKE ' 
PRINT DATEDIFF(millisecond,@d,SYSUTCDATETIME());

SET @d=SYSUTCDATETIME();
SELECT * FROM dbo.WithString WHERE CAST(SomeXML AS xml).exist('/root/row[SomeTest[@FindMe="hallo333"]]')=1
PRINT 'CAST NTEXT to XML and .exist()' 
PRINT DATEDIFF(millisecond,@d,SYSUTCDATETIME());

SET @d=SYSUTCDATETIME();
SELECT * FROM dbo.WithXML WHERE CAST(SomeXML AS nvarchar(MAX)) LIKE '%FindMe="hallo333"%'
PRINT 'String-Method LIKE after CAST XML to NVARCHAR(MAX)' 
PRINT DATEDIFF(millisecond,@d,SYSUTCDATETIME());

SET @d=SYSUTCDATETIME();
SELECT * FROM dbo.WithXML WHERE SomeXML.exist('/root/row[SomeTest[@FindMe="hallo333"]]')=1
PRINT 'native XML with .exist()' 
PRINT DATEDIFF(millisecond,@d,SYSUTCDATETIME());

GO
USE master;
GO
DROP DATABASE testShnugo;

Сначала я создаю таблицы и заполняю их 100 000 XML, как это

<root>
  <row nmbr="1">
    <SomeTest FindMe="hallo1">SomeTestValue1</SomeTest>
  </row>
</root>

Мои результаты

String-Method LIKE 
836

CAST NTEXT to XML and .exist()
1962

String-Method LIKE after CAST XML to NVARCHAR(MAX)
1079

native XML with .exist()
911

Как и ожидалосьсамый быстрый подход - строковый метод для строкового типа на очень маленьких строках .Но - конечно - это не будет таким мощным, как сложный XQuery, и не сможет справиться с пространствами имен, множественными вхождениями и т. Д.

Самым медленным является приведение NTEXT к XML с .exist()

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

И 100.000 нетривиальных вызовов XQuery против 100.000 различных XML-файлов почти так же быстро, как и подход с использованием чистой строки.

ОБНОВЛЕНИЕ 2: большие XML-файлы

Я повторилпротестируйте большие XML-файлы, просто изменив приведенный выше код в одну строку

    SELECT Nmbr,(SELECT TOP 100 Nmbr AS [@nmbr],CONCAT('hallo',x.Nmbr) AS [SomeTest/@FindMe],CONCAT('SomeTestValue',x.Nmbr) As [SomeTest] FROM Tally x FOR XML PATH('row'),ROOT('root'),TYPE)

Теперь каждый XML-файл будет состоять из 100 <row> элементов.

<root>
  <row nmbr="1">
    <SomeTest FindMe="hallo1">SomeTestValue1</SomeTest>
  </row>
  <row nmbr="2">
    <SomeTest FindMe="hallo2">SomeTestValue2</SomeTest>
  </row>
  <row nmbr="3">
    <SomeTest FindMe="hallo3">SomeTestValue3</SomeTest>
  </row>
  ...more of them

С поиском FindMe="hallo333" это ничего не вернет, но времени найти, что возвращать нечего, нам достаточно:

String-Method LIKE 
71959

CAST NTEXT to XML and .exist()
74773

String-Method LIKE after CAST XML to NVARCHAR(MAX)
104380

native XML with .exist()
16374

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

Пожалуйста, дайте мне знать ваш результат тоже.

...