Warum wird der sekundäre selektive Index nicht verwendet, wenn die where-Klausel nach `value ()` filtert?

13

Installieren:

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;

Beispiel-XML für jede Zeile:

<Number>314</Number>

Der Job für die Abfrage besteht darin, die Anzahl der Zeilen Tmit dem angegebenen Wert von zu zählen <Number>.

Es gibt zwei offensichtliche Möglichkeiten, dies zu tun:

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;

Es stellt sich heraus , dass value()und exists()erfordert zwei verschiedene Pfaddefinitionen für den selektiven XML - Index an der Arbeit.

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

Die sqlVersion ist für value()und die xqueryVersion ist für exist().

Sie könnten denken, dass ein solcher Index Ihnen einen Plan mit einer netten Suche geben würde, aber selektive XML-Indizes werden als Systemtabelle mit dem Primärschlüssel Tals Hauptschlüssel des gruppierten Schlüssels der Systemtabelle implementiert. Die angegebenen Pfade sind Spalten mit geringer Dichte in dieser Tabelle. Wenn Sie einen Index der tatsächlichen Werte der definierten Pfade benötigen, müssen Sie einen sekundären selektiven Index für jeden Pfadausdruck erstellen.

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);

Der Abfrageplan für exist()führt eine Suche im sekundären XML-Index durch, gefolgt von einer Schlüsselsuche in der Systemtabelle nach dem selektiven XML-Index (weiß nicht, warum dies erforderlich ist), und führt schließlich eine Suche durch T, um sicherzustellen, dass tatsächlich solche vorhanden sind Reihen drin. Der letzte Teil ist erforderlich, da zwischen der Systemtabelle und keine Fremdschlüsseleinschränkung besteht T.

Bildbeschreibung hier eingeben

Der Plan für die value()Abfrage ist nicht so schön. Es wird ein Clustered-Index-Scan Tmit einem Join mit verschachtelten Schleifen gegen eine Suche in der internen Tabelle durchgeführt, um den Wert aus der Spalte mit geringer Dichte abzurufen und schließlich nach dem Wert zu filtern.

Bildbeschreibung hier eingeben

Ob ein selektiver Index verwendet werden soll oder nicht, wird vor der Optimierung entschieden, aber ob ein sekundärer selektiver Index verwendet werden soll oder nicht, ist eine kostenbasierte Entscheidung des Optimierers.

Warum wird der sekundäre selektive Index nicht verwendet, wenn die where-Klausel aktiviert ist value()?

Aktualisieren:

Die Abfragen sind semantisch unterschiedlich. Wenn Sie eine Zeile mit dem Wert hinzufügen

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

Die exist()Version würde 2 Zeilen zählen und die values()Abfrage würde 1 Zeile zählen. Wenn Sie jedoch die hier angegebenen Indexdefinitionen mit der singletonDirektive SQL Server verwenden, können Sie keine Zeile mit mehreren <Number>Elementen hinzufügen .

Damit können wir die values()Funktion jedoch nicht verwenden , ohne [1]dem Compiler zu garantieren, dass wir nur einen einzigen Wert erhalten. Aus diesem [1]Grund haben wir eine Top-N-Sortierung im value()Plan.

Sieht so aus, als würde ich hier eine Antwort finden ...

Mikael Eriksson
quelle

Antworten:

11

Die Deklaration von singletonim Pfadausdruck des Index erzwingt, dass Sie nicht mehrere <Number>Elemente hinzufügen können, aber der XQuery-Compiler berücksichtigt dies nicht bei der Interpretation des Ausdrucks in der value()Funktion. Sie müssen angeben [1], um SQL Server glücklich zu machen. Die Verwendung von typisiertem XML mit einem Schema hilft auch dabei nicht. Aus diesem Grund erstellt SQL Server eine Abfrage, die ein sogenanntes Apply-Muster verwendet.

Am einfachsten zu demonstrieren ist es, reguläre Tabellen anstelle von XML zu verwenden, um die Abfrage, für die wir tatsächlich ausführen, Tund die interne Tabelle zu simulieren .

Hier ist das Setup für die interne Tabelle als echte Tabelle.

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;

Wenn beide Tabellen vorhanden sind, können Sie das Äquivalent der exist()Abfrage ausführen .

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
             );

Das Äquivalent der value()Abfrage würde so aussehen.

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;

Das top(1)und order by S.path_1_idist der Täter und es ist [1]der Xpath-Ausdruck, der die Schuld trägt.

Ich glaube nicht, dass es Microsoft möglich ist, dies mit der aktuellen Struktur der internen Tabelle zu beheben, selbst wenn Sie [1]die values()Funktion weglassen durften . Sie müssten wahrscheinlich mehrere interne Tabellen für jeden Pfadausdruck mit eindeutigen Einschränkungen erstellen, um dem Optimierer zu garantieren, dass es nur ein <number>Element für jede Zeile geben kann. Ich bin mir nicht sicher, ob das für das Optimierungsprogramm ausreichen würde, um aus dem Apply-Muster auszubrechen.

Für Sie, die das lustig und interessant finden und da Sie dies noch lesen, sind Sie es wahrscheinlich.

Einige Abfragen zur Struktur der internen Tabelle.

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
quelle