为什么 where 子句过滤 `value()` 时不使用二级选择性索引?

Mik*_*son 13 performance xml sql-server execution-plan sql-server-2012 query-performance

设置:

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;
Run Code Online (Sandbox Code Playgroud)

每行的示例 XML:

<Number>314</Number>
Run Code Online (Sandbox Code Playgroud)

查询的任务是计算T指定值为 的行数<Number>

有两种明显的方法可以做到这一点:

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;
Run Code Online (Sandbox Code Playgroud)

事实证明,value()并且exists()需要选择性XML索引工作两种不同的路径定义。

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

sql版本是value()xquery版本是exist()

您可能认为这样的索引会给您一个很好的查找计划,但选择性 XML 索引是作为系统表实现的,其主键是T作为系统表聚集键的前导键。指定的路径是该表中的稀疏列。如果您想要定义路径的实际值的索引,您需要创建二级选择性索引,每个路径表达式一个。

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;
Run Code Online (Sandbox Code Playgroud)

查询计划exist()在二级 XML 索引中进行搜索,然后在系统表中对选择性 XML 索引进行键查找(不知道为什么需要这样做),最后它进行查找T以确保确实存在行在那里。最后一部分是必需的,因为系统表和T.

在此处输入图片说明

value()查询计划不太好。它T使用嵌套循环连接对内部表上的查找进行聚集索引扫描,以从稀疏列中获取值,并最终过滤该值。

在此处输入图片说明

是否应该使用选择性索引是在优化之前决定的,但是否应该使用二级选择性索引是优化器基于成本的决定。

为什么 where 子句过滤时不使用二级选择性索引value()

更新:

查询在语义上是不同的。如果添加具有值的行

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

exist()版本将计算 2 行,values()查询将计算 1 行。但是,使用此处指定的索引定义,使用singletonSQL Server 指令将阻止您添加具有多个<Number>元素的行。

然而,这不允许我们在values()没有指定的情况下使用该函数[1]来保证编译器我们只会得到一个值。这[1]就是我们在value()计划中设置 Top N Sort 的原因。

看起来我在这里接近答案......

Mik*_*son 11

singleton索引的路径表达式中的声明强制您不能添加多个<Number>元素,但 XQuery 编译器在解释value()函数中的表达式时不会考虑这一点。您必须指定[1]以使 SQL Server 满意。使用带有模式的类型化 XML 也无济于事。正因为如此,SQL Server 构建了一个使用可称为“应用”模式的东西的查询。

最容易演示的是使用常规表而不是 XML 模拟我们实际执行的查询T和内部表。

这是将内部表设置为真实表的设置。

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;
Run Code Online (Sandbox Code Playgroud)

有了这两个表,您就可以执行等效的exist()查询。

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
             );
Run Code Online (Sandbox Code Playgroud)

value()查询的等价物看起来像这样。

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;
Run Code Online (Sandbox Code Playgroud)

top(1)order by S.path_1_id是罪魁祸首,它是[1]在XPath表达式是罪魁祸首。

我认为 Microsoft 不可能用内部表的当前结构来解决这个问题,即使你被允许[1]values()函数中删除。他们可能必须为每个路径表达式创建多个内部表,并带有唯一的约束,以保证优化器<number>每行只能有一个元素。不确定这实际上是否足以让优化器“打破应用模式”。

对于那些认为这很有趣和有趣的人来说,因为你还在阅读这篇文章,所以你可能是。

一些查询来看看内部表的结构。

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')
                     );
Run Code Online (Sandbox Code Playgroud)