为什么在使用XPath查询时需要CROSS APPLY?

时间:2014-05-06 15:01:58

标签: sql-server xpath

TL;博士

为什么没有:

SELECT 
    SomeXmlColumn.nodes('/people/person') AS foo(b)
FROM MyTable

工作?

前问题

我几乎曾经在SQL Server中看到(或得到过)使用XPath查询要求您使用CROSS APPLY将XML文档加入自身。

为什么?

For example:

SELECT 
   p.value('(./firstName)[1]', 'VARCHAR(8000)') AS firstName,
   p.value('(./lastName)[1]', 'VARCHAR(8000)') AS lastName
FROM table 
   CROSS APPLY field.nodes('/person') t(p)

For example

SELECT a.BatchXml.value('(Name)[1]', 'varchar(50)') AS Name,
    a.BatchXml.value('(IDInfo/IDType)[1]', 'varchar(50)') AS IDType,
    a.BatchXml.value('(IDInfo/IDOtherDescription)[1]', 'varchar(50)') AS IDOtherDescription
FROM BatchReports b
CROSS APPLY b.BatchFileXml.nodes('Customer') A(BatchXml)
WHERE a.BatchXml.exist('IDInfo/IDType[text()=3]')=1

For example

SELECT  b.BatchID,
        x.XmlCol.value('(ReportHeader/OrganizationReportReferenceIdentifier)[1]','VARCHAR(100)') AS OrganizationReportReferenceIdentifier,
        x.XmlCol.value('(ReportHeader/OrganizationNumber)[1]','VARCHAR(100)') AS OrganizationNumber
FROM    Batches b
CROSS APPLY b.RawXml.nodes('/CasinoDisbursementReportXmlFile/CasinoDisbursementReport') x(XmlCol);

And even from MSDN Books Online:

SELECT nref.value('first-name[1]', 'nvarchar(32)') FirstName,
       nref.value('last-name[1]', 'nvarchar(32)') LastName
FROM    [XmlFile] CROSS APPLY [Contents].nodes('//author') AS p(nref)

他们都使用它。但没有人(甚至不是SQL Server联机丛书)解释了为什么需要它,它解决了什么问题,它做了什么,或者它是如何工作的。

即使最简单的情况也需要它们

即使是采用XML的最简单的例子:

<people>
   <person><firstName>Jon</firstName><lastName>Johnson</lastName></person>
   <person><firstName>Kathy</firstName><lastName>Carter</lastName></person>
   <person><firstName>Bob</firstName><lastName>Burns</lastName></person>
</people>

并返回值:

FirstName  LastName
=========  ========
Jon        Johnson
Kathy      Carter
Bob        Burns

需要加入:

SELECT 
   p.value('(./firstName)[1]', 'VARCHAR(8000)') AS firstName,
   p.value('(./lastName)[1]', 'VARCHAR(8000)') AS lastName
FROM table 
   CROSS APPLY field.nodes('/person') t(p)

令人困惑的是它甚至没有使用它加入的表格,为什么需要它呢?

由于从未记录或解释过查询XML,希望我们现在可以解决这个问题。

它实际上做了什么?

所以让我们从一个实际的例子开始,因为我们想要一个实际的答案,这给出了一个实际的解释:

DECLARE @xml xml;
SET @xml = 
'<people>
   <person><firstName>Jon</firstName><lastName>Johnson</lastName></person>
   <person><firstName>Kathy</firstName><lastName>Carter</lastName></person>
   <person><firstName>Bob</firstName><lastName>Burns</lastName></person>
</people>';
;WITH MyTable AS (
    SELECT @xml AS SomeXmlColumn
)

现在我们可以查询 psuedo 表:

enter image description here

让我们从显而易见的

开始

首先,我需要人民。在真实 XML中,我可以轻松返回三行:

/people/person

这给出了包含三个节点的NodeList

<person><firstName>Jon</firstName><lastName>Johnson</lastName></person>
<person><firstName>Kathy</firstName><lastName>Carter</lastName></person>
<person><firstName>Bob</firstName><lastName>Burns</lastName></person>

在SQL Server中,查询相同:

SELECT 
   SomeXmlColumn.query('/people/person')
FROM MyTable

不会返回三行,而是返回包含三个节点的XML的一行:

<person>
  <firstName>Jon</firstName>
  <lastName>Johnson</lastName>
</person>
<person>
  <firstName>Kathy</firstName>
  <lastName>Carter</lastName>
</person>
<person>
  <firstName>Bob</firstName>
  <lastName>Burns</lastName>
</person>

显然这是不合适的,当我的最终目标是返回3 时。我不得不将这三行分成三行。

在名称

我的目标是获得firstNamelastName。在XPath中我可以做类似的事情:

/people/person/firstName|/people/person/lastName

它给了我六个节点,虽然它们不是相邻的

<firstName>Jon</firstName>
<lastName>Johnson</lastName>
<firstName>Kathy</firstName>
<lastName>Carter</lastName>
<firstName>Bob</firstName>
<lastName>Burns</lastName>

在SQL Server中,我们尝试类似的东西

SELECT 
    SomeXmlColumn.query('/people/person/firstName') AS FirstName,
    SomeXmlColumn.query('/people/person/lastName') AS LastName
FROM MyTable

它为我们提供了一个,每列包含一个XML片段:

FirstName                     LastName
============================  ============================
<firstName>Jon</firstName>    <lastName>Johnson</lastName>
<firstName>Kathy</firstName>  <lastName>Carter</lastName>
<firstName>Bob</firstName>    <lastName>Burns</lastName>

......现在我累了。我在four hours I spent asking yesterday's question之上花了三个小时写这个问题。我稍后会回到这个问题;当它在这里变凉时,我有更多精力去求助。

第二次风

根本问题在于,无论我做什么,我都只会返回一行。我想要返回三行(因为有三个人)。 SQL Server 具有可以将XML行(称为节点)转换为SQL Server行(称为行)的函数。它是.nodes函数:

  

当您想要将xml数据类型实例分解为关系数据时, nodes()方法很有用。它允许您识别将映射到新行的节点。

这意味着你打电话&#34;在.nodes数据类型上使用XPath查询的xml方法。过去在SQL Server中作为一行有三个节点的东西,回来(正确)为三个节点:

.nodes('/people/person') AS MyDerivedTable(SomeOtherXmlColumn)

从概念上讲,这会返回:

SomeOtherXmlColumn
------------------------------------------------------------------------
<person><firstName>Jon</firstName><lastName>Johnson</lastName></person>
<person><firstName>Kathy</firstName><lastName>Carter</lastName></person>
<person><firstName>Bob</firstName><lastName>Burns</lastName></person>

但如果您实际尝试使用它,它就无法运作:

DECLARE @xml xml;
SET @xml = 
'<people>
   <person><firstName>Jon</firstName><lastName>Johnson</lastName></person>
   <person><firstName>Kathy</firstName><lastName>Carter</lastName></person>
   <person><firstName>Bob</firstName><lastName>Burns</lastName></person>
</people>';
SELECT *
FROM @xml.nodes('/people/person') AS MyDervicedTable(SomeOtherXmlColumn)

给出错误:

  

Msg 493,Level 16,State 1,Line 8
  列&#39; SomeOtherXmlColumn&#39;从nodes()方法返回的是不能直接使用的。它只能与四种XML数据类型方法之一,exists(),nodes(),query()和value()一起使用,或者在IS NULL和IS NOT NULL检查中使用。

我认为这是因为我不允许查看结果集(即不允许*)。没问题。我将使用我原先使用的.query

SELECT SomeOtherXmlColumn.query('/') AS SomeOtherOtherXmlColumn
FROM @xml.nodes('/people/person') AS MyDervicedTable(SomeOtherXmlColumn)

返回行。但是,不是将节点列表拆分成行,而只是复制整个XML:

SomeOtherOtherXmlColumn
----------------------------------------
<people><person><firstName>Jon</firstName><lastName>Johnson</lastName></person><person><firstName>Kathy</firstName><lastName>Carter</lastName></person><person><firstName>Bob</firstName><lastName>Burns</lastName></person></people>
<people><person><firstName>Jon</firstName><lastName>Johnson</lastName></person><person><firstName>Kathy</firstName><lastName>Carter</lastName></person><person><firstName>Bob</firstName><lastName>Burns</lastName></person></people>
<people><person><firstName>Jon</firstName><lastName>Johnson</lastName></person><person><firstName>Kathy</firstName><lastName>Carter</lastName></person><person><firstName>Bob</firstName><lastName>Burns</lastName></person></people>

哪个有道理。我期待 SQL Server中的XPath查询表现得像XPath。但事后仔细阅读文档说不然:

  

nodes()方法的结果是一个包含原始XML实例的逻辑副本的行集。在这些逻辑副本中,每个行实例的上下文节点都设置为使用查询表达式标识的节点之一,以便后续查询可以相对于这些上下文节点进行导航。

现在使用xml

执行此操作

前面的示例适用于xml类型的变量。现在我们必须改进.nodes函数以使用包含xml列的表格:

SELECT 
   SomeXmlColumn.nodes('/people/person')
FROM MyTable

不,这不起作用:

  

Msg 227,Level 15,State 1,Line 8
  &#34;节&#34;不是有效的函数,属性或字段。

虽然.nodes 是<{1}}数据类型的有效方法,但当您尝试在xml上使用它时,它根本不起作用} 数据类型。在xml数据类型上使用时,它也不起作用:

xml
  

Msg 208,Level 16,State 1,Line 8
  无效的对象名称&#39; MyTable.SomeXmlColumn.nodes&#39;。

我认为是需要SELECT * FROM MyTable.SomeXmlColumn.nodes('/people/person') 修饰符的原因。不是因为您要加入任何内容,而是因为SQL Server解析器将拒绝识别CROSS APPLY,除非它前面带有关键字.nodes

cross apply

我们开始到达某个地方:

SELECT 
    'test' AS SomeTestColumn
FROM MyTable CROSS APPLY MyTable.SomeXmlColumn.nodes('/people/person') AS MyDerivedTable(SomeOtherXmlColumn)

因此,如果我们想要查看返回的XML:

SomeTestColumn
--------------
test
test
test

现在我们有三行。

SELECT SomeOtherXmlColumn.query('/') FROM (MyTable CROSS APPLY MyTable.SomeXmlColumn.nodes('/people/person') AS MyDerivedTable(SomeOtherXmlColumn)) 似乎不是用于加入,而只是一个允许cross apply工作的关键字

似乎SQL Server优化器只是拒绝接受任何

的使用
.nodes

你必须实际使用:

.nodes

这就是它的原因。如果是这样的话 - 那没关系。这是规则。这导致了多年的混乱;以为我正在使用CROSS APPLY .nodes 运算符加入其他内容。

除了我相信它还有更多。不知何故,实际上必须发生cross apply事件。但我无法看到 - 或为什么。

3 个答案:

答案 0 :(得分:14)

查询:

SELECT x.i.value('(./text())[1]', 'VARCHAR(10)')
FROM MyTable.SomeXmlColumn.nodes('./people/person/firstName') AS x(i);

不起作用,原因与此查询不起作用的原因相同:

SELECT *
FROM Person.Person.FirstName;

但这样做:

SELECT FirstName
FROM Person.Person;

-

FROM 子句需要rowset,所以这是有效的,因为 nodes()返回rowset:

DECLARE @xml AS XML = 
'<people>
   <person><firstName>Jon</firstName><lastName>Johnson</lastName></person>
   <person><firstName>Kathy</firstName><lastName>Carter</lastName></person>
   <person><firstName>Bob</firstName><lastName>Burns</lastName></person>
</people>';

SELECT x.i.value('(./text())[1]', 'VARCHAR(10)')
FROM @xml.nodes('./people/person/firstName') AS x(i);

如果xml不是变量而是表中的值,我们首先需要从这个值中提取行,这就是 CROSS APPLY 派上用场的时候:

SELECT x.i.value('(./text())[1]', 'VARCHAR(10)')
FROM MyTable as t
CROSS APPLY 
   t.SomeXmlColumn.nodes('./people/person/firstName') AS x(i);

CROSS APPLY 运算符将右表达式应用于左表(MyTable)中的每条记录。

  • 在MyTable表中有一条包含xml的记录。
  • CROSS APPLY获取此记录并将其公开给右侧的表达。
  • 右表达式使用nodes()函数提取记录。
  • 因此,有1 x 3 = 3条记录(xml节点),然后由SELECT子句处理。

与'普通' CROSS APPLY 查询比较:

SELECT c.CustomerID, soh.TotalDue, soh.OrderDate
FROM Sales.Customer AS c
CROSS APPLY
    (SELECT TOP(2) TotalDue, OrderDate
    FROM Sales.SalesOrderHeader
    WHERE CustomerID = c.CustomerID
ORDER BY TotalDue DESC) AS soh;

c.CustomerID是我们的t.SomeXmlColumn

答案 1 :(得分:5)

你问题的答案就在你的问题中。

  

nodes()方法的结果是行集

你不能这样做

WITH T(X) AS
(
SELECT 1
)
SELECT X, (SELECT 'A' AS Y UNION ALL SELECT 'B' AS Y)
FROM T

但你可以做到

WITH T(X) AS
(
SELECT 1
)
SELECT X, Y
FROM T
CROSS APPLY (SELECT 'A' AS Y UNION ALL SELECT 'B' AS Y) C

无论您在SELECT ... FROM T列表中调用哪些函数,直接SELECT都无法向结果集添加或减去行。这不是SQL的工作原理。

答案 2 :(得分:0)

我遇到了与您完全相同的问题。我无法获得行而不是XML表达式。我使用query('.').value(...)解决了这个问题。 在您的代码上,我想应该是这样的:

SELECT 
    MyDerivedTable.SomeOtherXmlColumn.query('.').value('/people/person')
FROM MyTable CROSS APPLY MyTable.SomeXmlColumn.nodes('/people/person')
AS MyDerivedTable(SomeOtherXmlColumn)