web-dev-qa-db-fra.com

Performances très étranges avec un index XML

Ma question est basée sur ceci: https://stackoverflow.com/q/35575990/5089204

Pour y répondre, j'ai fait le scénario de test suivant.

Scénario de test

Je crée d'abord une table de test et la remplis de 100 000 lignes. Un nombre aléatoire (0 à 1000) devrait conduire à ~ 100 lignes pour chaque nombre aléatoire. Ce nombre est placé dans un col varchar et en tant que valeur dans votre XML.

Ensuite, je fais un appel comme l'OP, il en a besoin avec .exist () et avec .nodes () avec un petit avantage pour la seconde, mais les deux prennent 5 à 6 secondes. En fait, je fais les appels deux fois: une deuxième fois dans l'ordre inversé et avec des paramètres de recherche légèrement modifiés et avec "// item" au lieu du chemin complet pour éviter les faux positifs via les résultats ou les plans mis en cache.

Ensuite, je crée un index XML et fais les mêmes appels

Maintenant - qu'est-ce qui m'a vraiment surpris! - le .nodes avec le chemin complet est beaucoup plus lent qu'auparavant (9 secondes) mais la .exist() est jusqu'à une demi-seconde, avec chemin complet même jusqu'à environ 0,10 sec. (tandis que .nodes() avec chemin court est mieux, mais toujours loin derrière .exist())

Des questions:

En bref, mes propres tests: les index XML peuvent faire exploser une base de données extrêmement. Ils peuvent accélérer considérablement les choses (voir éditer 2), mais peuvent également ralentir vos requêtes. J'aimerais comprendre comment ils fonctionnent ... Quand faut-il créer un index XML? Pourquoi .nodes() avec un index peut-il être pire que sans? Comment éviter l'impact négatif?

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 - Résultats

Ceci est un résultat avec SQL Server 2012 installé localement sur un ordinateur portable moyen Dans ce test, je n'ai pas pu reproduire l'impact extrêmement négatif sur NodesFullPath_with_index, Bien qu'il soit plus lent que sans l'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

Test EDIT 2 avec un XML plus grand

Selon la suggestion de TT, j'ai utilisé le XML ci-dessus, mais j'ai copié les nœuds item- pour atteindre environ 450 éléments. Je laisse le hit-node être très haut dans le XML (parce que je pense que .exist() s'arrêterait au premier hit, tandis que .nodes() continuerait)

La création de l'index XML a fait exploser le fichier mdf à ~ 21 Go, ~ 18 Go semblent appartenir à l'index (!!!)

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

Il se passe certainement beaucoup de choses ici, nous devrons simplement voir où cela mène.

Tout d'abord, la différence de synchronisation entre SQL Server 2012 et SQL Server 2014 est due au nouvel estimateur de cardinalité dans SQL Server 2014. Vous pouvez utiliser un indicateur de trace dans SQL Server 2014 pour forcer l'ancien estimateur, puis vous verrez le même timing dans SQL Server 2014 comme dans SQL Server 2012.

Comparer nodes() vs exist() n'est pas juste car ils ne renverront pas le même résultat s'il y a plus d'un élément correspondant dans le XML pour une ligne. exist() renverra une ligne de la table de base, alors que nodes() peut potentiellement vous donner plus d'une ligne retournée pour chaque ligne de la table de base.
Nous connaissons les données, mais SQL Server ne le fait pas et doit créer un plan de requête qui en tient compte.

Pour rendre la requête nodes() équivalente à la requête exist(), vous pouvez faire quelque chose comme ça.

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

Avec une requête comme celle-ci, il n'y a pas de différence entre l'utilisation de nodes() ou exist() et c'est parce que SQL Server crée presque le même plan pour les deux versions n'utilisant pas d'index et exactement le même plan lorsque l'index est utilisé. Cela est vrai à la fois pour SQL Server 2012 et SQL Server 2014.

Pour moi dans SQL Server 2012, les requêtes sans l'index XML prennent 6 secondes en utilisant la version modifiée de la requête nodes() ci-dessus. Il n'y a aucune différence entre utiliser le chemin complet ou le chemin court. Avec l'index XML en place, la version du chemin complet est la plus rapide et prend 5 ms et l'utilisation du chemin court prend environ 500 ms. L'examen des plans de requête vous dira pourquoi il existe une différence, mais la version courte est que lorsque vous utilisez un chemin court, SQL Server recherche dans l'index sur le chemin court (une recherche de plage en utilisant like) et renvoie 700000 lignes avant de supprimer les lignes qui ne correspondent pas à la valeur. Lorsque vous utilisez le chemin d'accès complet, SQL Server peut utiliser l'expression de chemin d'accès directement avec la valeur du nœud pour effectuer la recherche et ne renvoie que 105 lignes à partir de zéro pour travailler.

À l'aide de SQL Server 2014 et du nouvel estimateur de cardinalty, il n'y a aucune différence dans ces requêtes lors de l'utilisation d'un index XML. Sans utiliser l'index, les requêtes prennent toujours le même temps, mais elles durent 15 secondes. Clairement pas une amélioration ici lors de l'utilisation de nouvelles choses.

Je ne sais pas si j'ai complètement perdu la trace de votre question depuis que j'ai modifié les requêtes pour qu'elles soient équivalentes, mais voici ce que je pense que c'est maintenant.

Pourquoi la requête nodes() (version originale) avec un index XML en place est-elle beaucoup plus lente que lorsqu'un index n'est pas utilisé?

Eh bien, la réponse est que l'optimiseur de plan de requête SQL Server fait quelque chose de mal et qui introduit un opérateur de spoule. Je ne sais pas pourquoi, mais la bonne nouvelle est qu'il n'est plus là avec le nouvel estimateur de cardinalty dans SQL Server 2014.
Sans index en place, la requête prend environ 7 secondes, quel que soit l'estimateur de cardinalité utilisé. Avec l'index, cela prend 15 secondes avec l'ancien estimateur (SQL Server 2012) et environ 2 secondes avec le nouvel estimateur (SQL Server 2014).

Remarque: Les résultats ci-dessus sont valables avec vos données de test. Il peut y avoir une toute autre histoire à raconter si vous modifiez la taille, la forme ou la forme du XML. Aucun moyen de savoir avec certitude sans tester avec les données que vous avez réellement dans les tableaux.

Fonctionnement des index XML

Les index XML dans SQL Server sont implémentés en tant que tables internes. L'index XML principal crée la table avec la clé primaire de la table de base plus la colonne id du nœud, au total 12 colonnes. Il aura une ligne par element/node/attribute etc. Afin que la table puisse bien sûr devenir très grande en fonction de la taille du XML stocké. Avec un index XML principal en place, SQL Server peut utiliser la clé primaire de la table interne pour localiser les nœuds XML et les valeurs de chaque ligne de la table de base.

Les index XML secondaires sont de trois types. Lorsque vous créez un index XML secondaire, un index non clusterisé est créé sur la table interne et, selon le type d'index secondaire que vous créez, il aura des colonnes et des ordres de colonnes différents.

De CRÉER UN INDEX XML (Transact-SQL) :

VALEUR
Crée un index XML secondaire sur les colonnes où se trouvent les colonnes clés (valeur de nœud et chemin d'accès) de l'index XML principal.

CHEMIN
Crée un index XML secondaire sur des colonnes basées sur des valeurs de chemin et des valeurs de noeud dans l'index XML principal. Dans l'index secondaire PATH, les valeurs de chemin et de nœud sont des colonnes clés qui permettent des recherches efficaces lors de la recherche de chemins.

PROPRIÉTÉ
Crée un index XML secondaire sur les colonnes (PK, chemin et valeur de nœud) de l'index XML principal où PK est la clé primaire de la table de base.

Ainsi, lorsque vous créez un index PATH, la première colonne de cet index est l'expression du chemin et la deuxième colonne est la valeur de ce nœud. En fait, le chemin est stocké dans une sorte de format compressé et inversé. Le fait qu'il soit stocké inversé est ce qui le rend utile dans les recherches utilisant des expressions de chemin court. Dans votre cas de chemin court, vous avez recherché //item/value/@string, //item/@name Et //item. Étant donné que le chemin est stocké inversé dans la colonne, SQL Server peut utiliser une recherche de plage avec like = '€€€€€€%€€€€€€ Est le chemin inversé. Lorsque vous utilisez un chemin d'accès complet, il n'y a aucune raison d'utiliser like car le chemin entier est codé dans la colonne et la valeur peut également être utilisée dans le prédicat de recherche.

Vos questions :

Quand faut-il créer un index XML?

En dernier recours si jamais. Mieux vaut concevoir votre base de données afin que vous n'ayez pas à utiliser de valeurs dans XML pour filtrer dans une clause where. Si vous savez à l'avance que vous devez le faire, vous pouvez utiliser promotion de la propriété pour créer une colonne calculée que vous pouvez indexer si nécessaire. Depuis SQL Server 2012 SP1, vous disposez également d'index XML sélectifs. Le fonctionnement derrière la scène est à peu près le même qu'avec les index XML normaux, seulement vous spécifiez l'expression du chemin dans la définition d'index et seuls les nœuds qui correspondent sont indexés. De cette façon, vous pouvez économiser beaucoup d'espace.

Pourquoi .nodes () avec un index peut-il être pire que sans?

Lorsqu'un index XML est créé sur une table, SQL Server utilisera toujours cet index (les tables internes) pour obtenir les données. Cette décision est prise avant que l'optimiseur ait son mot à dire sur ce qui est rapide et ce qui ne l'est pas. L'entrée de l'optimiseur est réécrite de sorte qu'il utilise les tables internes et ensuite, c'est à l'optimiseur de faire de son mieux comme avec une requête régulière. Lorsqu'aucun index n'est utilisé, deux fonctions de table sont utilisées à la place. L'essentiel est que vous ne pouvez pas dire ce qui sera plus rapide sans test.

Comment éviter l'impact négatif?

Essai

33
Mikael Eriksson