Sehr seltsame Leistung mit einem XML-Index

32

Meine Frage basiert auf diesem: https://stackoverflow.com/q/35575990/5089204

Um dort eine Antwort zu geben, habe ich folgendes Test-Szenario gemacht.

Testszenario

Zuerst erstelle ich eine Testtabelle und fülle sie mit 100.000 Zeilen. Eine Zufallszahl (0 bis 1000) sollte für jede Zufallszahl ~ 100 Zeilen ergeben. Diese Zahl wird in eine varchar-Spalte und als Wert in Ihre XML-Datei eingefügt.

Dann rufe ich wie im OP dort mit .exist () und mit .nodes () mit einem kleinen Vorteil für die Sekunde auf, aber beide brauchen 5 bis 6 Sekunden. Tatsächlich rufe ich zweimal auf: ein zweites Mal in vertauschter Reihenfolge und mit leicht geänderten Suchparametern und mit "// item" anstelle des vollständigen Pfads, um Fehlalarme über zwischengespeicherte Ergebnisse oder Pläne zu vermeiden.

Dann erstelle ich einen XML-Index und mache die gleichen Aufrufe

Nun - was hat mich wirklich überrascht! - die .nodesmit vollem Pfad ist viel langsamer als zuvor (9 Sekunden), aber die .exist()ist auf eine halbe Sekunde reduziert , mit vollem Pfad sogar auf ungefähr 0,10 Sekunden. (zwar .nodes()mit kurzem weg besser, aber immer noch weit hinten .exist())

Fragen:

Meine eigenen Tests bringen es auf den Punkt: XML-Indizes können eine Datenbank extrem in die Luft jagen. Sie können die Dinge extrem beschleunigen (s. Bearbeiten 2), aber auch Ihre Abfragen verlangsamen. Ich würde gerne verstehen, wie sie funktionieren ... Wann sollte man einen XML-Index erstellen? Warum kann .nodes()mit einem Index schlechter sein als ohne? Wie kann man die negativen Auswirkungen vermeiden?

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;

EDIT 1 - Ergebnisse

Dies ist ein Ergebnis NodesFullPath_with_index, wenn SQL Server 2012 lokal auf einem mittleren Laptop installiert ist. In diesem Test konnte ich den extrem negativen Einfluss nicht reproduzieren , obwohl er langsamer ist als ohne den 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

EDIT 2 Test mit größerem XML

Gemäß dem Vorschlag von TT habe ich die obige XML- itemDatei verwendet, aber die Knoten kopiert , um ungefähr 450 Elemente zu erreichen. Ich habe den Hit-Node im XML sehr hoch gesetzt (weil ich denke, dass .exist()das beim ersten Treffer aufhören würde, während .nodes()es weitergeht)

Das Erstellen des XML-Indexes hat die Mdf-Datei auf ~ 21GB gesprengt, ~ 18GB scheinen zum Index zu gehören (!!!)

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
quelle

Antworten:

33

Hier ist sicher viel los, also müssen wir nur sehen, wohin das führt.

Zunächst einmal ist der Zeitunterschied zwischen SQL Server 2012 und SQL Server 2014 auf den neuen Kardinalitätsschätzer in SQL Server 2014 zurückzuführen. Sie können in SQL Server 2014 ein Ablaufverfolgungsflag verwenden, um den alten Schätzer zu erzwingen. Anschließend wird derselbe Zeitpunkt angezeigt Eigenschaften in SQL Server 2014 wie in SQL Server 2012.

Das Vergleichen von nodes()vs exist()ist nicht fair, da sie nicht dasselbe Ergebnis liefern, wenn mehr als ein übereinstimmendes Element in der XML für eine Zeile vorhanden ist. exist()gibt unabhängig davon eine Zeile aus der Basistabelle zurück, wobei nodes()möglicherweise mehr als eine Zeile für jede Zeile in der Basistabelle zurückgegeben wird.
Wir kennen die Daten, SQL Server jedoch nicht und muss einen Abfrageplan erstellen, der dies berücksichtigt.

Um die nodes()Abfrage mit der Abfrage gleichzusetzen exist(), können Sie so etwas tun.

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

Bei einer solchen Abfrage gibt es keinen Unterschied zwischen der Verwendung von nodes()oder exist(), da SQL Server für die beiden Versionen, die keinen Index verwenden, fast denselben Plan erstellt und genau denselben Plan, wenn der Index verwendet wird. Dies gilt sowohl für SQL Server 2012 als auch für SQL Server 2014.

Für mich in SQL Server 2012 dauern die Abfragen ohne den XML-Index mit der geänderten Version der nodes()obigen Abfrage 6 Sekunden . Es gibt keinen Unterschied zwischen der Verwendung des vollständigen Pfads und des kurzen Pfads. Wenn der XML-Index vorhanden ist, ist die Vollpfadversion die schnellste und benötigt 5 ms und die Verwendung des Kurzpfads dauert ca. 500 ms. Wenn Sie die Abfragepläne untersuchen, werden Sie feststellen, warum es einen Unterschied gibt. Die Kurzversion besteht jedoch darin, dass SQL Server bei Verwendung eines likeKurzpfads den Index des Kurzpfads (eine Bereichssuche mit ) durchsucht und 700000 Zeilen zurückgibt, bevor die Zeilen verworfen werden, die verwendet werden stimmen nicht mit dem Wert überein. Wenn der vollständige Pfad verwendet wird, kann SQL Server den Pfadausdruck direkt zusammen mit dem Wert des Knotens verwenden, um die Suche durchzuführen, und gibt nur 105 Zeilen von Grund auf neu zurück, um daran zu arbeiten.

Bei Verwendung von SQL Server 2014 und dem neuen Cardinalty Estimator gibt es keinen Unterschied bei diesen Abfragen, wenn ein XML-Index verwendet wird. Ohne Verwendung des Index dauern die Abfragen immer noch genauso lange, betragen jedoch 15 Sekunden. Klar keine Besserung hier bei der Verwendung neuer Sachen.

Ich bin mir nicht sicher, ob ich den Überblick darüber verloren habe, worum es bei Ihrer Frage tatsächlich geht, da ich die Abfragen so geändert habe, dass sie gleichwertig sind.

Warum ist die nodes()Abfrage (Originalversion) mit einem vorhandenen XML-Index wesentlich langsamer als ohne Verwendung eines Index?

Nun, die Antwort ist, dass das SQL Server-Abfrageplanoptimierungsprogramm etwas Schlechtes tut und einen Spool-Operator einführt. Ich weiß nicht warum, aber die gute Nachricht ist, dass es mit dem neuen Kardinalitätsschätzer in SQL Server 2014 nicht mehr da ist.
Ohne vorhandene Indizes dauert die Abfrage ungefähr 7 Sekunden, unabhängig davon, welcher Kardinalitätsschätzer verwendet wird. Mit dem Index dauert es 15 Sekunden mit dem alten Schätzer (SQL Server 2012) und ungefähr 2 Sekunden mit dem neuen Schätzer (SQL Server 2014).

Hinweis: Die obigen Ergebnisse gelten für Ihre Testdaten. Es kann eine ganz andere Geschichte zu erzählen geben, wenn Sie die Größe, Form oder Form des XML ändern. Keine Möglichkeit, es sicher zu wissen, ohne die Daten zu testen, die Sie tatsächlich in den Tabellen haben.

Wie funktionieren die XML-Indizes?

XML-Indizes in SQL Server werden als interne Tabellen implementiert. Der primäre XML-Index erstellt die Tabelle mit dem Primärschlüssel der Basistabelle plus der Knoten-ID-Spalte (insgesamt 12 Spalten). Es wird eine Zeile pro haben, element/node/attribute etc.so dass die Tabelle natürlich abhängig von der Größe des gespeicherten XML sehr groß werden kann. Wenn ein primärer XML-Index vorhanden ist, kann SQL Server den Primärschlüssel der internen Tabelle verwenden, um XML-Knoten und -Werte für jede Zeile in der Basistabelle zu suchen.

Es gibt drei Arten von sekundären XML-Indizes. Wenn Sie einen sekundären XML-Index erstellen, wird für die interne Tabelle ein nicht gruppierter Index erstellt. Je nachdem, welchen Typ von sekundärem Index Sie erstellen, werden unterschiedliche Spalten und Spaltenreihenfolgen verwendet.

Aus CREATE XML INDEX (Transact-SQL) :

VALUE
Erstellt einen sekundären XML-Index für Spalten, in denen sich Schlüsselspalten (Knotenwert und Pfad) des primären XML-Index befinden.

PATH
Erstellt einen sekundären XML-Index für Spalten, die auf Pfad- und Knotenwerten im primären XML-Index basieren. Im sekundären Index PATH sind die Pfad- und Knotenwerte Schlüsselspalten, die eine effiziente Suche bei der Suche nach Pfaden ermöglichen.

PROPERTY
Erstellt einen sekundären XML-Index für Spalten (PK, Pfad und Knotenwert) des primären XML-Index, wobei PK der Primärschlüssel der Basistabelle ist.

Wenn Sie also einen PATH-Index erstellen, ist die erste Spalte in diesem Index der Pfadausdruck und die zweite Spalte der Wert in diesem Knoten. Tatsächlich wird der Pfad in einer Art komprimiertem Format gespeichert und umgekehrt. Daß es umgekehrt gespeichert wird, macht es bei Suchvorgängen unter Verwendung von Kurzpfadausdrücken nützlich. In Ihrem Kurzwegfall haben Sie gesucht nach //item/value/@string, //item/@nameund //item. Da der Pfad in der Spalte umgekehrt gespeichert ist, kann SQL Server einen verwenden Bereich mit suchen , like = '€€€€€€%wo €€€€€€ist der Weg umgekehrt. Wenn Sie einen vollständigen Pfad verwenden, gibt es keinen Grund zur Verwendung, likeda der gesamte Pfad in der Spalte codiert ist und der Wert auch im Suchprädikat verwendet werden kann.

Ihre Fragen :

Wann sollte man einen XML-Index erstellen?

Als letzter Ausweg, wenn überhaupt. Entwerfen Sie Ihre Datenbank besser, damit Sie keine Werte in XML verwenden müssen, nach denen in einer where-Klausel gefiltert werden soll. Wenn Sie zuvor wissen, dass Sie dies tun müssen, können Sie mithilfe der Eigenschaftsheraufstufung eine berechnete Spalte erstellen, die Sie bei Bedarf indizieren können. Seit SQL Server 2012 SP1 stehen Ihnen auch ausgewählte XML-Indizes zur Verfügung. Die Funktionsweise hinter den Kulissen ähnelt der bei normalen XML-Indizes, nur dass Sie den Pfadausdruck in der Indexdefinition angeben und nur die übereinstimmenden Knoten indiziert werden. Auf diese Weise können Sie viel Platz sparen.

Warum kann .nodes () mit einem Index schlechter sein als ohne?

Wenn für eine Tabelle ein XML-Index erstellt wurde, verwendet SQL Server immer diesen Index (die internen Tabellen), um die Daten abzurufen. Diese Entscheidung wird getroffen, bevor der Optimierer entscheidet, was schnell ist und was nicht. Die Eingabe in den Optimierer wird neu geschrieben, sodass die internen Tabellen verwendet werden. Danach muss der Optimierer sein Bestes geben, wie bei einer regulären Abfrage. Wenn kein Index verwendet wird, werden stattdessen einige Tabellenwertfunktionen verwendet. Das Fazit ist, dass man ohne Tests nicht sagen kann, was schneller sein wird.

Wie kann man die negativen Auswirkungen vermeiden?

Testen

Mikael Eriksson
quelle
2
Ihre Vorstellungen über den Unterschied von .nodes()und .exist()überzeugen. Auch die Tatsache, dass der Index mit full path searchschneller ist, scheint leicht zu verstehen. Dies würde bedeuten: Wenn Sie einen XML-Index erstellen, müssen Sie sich immer des negativen Einflusses eines generischen XPath bewusst sein ( //oder *oder ..oder [filter]oder irgendetwas, das nicht nur Xpath ist ...). Tatsächlich sollten Sie nur den vollständigen Pfad verwenden - ein großartiger
Rückzieher