Bardzo dziwna wydajność z indeksem XML

32

Moje pytanie opiera się na tym: https://stackoverflow.com/q/35575990/5089204

Aby udzielić odpowiedzi, wykonałem następujący scenariusz testowy.

Scenariusz testowy

Najpierw tworzę tabelę testową i wypełniam ją 100 000 wierszy. Liczba losowa (od 0 do 1000) powinna prowadzić do ~ 100 wierszy dla każdej liczby losowej. Liczba ta jest umieszczana w kolumnie varchar i jako wartość w pliku XML.

Następnie wykonuję wywołanie takie jak OP, które jest potrzebne przy użyciu .exist () i .nodes () z niewielką przewagą na sekundę, ale oba zajmują od 5 do 6 sekund. W rzeczywistości wykonuję połączenia dwa razy: drugi raz w kolejności zamienianej i z nieznacznie zmienionymi parametrami wyszukiwania oraz z „// item” zamiast pełnej ścieżki, aby uniknąć fałszywych wyników pozytywnych poprzez buforowane wyniki lub plany.

Następnie tworzę indeks XML i wykonuję te same wywołania

Teraz - co naprawdę mnie zaskoczyło! - .nodesz pełną ścieżką jest znacznie wolniejszy niż wcześniej (9 sekund), ale jest krótszy .exist()do pół sekundy, z pełną ścieżką nawet do około 0,10 sekundy. (podczas gdy .nodes()z krótka droga jest lepsza, ale nadal daleko w tyle .exist())

Pytania:

Moje własne testy są w skrócie: indeksy XML mogą bardzo wysadzić bazę danych. Mogą bardzo przyspieszyć rzeczy (s. Edycja 2), ale mogą również spowolnić twoje zapytania. Chciałbym zrozumieć, jak one działają ... Kiedy należy utworzyć indeks XML? Dlaczego .nodes()z indeksem może być gorzej niż bez? Jak można uniknąć negatywnego wpływu?

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;

EDYCJA 1 - Wyniki

To jeden wynik z SQL Server 2012 zainstalowanym lokalnie na średnim laptopie. W tym teście nie mogłem odtworzyć wyjątkowo negatywnego wpływu NodesFullPath_with_index, chociaż jest on wolniejszy niż bez indeksu ...

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

EDYCJA 2 Test z większym XML

Zgodnie z sugestią TT użyłem powyższego kodu XML, ale skopiowałem item-nodes, aby osiągnąć około 450 pozycji. Pozwoliłem, aby węzeł trafienia znajdował się bardzo wysoko w pliku XML (ponieważ myślę, .exist()że zatrzymałoby się to przy pierwszym trafieniu, a .nodes()kontynuowałoby)

Utworzenie indeksu XML wysadziło plik mdf do ~ 21 GB, ~ 18 GB wydaje się należeć do indeksu (!!!)

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
źródło

Odpowiedzi:

33

Na pewno wiele się tutaj dzieje, więc będziemy musieli zobaczyć, dokąd to prowadzi.

Po pierwsze, różnica w czasie między SQL Server 2012 a SQL Server 2014 wynika z nowego estymatora liczności w SQL Server 2014. Możesz użyć flagi śledzenia w SQL Server 2014, aby wymusić stary estymator, a wtedy zobaczysz ten sam czas cechy w SQL Server 2014 jak w SQL Server 2012.

Porównywanie nodes()vs exist()nie jest sprawiedliwe, ponieważ nie zwrócą tego samego wyniku, jeśli w pliku XML znajduje się więcej niż jeden dopasowany element. exist()zwróci jeden wiersz z tabeli bazowej niezależnie, podczas gdy nodes()potencjalnie może dać więcej niż jeden wiersz zwrócony dla każdego wiersza w tabeli podstawowej.
Znamy dane, ale SQL Server nie zna i musi zbudować plan zapytań, który bierze to pod uwagę.

Aby nodes()zapytanie było równoważne z exist()zapytaniem, możesz zrobić coś takiego.

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

Przy takim zapytaniu nie ma różnicy między używaniem nodes()i exist()i dlatego, że SQL Server buduje prawie ten sam plan dla dwóch wersji nieużywających indeksu i dokładnie taki sam plan, gdy indeks jest używany. Dotyczy to zarówno SQL Server 2012, jak i SQL Server 2014.

Dla mnie w SQL Server 2012 zapytania bez indeksu XML zajmują 6 sekund przy użyciu zmodyfikowanej wersji nodes()powyższego zapytania. Nie ma różnicy między użyciem pełnej ścieżki lub krótkiej ścieżki. Po wprowadzeniu indeksu XML pełna wersja ścieżki jest najszybsza i zajmuje 5 ms, a użycie krótkiej ścieżki zajmuje około 500 ms. Analiza planów zapytań pokaże, dlaczego istnieje różnica, ale krótka wersja jest taka, że ​​gdy używasz krótkiej ścieżki, SQL Server szuka indeksu na krótkiej ścieżce (szukanie zakresu za pomocą like) i zwraca 700000 wierszy przed odrzuceniem wierszy, które nie pasują do wartości. Podczas korzystania z pełnej ścieżki SQL Server może używać wyrażenia ścieżki bezpośrednio wraz z wartością węzła do wyszukiwania i zwraca tylko 105 wierszy od zera do pracy.

Korzystając z SQL Server 2014 i nowego estymatora cardinalty, nie ma różnicy w tych zapytaniach przy użyciu indeksu XML. Bez użycia indeksu zapytania nadal zajmują tyle samo czasu, ale jest to 15 sekund. Wyraźnie nie jest to poprawa w przypadku korzystania z nowych rzeczy.

Nie jestem pewien, czy całkowicie straciłem świadomość tego, o co właściwie chodzi w twoim pytaniu, ponieważ zmodyfikowałem zapytania, aby były równoważne, ale oto, co moim zdaniem jest teraz.

Dlaczego nodes()zapytanie (wersja oryginalna) z indeksem XML jest znacznie wolniejsze niż wtedy, gdy indeks nie jest używany?

Odpowiedź brzmi: optymalizator planu zapytań SQL Server robi coś złego i wprowadza operatora buforowania. Nie wiem dlaczego, ale dobrą wiadomością jest to, że nie ma go już dzięki nowemu estymatorowi cardinalty w SQL Server 2014.
Bez żadnych indeksów zapytanie zajmuje około 7 sekund bez względu na to, jaki estymator kardynalności jest używany. Z indeksem zajmuje 15 sekund w przypadku starego estymatora (SQL Server 2012) i około 2 sekund w przypadku nowego estymatora (SQL Server 2014).

Uwaga: powyższe ustalenia dotyczą danych testowych. Może się zdarzyć, że zmienisz rozmiar, kształt lub formę pliku XML. Nie ma możliwości, aby się upewnić bez testowania z danymi, które faktycznie masz w tabelach.

Jak działają indeksy XML

Indeksy XML w SQL Server są implementowane jako tabele wewnętrzne. Główny indeks XML tworzy tabelę z kluczem podstawowym tabeli podstawowej i kolumną identyfikatora węzła, łącznie 12 kolumn. Będzie miał jeden wiersz na, element/node/attribute etc.dzięki czemu tabela może oczywiście stać się naprawdę duża, w zależności od wielkości przechowywanego pliku XML. Po wprowadzeniu podstawowego indeksu XML program SQL Server może używać klucza podstawowego tabeli wewnętrznej do lokalizowania węzłów XML i wartości dla każdego wiersza w tabeli podstawowej.

Wtórne indeksy XML występują w trzech typach. Podczas tworzenia wtórnego indeksu XML w tabeli wewnętrznej tworzony jest indeks nieklastrowany, który w zależności od typu tworzonego indeksu dodatkowego będzie miał różne kolumny i kolejność kolumn.

Z CREATE XML INDEX (Transact-SQL) :

WARTOŚĆ
Tworzy wtórny indeks XML w kolumnach, w których kluczowymi kolumnami są (wartość i ścieżka węzła) podstawowego indeksu XML.

ŚCIEŻKA
Tworzy wtórny indeks XML na kolumnach zbudowanych na wartościach ścieżek i wartości węzłów w podstawowym indeksie XML. W indeksie wtórnym PATH wartości ścieżki i węzła są kluczowymi kolumnami, które umożliwiają wydajne wyszukiwanie podczas wyszukiwania ścieżek.

PROPERTY
Tworzy wtórny indeks XML na kolumnach (PK, ścieżka i wartość węzła) podstawowego indeksu XML, gdzie PK jest kluczem podstawowym tabeli podstawowej.

Dlatego podczas tworzenia indeksu PATH pierwsza kolumna w tym indeksie jest wyrażeniem ścieżki, a druga kolumna jest wartością w tym węźle. W rzeczywistości ścieżka jest przechowywana w pewnego rodzaju skompresowanym formacie i odwrócona. Przechowywanie w odwrotnej kolejności sprawia, że ​​jest on użyteczny w wyszukiwaniu przy użyciu wyrażeń krótkiej ścieżki. W przypadku krótkiej ścieżki, której szukałeś //item/value/@string, //item/@nameoraz //item. Ponieważ ścieżka jest przechowywana w kolumnie odwróconej, SQL Server może używać wyszukiwania w zakresie, like = '€€€€€€%gdzie €€€€€€ścieżka jest odwrócona. Kiedy używasz pełnej ścieżki, nie ma powodu, aby używać, likeponieważ cała ścieżka jest zakodowana w kolumnie, a wartość może być również użyta w predykacie wyszukiwania.

Twoje pytania :

Kiedy należy utworzyć indeks XML?

W ostateczności, jeśli kiedykolwiek. Lepiej zaprojektuj bazę danych, abyś nie musiał używać wartości w XML do filtrowania w klauzuli where. Jeśli wiesz wcześniej, że musisz to zrobić, możesz użyć promocji nieruchomości, aby utworzyć kolumnę obliczeniową, którą możesz indeksować w razie potrzeby. Od wersji SQL Server 2012 SP1 dostępne są również selektywne indeksy XML. Działania za sceną są prawie takie same jak w przypadku zwykłych indeksów XML, tylko Ty określasz wyrażenie ścieżki w definicji indeksu i indeksowane są tylko pasujące do niego węzły. W ten sposób możesz zaoszczędzić dużo miejsca.

Dlaczego .nodes () z indeksem może być gorszy niż bez?

Gdy w tabeli utworzony jest indeks XML, SQL Server zawsze użyje tego indeksu (wewnętrznych tabel), aby uzyskać dane. Ta decyzja jest podejmowana, zanim optymalizator nie wypowie się na temat tego, co jest szybkie, a co nie jest szybkie. Dane wejściowe do optymalizatora są przepisywane, więc korzysta z wewnętrznych tabel, a następnie optymalizator musi zrobić wszystko, co w jego mocy, jak w przypadku zwykłego zapytania. Gdy nie stosuje się żadnego indeksu, zamiast niego używanych jest kilka funkcji o wartościach przechowywanych w tabeli. Najważniejsze jest to, że nie można powiedzieć, co będzie szybsze bez testowania.

Jak można uniknąć negatywnego wpływu?

Testowanie

Mikael Eriksson
źródło
2
Twoje pomysły na różnicę .nodes()i .exist()są przekonujące. Również fakt, że indeks z full path searchjest szybszy, wydaje się łatwy do zrozumienia. Oznaczałoby to: Jeśli utworzyć indeks XML, należy zawsze mieć świadomość negatywnego wpływu każdej z XPath generycznego ( //lub *lub ..lub [filter]lub cokolwiek nie po prostu XPath ...). W rzeczywistości powinieneś korzystać tylko z pełnej ścieżki - całkiem niezły remis ...
Shnugo,