Prestazioni molto strane con un indice XML


32

La mia domanda si basa su questo: https://stackoverflow.com/q/35575990/5089204

Per dare una risposta lì ho fatto il seguente scenario di test.

Scenario di prova

Per prima cosa creo una tabella di test e la riempio con 100.000 righe. Un numero casuale (da 0 a 1000) dovrebbe portare a ~ 100 righe per ogni numero casuale. Questo numero viene inserito in un varchar col e come valore nel tuo XML.

Quindi faccio una chiamata come l'OP che ne ha bisogno con .exist () e con .nodes () con un piccolo vantaggio per il secondo, ma entrambi impiegano dai 5 ai 6 secondi. In effetti eseguo le chiamate due volte: una seconda volta in ordine scambiato e con parametri di ricerca leggermente modificati e con "// item" invece del percorso completo per evitare falsi positivi tramite risultati o piani memorizzati nella cache.

Quindi creo un indice XML e faccio le stesse chiamate

Ora, cosa mi ha davvero sorpreso! - la .nodescon percorso completo è molto più lento rispetto a prima (9 secondi), ma la .exist()è giù per mezzo secondo, con percorso completo anche fino a circa 0,10 sec. (mentre .nodes()con percorso breve è meglio, ma ancora molto indietro .exist())

Domande:

I miei test si presentano in breve: gli indici XML possono far esplodere un database estremamente. Possono velocizzare estremamente le cose (vedi modifica 2), ma possono anche rallentare le tue domande. Mi piacerebbe capire come funzionano ... Quando si dovrebbe creare un indice XML? Perché .nodes()con un indice può essere peggio che senza? Come si può evitare l'impatto negativo?

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 - Risultati

Questo è un risultato con SQL Server 2012 installato localmente su un laptop medio In questo test non sono riuscito a riprodurre l'impatto estremamente negativo su NodesFullPath_with_index, sebbene sia più lento che senza l'indice ...

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 con XML più grande

Secondo il suggerimento di TT, ho usato l'XML sopra, ma itemho copiato i nodi - nodo per raggiungere circa 450 elementi. Ho lasciato che l'hit-node fosse molto in alto nell'XML (perché penso che .exist()si fermerebbe al primo hit, mentre .nodes()continuerebbe)

La creazione dell'indice XML ha fatto saltare il file mdf a ~ 21 GB, ~ 18 GB sembrano appartenere all'indice (!!!)

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 !!!

Risposte:


33

C'è sicuramente molto da fare qui, quindi dovremo solo vedere dove questo porta.

Innanzitutto, la differenza di tempistica tra SQL Server 2012 e SQL Server 2014 è dovuta al nuovo stimatore di cardinalità in SQL Server 2014. È possibile utilizzare un flag di traccia in SQL Server 2014 per forzare il vecchio stimatore e quindi vedrai lo stesso tempismo caratteristiche in SQL Server 2014 come in SQL Server 2012.

Il confronto nodes()vs exist()non è corretto in quanto non restituiranno lo stesso risultato se ci sono più elementi corrispondenti nell'XML per una riga. exist()restituirà una riga dalla tabella di base a prescindere, mentre nodes()potenzialmente può darti più di una riga restituita per ogni riga nella tabella di base.
Conosciamo i dati ma SQL Server non lo fa e deve creare un piano di query che ne tenga conto.

Per rendere la nodes()query equivalente alla exist()query, potresti fare qualcosa del genere.

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

Con una query del genere non esiste alcuna differenza tra l'utilizzo nodes()o exist()e ciò perché SQL Server crea quasi lo stesso piano per le due versioni che non utilizzano un indice e esattamente lo stesso piano quando viene utilizzato l'indice. Questo è vero sia per SQL Server 2012 che per SQL Server 2014.

Per me in SQL Server 2012 le query senza l'indice XML richiedono 6 secondi utilizzando la versione modificata della nodes()query sopra. Non vi è alcuna differenza tra l'utilizzo del percorso completo o del percorso breve. Con l'indice XML in atto, la versione del percorso completo è la più veloce e richiede 5 ms e l'utilizzo del percorso breve richiede circa 500 ms. L'esame dei piani di query ti spiegherà perché esiste una differenza, ma la versione breve è che quando si utilizza un percorso breve, SQL Server cerca nell'indice sul percorso breve (un intervallo cerca utilizzando like) e restituisce 700000 righe prima di scartare le righe che non corrispondono sul valore. Quando si utilizza il percorso completo, SQL Server può utilizzare l'espressione del percorso direttamente insieme al valore del nodo per eseguire la ricerca e restituire solo 105 righe da zero su cui lavorare.

Utilizzando SQL Server 2014 e il nuovo stimatore cardinale, non vi è alcuna differenza in queste query quando si utilizza un indice XML. Senza utilizzare l'indice, le query richiedono ancora la stessa quantità di tempo, ma sono 15 secondi. Chiaramente non è un miglioramento qui quando si usano nuove cose.

Non sono sicuro di aver perso completamente la traccia di ciò che la tua domanda è in realtà da quando ho modificato le query per essere equivalenti, ma ecco cosa credo che sia ora.

Perché la nodes()query (versione originale) con un indice XML in atto è significativamente più lenta rispetto a quando non viene utilizzato un indice?

Bene, la risposta è che l'ottimizzatore del piano di query di SQL Server fa qualcosa di male e che sta introducendo un operatore di spool. Non so perché, ma la buona notizia è che non è più disponibile con il nuovo strumento per la stima della cardinalità in SQL Server 2014.
In assenza di indici, la query richiede circa 7 secondi, indipendentemente dallo strumento per la valutazione della cardinalità. Con l'indice ci vogliono 15 secondi con il vecchio stimatore (SQL Server 2012) e circa 2 secondi con il nuovo stimatore (SQL Server 2014).

Nota: i risultati sopra riportati sono validi con i dati del test. Può esserci una storia completamente diversa da raccontare se si modificano le dimensioni, la forma o la forma dell'XML. Non c'è modo di saperlo senza provare con i dati effettivamente presenti nelle tabelle.

Come funzionano gli indici XML

Gli indici XML in SQL Server sono implementati come tabelle interne. L'indice XML primario crea la tabella con la chiave primaria della tabella di base più la colonna ID nodo, per un totale di 12 colonne. Avrà una riga per ogni volta element/node/attribute etc.che la tabella può ovviamente diventare molto grande a seconda della dimensione dell'XML memorizzato. Con un indice XML primario in atto, SQL Server può utilizzare la chiave primaria della tabella interna per individuare nodi e valori XML per ogni riga nella tabella di base.

Gli indici XML secondari sono disponibili in tre tipi. Quando si crea un indice XML secondario, esiste un indice non cluster creato nella tabella interna e, a seconda del tipo di indice secondario creato, avrà colonne e ordini di colonne diversi.

Da CREATE XML INDEX (Transact-SQL) :

VALUE
Crea un indice XML secondario su colonne in cui le colonne chiave sono (valore e percorso del nodo) dell'indice XML primario.

PERCORSO
Crea un indice XML secondario su colonne basate su valori percorso e valori nodo nell'indice XML primario. Nell'indice secondario PATH, i valori path e node sono colonne chiave che consentono ricerche efficienti durante la ricerca di percorsi.

PROPRIETÀ
Crea un indice XML secondario su colonne (PK, percorso e valore nodo) dell'indice XML primario in cui PK è la chiave primaria della tabella di base.

Pertanto, quando si crea un indice PATH, la prima colonna in quell'indice è l'espressione del percorso e la seconda colonna è il valore in quel nodo. In realtà, il percorso è memorizzato in una sorta di formato compresso e invertito. Il fatto che sia memorizzato invertito è ciò che lo rende utile nelle ricerche che usano espressioni di percorsi brevi. Nel tuo breve percorso hai cercato //item/value/@string, //item/@namee //item. Poiché il percorso è archiviato invertito nella colonna, SQL Server può utilizzare un intervallo di ricerca con like = '€€€€€€%dove €€€€€€è il percorso invertito. Quando si utilizza un percorso completo, non vi è alcun motivo da utilizzare likepoiché l'intero percorso è codificato nella colonna e il valore può essere utilizzato anche nel predicato di ricerca.

Le vostre domande :

Quando si dovrebbe creare un indice XML?

Come ultima risorsa se mai. Meglio progettare il database in modo da non dover usare valori all'interno di XML per filtrare in una clausola where. Se si sa in anticipo che è necessario farlo, è possibile utilizzare la promozione delle proprietà per creare una colonna calcolata che è possibile indicizzare se necessario. Da SQL Server 2012 SP1 sono disponibili anche indici XML selettivi. I meccanismi dietro la scena sono praticamente gli stessi dei normali indici XML, solo tu specifichi l'espressione del percorso nella definizione dell'indice e solo i nodi corrispondenti sono indicizzati. In questo modo puoi risparmiare molto spazio.

Perché .nodes () con un indice può essere peggio che senza?

Quando esiste un indice XML creato su una tabella, SQL Server utilizzerà sempre quell'indice (le tabelle interne) per ottenere i dati. Tale decisione viene presa prima che l'ottimizzatore abbia voce in capitolo su ciò che è veloce e ciò che non è veloce. L'input per l'ottimizzatore viene riscritto in modo che utilizzi le tabelle interne e successivamente spetta all'ottimizzatore fare del suo meglio come con una query normale. Quando non viene utilizzato alcun indice, vengono invece utilizzate un paio di funzioni con valori di tabella. La linea di fondo è che non puoi dire cosa sarà più veloce senza test.

Come si può evitare l'impatto negativo?

analisi


2
Le tue idee sulla differenza .nodes()e .exist()sono convincenti. Anche il fatto che l'indice con full path searchsia più veloce sembra facile da capire. Ciò significherebbe: se crei un indice XML, devi sempre essere consapevole dell'influenza negativa con qualsiasi XPath generico ( //o *o ..o [filter]o qualsiasi altra cosa non solo Xpath ...). In effetti, dovresti usare solo il percorso completo - un grande pareggio ...
Shnugo,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.