为什么 SQL 使用相同的 XML 查询结构进行索引扫描和查找?

Mid*_*one 9 performance xml sql-server t-sql index-tuning

我已经设置了一个测试来对 SQL 服务器中的 xml 性能进行基准测试。

测试设置

  • 一百万行数据
  • XML 定义的具有主键的列
  • 一个主要的 XML 索引
  • 路径上的辅助 XML 索引
  • XML 数据在格式上相似,但在每个文档中都有可变的标签名称

表和索引设计

CREATE TABLE [dbo].[xml_Test]
(
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [GUID] [varchar](50) NULL,
    [JSON_Data] [varchar](max) NULL,
    [XML_Data] [xml] NULL,
    CONSTRAINT [PK_xml_Test] PRIMARY KEY CLUSTERED ([ID] ASC)
);

ALTER TABLE [dbo].[xml_Test] ADD  CONSTRAINT [DF_xml_Test_GUID]  DEFAULT (newid()) FOR [GUID];
ALTER TABLE [dbo].[xml_Test] ADD  CONSTRAINT [PK_xml_Test] PRIMARY KEY CLUSTERED ([ID] ASC);

CREATE PRIMARY XML INDEX [PK_xml] ON [dbo].[xml_Test]
(   [XML_Data]);

CREATE XML INDEX [IX_xml_Path] ON [dbo].[xml_Test] 
(   [XML_Data]) 
USING XML INDEX [PK_xml] FOR PATH;
Run Code Online (Sandbox Code Playgroud)

示例 XML 架构

<data>
  <id>3812</id>
  <guid>E3735046-1183-4A79-B8EE-806312B533D6",</guid>
  <firstName>John</firstName>
  <lastName>Doe</lastName>
  <tel>123-123-1234</tel>
  <city>Toronto</city>
  <prov>Ontario</prov>
  <Q.49.R.47>14325</Q.49.R.47>
  <Q.1>14326</Q.1>
  <Q.9>143257</Q.9>
  <Q.25>14328</Q.25>
  <Q.50>14329</Q.50>
  <Q.51>14330</Q.51>
  <Q.30>14331</Q.30>
  <Q.22>14332</Q.22>
  <Q.100>14333</Q.100>
  <Q.70.R.4>1</Q.70.R.4>
  <Q.43>14335</Q.43>
  <Q.3>14336</Q.3>
  <Q.84.R.21.L.19>1</Q.84.R.21.L.19>
  <done>1</done>
</data>
Run Code Online (Sandbox Code Playgroud)

当我使用两个不同的值去查询表时,我得到了一个非常有效的搜索结果和一个扫描需要更长的数量级才能完成的另一个结果。

我不确定为什么第二个查询无法对索引进行查找。我认为这可能与开始时不在索引中的元素有关,但即使如此,我也不知道为什么需要扫描,除非索引确实没有索引所有路径。

通过粘贴计划执行计划

查询 A

select ID, 'Query A'
from xml_test
where XML_Data.exist('(/data/Q.70.R.4)[1]')=1
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

查询 B

select id, 'Query B'
from xml_test
where XML_Data.exist('(/data/Q.61.R.15)[1]')=1 
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

如您所见,查询 A 的执行时间为 5 毫秒,而查询 B 的执行时间几乎为 15 秒。

相同的查询格式但不同的输入会产生如此明显低效的执行计划的原因是什么?

在旁边

  • 我还使用它来测试具有相同格式的 JSON 数据的 JSON,我的结果显示,与 XML 查询 A 相同的 JSON 查询需要 22,344 毫秒(22 秒!)的 CPU 时间(3,419 毫秒时钟时间)来运行。
  • 如果 XML 索引可以通过搜索操作可靠地工作,那么与我在网上研究和阅读的内容相反,它显然是性能更高的读取选项。
  • 当查询大量数据时,除了用于 blob 存储或您有一个小数据集的地方,我完全不推荐在 SQL 服务器内部使用 JSON,也就是说,不要在生产中使用它来获取应该是的可搜索数据索引。还有其他专门为此设计的良好服务。

Mik*_*son 9

SQL Server 中的 XML 索引是作为内部表实现的,它是节点表的持久版本,与 XML 分解函数生成的非常相似。

内部表中的一列称为 hid,该列包含在查找路径表达式时使用的值。该值是一个ordpath值。当您创建路径 xml 索引时,您将获得一个以 hid 作为前导列的非聚集索引。

当 SQL Server 向表中插入 XML 数据时,会生成特定元素名称的 ordpath。为了对路径进行编码,将连接不同的 ordpath 值。

简化后的路径 X/Y/Z 可以表示为 1.2.3,Y/Z/Z 可以表示为 2.3.3。

SQL Server 必须有办法为相同的元素名称生成相同的 ordpath 值。这可以通过某种字典或表来完成,这些字典或表跟踪一个 XML 索引的所有生成的 ordpath 值。我在这里有点模糊,因为我没有在任何地方找到任何说明这是如何完成的,我也没有找到它的存储位置。

使用此 XML 插入一行

<X>
  <Y>
    <Z />
  </Y>
</X>
Run Code Online (Sandbox Code Playgroud)

将在内部表中为您提供此内容,并添加一列额外的列以显示以 hid 编码的路径表达式

id      hid     pk1
0x              1    
0x58          1     Z
0x5AC0  Á€À€    1     Y/Z
0x5AD6  €Á€À€  1     X/Y/Z
Run Code Online (Sandbox Code Playgroud)

您可以使用 DAC 登录查看内部表。

当您查询路径表达式时会发生什么(在这里我再次有点推测)是用于确保具有相同名称的元素获得相同 ordpath 值的相同字典/查找表用于查找 hid用于内部表中的索引查找。XQuery 中的路径表达式始终是静态的,因此用于查询的 hid 值可以存储在查询计划中并重新使用。

对存在的节点的查询得到一个查询,在 hid 上查找,其中Î要搜索的 hid 值。

<SeekKeys>
  <Prefix ScanType="EQ">
    <RangeColumns>
      <ColumnReference Database="[yy]" Schema="[sys]" Table="[xml_index_nodes_658101385_256000]" 
                       Alias="[NodeThatExist:1]" Column="hid" />
    </RangeColumns>
    <RangeExpressions>
      <ScalarOperator ScalarString="'Î'">
        <Const ConstValue="'Î'" />
      </ScalarOperator>
    </RangeExpressions>
  </Prefix>
</SeekKeys>
Run Code Online (Sandbox Code Playgroud)

对不存在的节点的查询为您提供了一种非常不同的行定位方式。

select count(*)
from dbo.T
where T.X.exist('NodeDoesNotExist') = 1
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

计算标量使用一些函数来计算过滤运算符中检查的值。

在此处输入图片说明

我在这里所做的一些测试可以在这里找到。请注意,您实际上并不需要为索引中的元素生成 hid 来获取搜索计划,您只需尝试插入它即可。

作为旁注,查询的重写以使用一种方式或另一种方式来查找 XML 查询的行是在查询优化器开始执行其工作之前完成的,因此,如果您获得了搜索,也取决于隐藏内部表中的列。


Paul White 在聊天中提到了一些关于插入的元素名称的存储位置:

QNames 存储在 sys.sysqnames 中。XML algebrizer 需要现有的已知 QName 来生成优化的路径转换。即使用路径索引。将条目添加到系统事务中的 sys.sysqnames 中,不会感觉到用户事务


Pau*_*ite 5

如果可以,请考虑以更常用的格式存储您的 xml。这可能需要在流程的早期阶段进行更改,或者在导入数据时进行一些预处理,但这很值得。

关键观察是元素名称中的编码信息非常不寻常。使用包含可变数据的可预测结构(理想情况下符合模式)的 xml 通常是一个更好的主意。

为了说明这一点,我已将示例数据重组为以下更一般的项目价值组织:

CREATE TABLE [dbo].[xml_Test]
(
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [GUID] [varchar](50) NULL DEFAULT NEWID(),
    [JSON_Data] [varchar](max) NULL,
    [XML_Data] [xml] NULL,
    CONSTRAINT [PK_xml_Test] PRIMARY KEY CLUSTERED ([ID] ASC)
);

INSERT TOP (10000) 
    dbo.xml_Test (XML_Data)
SELECT
    CONVERT(xml,
        N'
        <data>
            <id>3812</id>
            <guid>E3735046-1183-4A79-B8EE-806312B533D6</guid>
            <firstName>John</firstName>
            <lastName>Doe</lastName>
            <tel>123-123-1234</tel>
            <city>Toronto</city>
            <prov>Ontario</prov>
            <qitems>
                <item><name>Q.49.R.47</name><value>14325</value></item>
                <item><name>Q.1</name><value>14326</value></item>
                <item><name>Q.9</name><value>14357</value></item>
                <item><name>Q.25</name><value>14328</value></item>
                <item><name>Q.50</name><value>14329</value></item>
                <item><name>Q.51</name><value>14330</value></item>
                <item><name>Q.30</name><value>14331</value></item>
                <item><name>Q.22</name><value>14332</value></item>
                <item><name>Q.100</name><value>14333</value></item>
                <item><name>Q.70.R.4.1</name><value>1</value></item>
                <item><name>Q.43</name><value>14335</value></item>
                <item><name>Q.3</name><value>14336</value></item>
                <item><name>Q.84.R.21.L.19</name><value>1</value></item>
            </qitems>
            <done>1</done>
        </data>
        ')
FROM sys.all_columns AS AC1
CROSS JOIN sys.all_columns AS AC2;
Run Code Online (Sandbox Code Playgroud)

我碰巧使用了以元素为中心的设计,但如果您愿意,也可以在item元素上同样使用namevalue属性。

添加一个具有不同 Q 名称(Q.999)的 xml 实例:

INSERT dbo.xml_Test (XML_Data)
SELECT
    CONVERT(xml,
        N'
        <data>
            <id>3812</id>
            <guid>E3735046-1183-4A79-B8EE-806312B533D6</guid>
            <firstName>John</firstName>
            <lastName>Doe</lastName>
            <tel>123-123-1234</tel>
            <city>Toronto</city>
            <prov>Ontario</prov>
            <qitems>
                <item><name>Q.999</name><value>-999</value></item>
            </qitems>
            <done>1</done>
        </data>
        ');
Run Code Online (Sandbox Code Playgroud)

重新组织的结构适用于选择性 xml 索引

CREATE SELECTIVE XML INDEX sxi
ON dbo.xml_Test (XML_Data)
FOR  
(  
    qitem_path = '/data/qitems/item' AS XQUERY 'node()',
    qitem_name =  '/data/qitems/item/name' AS XQUERY 'xs:string' MAXLENGTH(20) SINGLETON
    --qitem_value = '/data/qitems/item/value' AS SQL integer SINGLETON,
);

-- Optional secondary index for name
CREATE XML INDEX sxi_qitem_name
ON dbo.xml_Test (XML_Data)
USING XML INDEX sxi
FOR  (qitem_name);

Run Code Online (Sandbox Code Playgroud)

如果您希望索引对提取有用,请取消注释值路径。

定位 Q.999 名称现在非常有效:

SELECT
    --XT.XML_Data.value('(./data/qitems/item/value)[1]', 'integer'),
    XT.ID
FROM dbo.xml_Test AS XT
WHERE 
    XT.XML_Data.exist('./data/qitems/item[name="Q.999"]') = 1;
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

搜索不存在的名称 (Q.61.R.15) 同样有效:

SELECT
    --XT.XML_Data.value('(./data/qitems/item/value)[1]', 'integer'),
    XT.ID
FROM dbo.xml_Test AS XT
WHERE 
    XT.XML_Data.exist('./data/qitems/item[name="Q.61.R.15"]') = 1;
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

NOT NULL如果合适,您可以通过创建 xml column 来避免其中的一个连接。