Pourquoi l'index sélectif secondaire n'est-il pas utilisé lorsque la clause where filtre sur `value ()`?

13

Installer:

create table dbo.T
(
  ID int identity primary key,
  XMLDoc xml not null
);

insert into dbo.T(XMLDoc)
select (
       select N.Number
       for xml path(''), type
       )
from (
     select top(10000) row_number() over(order by (select null)) as Number
     from sys.columns as c1, sys.columns as c2
     ) as N;

Exemple de XML pour chaque ligne:

<Number>314</Number>

Le travail de la requête consiste à compter le nombre de lignes dans Tavec une valeur spécifiée de <Number>.

Il existe deux façons évidentes de procéder:

select count(*)
from dbo.T as T
where T.XMLDoc.value('/Number[1]', 'int') = 314;

select count(*)
from dbo.T as T
where T.XMLDoc.exist('/Number[. eq 314]') = 1;

Il se avère que value()et exists()nécessite deux définitions de chemin différent pour l'index XML sélectif au travail.

create selective xml index SIX_T on dbo.T(XMLDoc) for
(
  pathSQL = '/Number' as sql int singleton,
  pathXQUERY = '/Number' as xquery 'xs:double' singleton
);

La sqlversion est pour value()et la xqueryversion est pour exist().

Vous pourriez penser qu'un index comme celui-ci vous donnerait un plan avec une bonne recherche mais les index XML sélectifs sont implémentés comme une table système avec la clé primaire Tcomme clé principale de la clé en cluster de la table système. Les chemins spécifiés sont des colonnes éparses dans cette table. Si vous souhaitez un index des valeurs réelles des chemins définis, vous devez créer un index sélectif secondaire, un pour chaque expression de chemin.

create xml index SIX_T_pathSQL on dbo.T(XMLDoc)
  using xml index SIX_T for (pathSQL);

create xml index SIX_T_pathXQUERY on dbo.T(XMLDoc)
  using xml index SIX_T for (pathXQUERY);

Le plan de requête pour le exist()fait une recherche dans l'index XML secondaire suivi d'une recherche de clé dans la table système pour l'index XML sélectif (je ne sais pas pourquoi cela est nécessaire) et enfin il fait une recherche Tpour s'assurer qu'il y en a réellement lignes là-dedans. La dernière partie est nécessaire car il n'y a pas de contrainte de clé étrangère entre la table système et T.

entrez la description de l'image ici

Le plan de la value()requête n'est pas si agréable. Il effectue un balayage d'index en cluster Tavec une jointure de boucles imbriquées contre une recherche sur la table interne pour obtenir la valeur de la colonne clairsemée et enfin filtre sur la valeur.

entrez la description de l'image ici

Si un indice sélectif doit être utilisé ou non est décidé avant l'optimisation, mais si un indice sélectif secondaire doit être utilisé ou non est une décision basée sur les coûts par l'optimiseur.

Pourquoi l'index sélectif secondaire n'est-il pas utilisé lorsque la clause where est filtrée value()?

Mise à jour:

Les requêtes sont sémantiquement différentes. Si vous ajoutez une ligne avec la valeur

<Number>313</Number>
<Number>314</Number>` 

la exist()version compterait 2 lignes et la values()requête compterait 1 ligne. Mais avec les définitions d'index telles qu'elles sont spécifiées ici, l'utilisation de la singletondirective SQL Server vous empêchera d'ajouter une ligne avec plusieurs <Number>éléments.

Cela ne nous permet cependant pas d'utiliser la values()fonction sans spécifier [1]pour garantir au compilateur que nous n'obtiendrons qu'une seule valeur. C'est [1]la raison pour laquelle nous avons un Top N Sort dans le value()plan.

On dirait que je me rapproche d'une réponse ici ...

Mikael Eriksson
la source

Réponses:

11

La déclaration de singletondans le chemin expression de l'index impose que vous ne pouvez pas ajouter plusieurs <Number>éléments mais le compilateur XQuery ne prend pas cela en compte lors de l'interprétation de l'expression dans la value()fonction. Vous devez spécifier [1]pour rendre SQL Server heureux. L'utilisation de XML typé avec un schéma n'aide pas non plus. Et à cause de cela, SQL Server crée une requête qui utilise quelque chose qui pourrait être appelé un modèle «appliquer».

Le plus simple à démontrer est d'utiliser des tables régulières au lieu de XML simulant la requête contre laquelle nous sommes réellement en train d'exécuter Tet la table interne.

Voici la configuration de la table interne comme une vraie table.

create table dbo.xml_sxi_table
(
  pk1 int not null,
  row_id int,
  path_1_id varbinary(900),
  pathSQL_1_sql_value int,
  pathXQUERY_2_value float
);

go

create clustered index SIX_T on xml_sxi_table(pk1, row_id);
create nonclustered index SIX_pathSQL on xml_sxi_table(pathSQL_1_sql_value) where path_1_id is not null;
create nonclustered index SIX_T_pathXQUERY on xml_sxi_table(pathXQUERY_2_value) where path_1_id is not null;

go

insert into dbo.xml_sxi_table(pk1, row_id, path_1_id, pathSQL_1_sql_value, pathXQUERY_2_value)
select T.ID, 1, T.ID, T.ID, T.ID
from dbo.T;

Avec les deux tables en place, vous pouvez exécuter l'équivalent de la exist()requête.

select count(*)
from dbo.T
where exists (
             select *
             from dbo.xml_sxi_table as S
             where S.pk1 = T.ID and
                   S.pathXQUERY_2_value = 314 and
                   S.path_1_id is not null
             );

L'équivalent de la value()requête ressemblerait à ceci.

select count(*)
from dbo.T
where (
      select top(1) S.pathSQL_1_sql_value
      from dbo.xml_sxi_table as S
      where S.pk1 = T.ID and
            S.path_1_id is not null
      order by S.path_1_id
      ) = 314;

Le top(1)et order by S.path_1_idest le coupable et c'est [1]dans l'expression de Xpath qu'il faut blâmer.

Je ne pense pas qu'il soit possible pour Microsoft de résoudre ce problème avec la structure actuelle de la table interne même si vous étiez autorisé à laisser de côté [1]la values()fonction. Ils devraient probablement créer plusieurs tables internes pour chaque expression de chemin avec des contraintes uniques en place pour garantir à l'optimiseur qu'il ne peut y avoir qu'un seul <number>élément pour chaque ligne. Je ne suis pas sûr que cela suffirait réellement à l'optimiseur de "sortir du modèle d'application".

Pour vous qui pensez que c'est amusant et intéressant et puisque vous lisez encore ceci, vous l'êtes probablement.

Quelques requêtes pour regarder la structure de la table interne.

select T.name, 
       T.internal_type_desc, 
       object_name(T.parent_id) as parent_table_name
from sys.internal_tables as T
where T.parent_id = object_id('T');

select C.name as column_name, 
       C.column_id,
       T.name as type_name,
       C.max_length,
       C.is_sparse,
       C.is_nullable
from sys.columns as C
  inner join sys.types as T
    on C.user_type_id = T.user_type_id
where C.object_id in (
                     select T.object_id 
                     from sys.internal_tables as T 
                     where T.parent_id = object_id('T')
                     )
order by C.column_id;

select I.name as index_name,
       I.type_desc,
       I.is_unique,
       I.filter_definition,
       IC.key_ordinal,
       C.name as column_name, 
       C.column_id,
       T.name as type_name,
       C.max_length,
       I.is_unique,
       I.is_unique_constraint
from sys.indexes as I
  inner join sys.index_columns as IC
    on I.object_id = IC.object_id and
       I.index_id = IC.index_id
  inner join sys.columns as C
    on IC.column_id = C.column_id and
       IC.object_id = C.object_id
  inner join sys.types as T
    on C.user_type_id = T.user_type_id
where I.object_id in (
                     select T.object_id 
                     from sys.internal_tables as T 
                     where T.parent_id = object_id('T')
                     );
Mikael Eriksson
la source