TL;DR
Почему нет:
SELECT
SomeXmlColumn.nodes('/people/person') AS foo(b)
FROM MyTable
работа?
До вопроса
Почти каждый ответ, который я видел (или получил) для использования запросов XPath в SQL Server, требует, чтобы вы присоединились к документу XML обратно к себе, используя CROSS APPLY
.
Почему?
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)
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
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);
И даже из электронных книг MSDN:
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 Books Online) не объясняет, зачем он нужен, какую проблему он решает, что он делает или как он работает.
Даже самый простой случай нуждается в них
Даже самый простой пример использования 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, на которую мы можем запросить:
Начнем с очевидного
Сначала мне нужны люди. В реальном 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 строки. Мне как-то приходится разбивать три строки на три строки.
К именам
Моя фактическая цель - получить firstName
и lastName
. В 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>
... и теперь я устал. Я потратил три часа на то, чтобы написать этот вопрос, поверх четырех часов, которые я просил вчера задать вопрос. Я вернусь к этому вопросу позже; когда здесь прохладно, и у меня больше энергии, чтобы просить о помощи.
Второй ветер
Основная проблема заключается в том, что независимо от того, что я делаю, я получаю только одну возвращаемую строку. Я хочу вернуть три строки (потому что есть три человека). У SQL Server есть функция, которая может преобразовывать строки XML (называемые узлами) в строки SQL Server (называемые строками). Это функция .nodes
:
Метод nodes() полезен, если вы хотите уничтожить экземпляр типа XML xml в реляционных данных. Он позволяет идентифицировать узлы, которые будут отображаться в новую строку.
Это означает, что вы вызываете метод .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, уровень 16, состояние 1, строка 8
Столбец "SomeOtherXmlColumn", который был возвращен методом nodes(), не может использоваться напрямую. Его можно использовать только с одним из четырех методов типа данных XML: exist(), 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>
Это имеет смысл. Я ожидал, что запрос XPath в SQL Server будет вести себя как XPath. Но ретроспективное внимательное чтение документов говорит иначе:
Результатом метода nodes() является набор строк, содержащий логические копии исходных экземпляров XML. В этих логических копиях контекст node каждого экземпляра строки устанавливается в один из узлов, идентифицированных с выражением запроса, так что последующие запросы могут перемещаться относительно этих контекстных узлов.
Теперь сделайте это с столбцом xml
Предыдущий пример был для переменной типа xml
. Теперь нам нужно модифицировать функцию .nodes
для работы со таблицей, содержащей столбец xml
:
SELECT
SomeXmlColumn.nodes('/people/person')
FROM MyTable
Нет, это не работает:
Msg 227, уровень 15, состояние 1, строка 8
"nodes" не является допустимой функцией, свойством или полем.
Хотя .nodes
является допустимым методом типа данных xml
, он просто не работает, когда вы пытаетесь использовать его в типе данных xml
. Он также не работает при использовании в типе xml
:
SELECT *
FROM MyTable.SomeXmlColumn.nodes('/people/person')
Msg 208, уровень 16, состояние 1, строка 8
Недопустимое имя объекта "MyTable.SomeXmlColumn.nodes".
Я полагаю, поэтому необходим модификатор CROSS APPLY
. Не потому, что вы что-то присоединяетесь, а потому, что парсер SQL Server откажется распознавать .nodes
, если ему не предшествуют ключевые слова CROSS APPLY
:
SELECT
'test' AS SomeTestColumn
FROM MyTable CROSS APPLY MyTable.SomeXmlColumn.nodes('/people/person') AS MyDerivedTable(SomeOtherXmlColumn)
И мы начинаем куда-то:
SomeTestColumn
--------------
test
test
test
Итак, если мы хотим увидеть XML, который возвращается:
SELECT
SomeOtherXmlColumn.query('/')
FROM (MyTable CROSS APPLY MyTable.SomeXmlColumn.nodes('/people/person') AS MyDerivedTable(SomeOtherXmlColumn))
Теперь у нас есть три строки.
Кажется, что CROSS APPLY
не используется для соединения, а просто ключевое слово, которое позволяет .nodes
работать
И похоже, что оптимизатор SQL Server просто отказывается принять любое использование
.nodes
и вы действительно должны использовать:
CROSS APPLY .nodes
И вот как это. И если это случай - это хорошо. Это правило. И это привело к многолетней путанице; думая, что я присоединяюсь к чему-то другому с оператором CROSS APPLY
.
Кроме того, я верю, что в этом есть нечто большее. На самом деле должно быть что-то вроде CROSS APPLY
. Но я не вижу, где - или почему.