Très étrange performance avec un index XML

32

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 donner environ 100 lignes pour chaque nombre aléatoire. Ce nombre est placé dans un 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 léger avantage pour la seconde, mais les deux prennent entre 5 et 6 secondes. En fait, je fais les appels deux fois: une seconde fois dans l'ordre échangé 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.

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

Maintenant, qu'est-ce qui m'a vraiment surpris? - le .nodesavec le chemin complet est beaucoup plus lente qu'auparavant (9 secs) , mais l' .exist()est vers le bas à une demi - seconde, avec le chemin complet , même jusqu'à environ 0,10 sec. (tandis .nodes()qu'avec court chemin c'est mieux, mais toujours loin derrière .exist())

Des questions:

En bref, mes propres tests: les index XML peuvent considérablement faire exploser une base de données. Ils peuvent considérablement accélérer les choses (par exemple, é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()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

C'est un résultat avec SQL Server 2012 installé localement sur un ordinateur portable moyen. Dans ce test, je ne pouvais pas reproduire l'impact extrêmement négatif NodesFullPath_with_index, bien que ce 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

EDIT 2 Test avec un XML plus grand

Selon la suggestion de TT, j'ai utilisé le itemcode XML ci-dessus, mais j'ai copié les nœuds pour atteindre environ 450 éléments. J'ai laissé le noeud de hit être très haut dans le XML (parce que je pense que ça .exist()s'arrêterait au premier hit, tout en .nodes()continuant)

La création de l'index XML a fait exploser le fichier mdf à environ 21 Go , environ 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 !!!
Shnugo
la source

Réponses:

33

Bien sûr, il se passe beaucoup de choses, il ne reste plus qu'à 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é de 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. caractéristiques dans SQL Server 2014 comme dans SQL Server 2012.

La comparaison de nodes()vs exist()n'est pas juste car ils ne renverront pas le même résultat s'il existe plus d'un élément correspondant dans le XML pour une ligne. exist()retournera une ligne de la table de base peu importe, 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 sait pas et doit créer un plan de requête tenant compte de cela.

Pour que la nodes()requête soit équivalente à la exist()requête, vous pouvez faire quelque chose comme ceci.

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 telle que celle-ci, il n’ya aucune différence entre nodes()ou exist()et c’est que SQL Server construit à peu près 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 index XML prennent 6 secondes à l'aide de la version modifiée de la nodes()requête ci-dessus. Il n'y a pas de 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. L'utilisation du chemin court prend environ 500 ms. L'examen des plans de requête vous indiquera pourquoi il existe une différence, mais la version abrégée indique que lorsque vous utilisez un chemin d'accès court, SQL Server recherche l'index sur le chemin d'accès court (une recherche de plage à l'aide de like) et renvoie 700 000 lignes avant de les ignorer. ne correspondent pas sur la valeur. Lors de l'utilisation du 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 laquelle commencer à travailler.

À l'aide de SQL Server 2014 et du nouvel estimateur cardinalty, il n'y a aucune différence entre 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 15 secondes. Ce n'est clairement pas une amélioration ici lors de l'utilisation de nouvelles choses.

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

Pourquoi la nodes()requête (version d'origine) 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 de SQL Server fait quelque chose de mal et introduit un opérateur de spool. Je ne sais pas pourquoi, mais la bonne nouvelle est qu'il n'est plus là avec le nouvel estimateur cardinalty de 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. Si vous changez la taille, la forme ou la forme du XML, vous pouvez avoir une histoire complètement différente. Aucun moyen de savoir avec certitude sans tester avec les données que vous avez réellement dans les tableaux.

Comment fonctionnent les index XML

Les index XML dans SQL Server sont implémentés en tant que tables internes. L'index XML primaire crée la table avec la clé primaire de la table de base plus la colonne id du noeud, soit 12 colonnes au total. Il aura une ligne par ligne, element/node/attribute etc.ce qui peut bien sûr devenir très volumineux en fonction de la taille du XML stocké. Avec un index XML primaire 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, en fonction du type d'index secondaire que vous créez, il comporte des colonnes et des ordres de colonne différents.

À partir de CREATE XML INDEX (Transact-SQL) :

VALUE
Crée un index XML secondaire sur les colonnes où les colonnes de clé sont (valeur du nœud et chemin) de l'index XML primaire.

PATH
Crée un index XML secondaire sur les colonnes, basé sur les valeurs de chemin d'accès et les valeurs de nœud de 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.

PROPERTY
Crée un index XML secondaire sur les colonnes (PK, valeur de chemin et de noeud) de l'index XML primaire, 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 de chemin d'accès et la seconde colonne est la valeur de ce nœud. En réalité, 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, vous avez recherché //item/value/@string, //item/@nameet //item. Comme le chemin d'accès est stocké inversé dans la colonne, SQL Server peut utiliser une recherche d'intervalle avec like = '€€€€€€%€€€€€€le chemin d'accès est inversé. Lorsque vous utilisez un chemin d'accès complet, il n'y a aucune raison de l'utiliser, likecar le chemin d'accès complet 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. Il est préférable de concevoir votre base de données afin de ne pas avoir à utiliser de valeurs XML pour filtrer dans une clause where. Si vous savez au préalable 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, des index XML sélectifs sont également disponibles. Le fonctionnement de la scène est à peu près identique à celui des index XML classiques, vous spécifiez uniquement l'expression de chemin dans la définition de l'index et seuls les nœuds correspondants sont indexés. De cette façon, vous économiserez 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 n'est pas rapide. L'entrée de l'optimiseur est réécrite afin qu'il utilise les tables internes. Ensuite, il appartient à l'optimiseur de faire de son mieux, comme avec une requête classique. Lorsqu'aucun index n'est utilisé, quelques fonctions de table sont utilisées. L'essentiel est que vous ne pouvez pas dire ce qui sera plus rapide sans tester.

Comment pourrait-on éviter l'impact négatif?

Essai

Mikael Eriksson
la source
2
Vos idées sur la différence de .nodes()et .exist()sont convaincantes. De plus, le fait que l'index avec full path searchsoit plus rapide semble facile à comprendre. Cela signifierait: Si vous créez un index XML, vous devez toujours être conscient de l'influence négative de tout XPath générique ( //ou *ou ..ou [filter]ou de tout ce qui n'est pas simplement Xpath ...). En fait, vous ne devriez utiliser que le chemin complet - un tirage au sort plutôt réussi ...
Shnugo