SQL Сервер XML Обработка: объединение разных узлов на основе идентификатора - PullRequest
1 голос
/ 03 марта 2020

Я пытаюсь запросить XML с SQL. Предположим, у меня есть следующее XML.

<xml>
    <dataSetData>
        <text>ABC</text>
    </dataSetData>
    <generalData>
        <id>123</id>
        <text>text data</text>
    </generalData>
    <generalData>
        <id>456</id>
        <text>text data 2</text>
    </generalData>
    <specialData>
        <id>123</id>
        <text>special data text</text>
    </specialData>
    <specialData>
        <id>456</id>
        <text>special data text 2</text>
    </specialData>
</xml>

Я хочу написать запрос SELECT, который возвращает 2 строки следующим образом:

DataSetData | GeneralDataID | GeneralDataText | SpecialDataTest
ABC         | 123           | text data       | special data text
ABC         | 456           | text data  2    | special data text 2

Мой текущий подход заключается в следующем:

SELECT 
    dataset.nodes.value('(dataSetData/text)[1]', 'nvarchar(500)'),
    general.nodes.value('(generalData/text)[1]', 'nvarchar(500)'),
    special.nodes.value('(specialData/text)[1]', 'nvarchar(500)'),
FROM @MyXML.nodes('xml') AS dataset(nodes)
   OUTER APPLY @MyXML.nodes('xml/generalData') AS general(nodes)
   OUTER APPLY @MyXML.nodes('xml/specialData') AS special(nodes)
WHERE 
    general.nodes.value('(generalData/text/id)[1]', 'nvarchar(500)') = special.nodes.value('(specialData/text/id)[1]', 'nvarchar(500)')

Что мне здесь не нравится, так это то, что я должен использовать OUTER APPLY дважды и что я должен использовать предложение WHERE для JOIN правильных элементов.

Поэтому мой вопрос: Можно ли построить запрос таким образом, чтобы мне не приходилось использовать предложение WHERE таким образом, потому что я совершенно уверен, что это очень негативно влияет на производительность, если файлы становятся больше.

Разве не возможно JOIN правильные узлы (то есть соответствующие узлы generalData и specialData) с некоторым оператором XPATH?

Ответы [ 3 ]

2 голосов
/ 03 марта 2020

Я хочу предложить еще одно решение:

DECLARE @xml XML=
N'<xml>
    <dataSetData>
        <text>ABC</text>
    </dataSetData>
    <generalData>
        <id>123</id>
        <text>text data</text>
    </generalData>
    <generalData>
        <id>456</id>
        <text>text data 2</text>
    </generalData>
    <specialData>
        <id>123</id>
        <text>special data text</text>
    </specialData>
    <specialData>
        <id>456</id>
        <text>special data text 2</text>
    </specialData>
</xml>';

- Запрос

SELECT @xml.value('(/xml/dataSetData/text/text())[1]','varchar(100)')
      ,B.*
      ,@xml.value('(/xml/specialData[(id/text())[1] cast as xs:int? = sql:column("B.General_Id")]/text/text())[1]','varchar(100)') AS Special_Text
FROM @xml.nodes('/xml/generalData') A(gd)
CROSS APPLY(SELECT A.gd.value('(id/text())[1]','int') AS General_Id
                  ,A.gd.value('(text/text())[1]','varchar(100)') AS General_Text) B;

Идея вкратце:

  • Мы можем прочитать <dataSetData>, как это не повторяется, непосредственно из переменной.
  • Мы можем использовать .nodes(), чтобы получить производный набор всех <generalData> записей.
  • Теперь magi c трюк : я использую APPLY, чтобы получить значения из XML как обычных столбцов в набор результатов.
  • Этот трюк теперь позволяет использовать sql:column() для построения предиката XQuery для поиска соответствующего <specialData>.

Еще один подход с FLWOR

Вы можете попробовать это:

SELECT @xml.query
('
    <xml>
    {
    for $i in distinct-values(/xml/generalData/id/text())
    return
    <combined dsd="{/xml/dataSetData/text/text()}"
              id="{$i}"
              gd="{/xml/generalData[id=$i]/text/text()}"
              sd="{/xml/specialData[id=$i]/text/text()}"/>
    }
    </xml>
');

Результат

<xml>
  <combined dsd="ABC" id="123" gd="text data" sd="special data text" />
  <combined dsd="ABC" id="456" gd="text data 2" sd="special data text 2" />
</xml>

Идея вкратце:

  • С помощью distinct-values() мы получаем список всех значений идентификатора в вашем XML
  • мы можем повторить это и выбрать соответствующие значения
  • Мы возвращаем результат как реструктурированный XML

Теперь вы можете использовать .nodes('/xml/combined') против этого нового XML и получить все значения с легкостью.

Тест производительности

Я просто хочу добавить тест производительности:

CREATE TABLE dbo.TestXml(TheXml XML);
INSERT INTO dbo.TestXml VALUES
(
  (
    SELECT 'blah1' AS [dataSetData/text]
          ,(SELECT o.[object_id] AS [id]
                  ,o.[name]      AS [text] 
            FROM sys.objects o
            FOR XML PATH('generalData'),TYPE)
          ,(SELECT o.[object_id] AS [id]
                  ,o.create_date AS [text] 
            FROM sys.objects o
            FOR XML PATH('specialData'),TYPE)
    FOR XML PATH('xml'),TYPE
  )
)
,(
  (
    SELECT 'blah2' AS [dataSetData/text]
          ,(SELECT o.[object_id] AS [id]
                  ,o.[name]      AS [text] 
            FROM sys.objects o
            FOR XML PATH('generalData'),TYPE)
          ,(SELECT o.[object_id] AS [id]
                  ,o.create_date AS [text] 
            FROM sys.objects o
            FOR XML PATH('specialData'),TYPE)
    FOR XML PATH('xml'),TYPE
  )
)
,(
  (
    SELECT 'blah3' AS [dataSetData/text]
          ,(SELECT o.[object_id] AS [id]
                  ,o.[name]      AS [text] 
            FROM sys.objects o
            FOR XML PATH('generalData'),TYPE)
          ,(SELECT o.[object_id] AS [id]
                  ,o.create_date AS [text] 
            FROM sys.objects o
            FOR XML PATH('specialData'),TYPE)
    FOR XML PATH('xml'),TYPE
  )
);
GO
--just a dummy call to avoid *first call bias*
SELECT x.query('.') FROM dbo.TestXml
                    CROSS APPLY TheXml.nodes('/xml//*') A(x)
GO

DECLARE @t DATETIME2=SYSUTCDATETIME();
--My first approach
SELECT TheXml.value('(/xml/dataSetData/text/text())[1]','varchar(100)') AS DataSetValue
      ,B.*
      ,TheXml.value('(/xml/specialData[(id/text())[1] cast as xs:int? = sql:column("B.General_Id")]/text/text())[1]','varchar(100)') AS Special_Text
INTO dbo.testResult1
FROM dbo.TestXml
CROSS APPLY TheXml.nodes('/xml/generalData') A(gd)
CROSS APPLY(SELECT A.gd.value('(id/text())[1]','int') AS General_Id
                  ,A.gd.value('(text/text())[1]','varchar(100)') AS General_Text) B;
SELECT DATEDIFF(MILLISECOND,@t,SYSUTCDATETIME());
GO


DECLARE @t DATETIME2=SYSUTCDATETIME();
--My second approach
SELECT B.c.value('@dsd','varchar(100)') AS dsd
      ,B.c.value('@id','int') AS id
      ,B.c.value('@gd','varchar(100)') AS gd
      ,B.c.value('@sd','varchar(100)') AS sd
INTO dbo.TestResult2
FROM dbo.TestXml
CROSS APPLY (SELECT TheXml.query
('
    <xml>
    {
    for $i in distinct-values(/xml/generalData/id/text())
    return
    <combined dsd="{/xml/dataSetData/text/text()}"
              id="{$i}"
              gd="{/xml/generalData[id=$i]/text/text()}"
              sd="{/xml/specialData[id=$i]/text/text()}"/>
    }
    </xml>
') AS ResultXml) A
CROSS APPLY A.ResultXml.nodes('/xml/combined') B(c) 

SELECT DATEDIFF(MILLISECOND,@t,SYSUTCDATETIME());
GO

DECLARE @t DATETIME2=SYSUTCDATETIME();
--Yitzhak'S approach
SELECT c.value('(dataSetData/text/text())[1]', 'VARCHAR(20)') AS DataSetData
    , g.value('(id/text())[1]', 'INT') AS GeneralDataID 
    , g.value('(text/text())[1]', 'VARCHAR(30)') AS GeneralDataText
    , sp.value('(id/text())[1]', 'INT') AS SpecialDataID 
    , sp.value('(text/text())[1]', 'VARCHAR(30)') AS SpecialDataTest
INTO dbo.TestResult3
FROM dbo.TestXml
CROSS APPLY TheXml.nodes('/xml') AS t(c)
    OUTER APPLY c.nodes('generalData') AS general(g)
    OUTER APPLY c.nodes('specialData') AS special(sp)
WHERE g.value('(id/text())[1]', 'INT') = sp.value('(id/text())[1]', 'INT');

SELECT DATEDIFF(MILLISECOND,@t,SYSUTCDATETIME());
GO

SELECT * FROM TestResult1;
SELECT * FROM TestResult2;
SELECT * FROM TestResult3;
GO
--careful with real data!
DROP TABLE testResult1
DROP TABLE testResult2
DROP TABLE testResult3
DROP TABLE dbo.TestXml;

Результат явно указывает на XQuery . (Кто-то может сказать так печально! сейчас :-)).

Предикатный подход является самым медленным (4700 мс). Подход FLWOR имеет ранг 2 (1200 мс), и победитель - tatatataaaaa - подход Ицхака (400 мс, фактор ~ 10!).

Какое решение лучше для вас, будет зависеть на фактических данных (количество элементов на XML, количество XML и т. д.). Но визуальная элегантность - к сожалению - не единственный параметр для этого выбора: -)

2 голосов
/ 03 марта 2020

Ваши выражения XPath полностью отключены.

Пожалуйста, попробуйте следующее. Это довольно эффективно. Вы можете проверить его производительность с большим XML.

SQL

-- DDL and sample data population, start
DECLARE @xml XML = 
N'<xml>
    <dataSetData>
        <text>ABC</text>
    </dataSetData>
    <generalData>
        <id>123</id>
        <text>text data</text>
    </generalData>
    <generalData>
        <id>456</id>
        <text>text data 2</text>
    </generalData>
    <specialData>
        <id>123</id>
        <text>special data text</text>
    </specialData>
    <specialData>
        <id>456</id>
        <text>special data text 2</text>
    </specialData>
</xml>';
-- DDL and sample data population, end

SELECT c.value('(dataSetData/text/text())[1]', 'VARCHAR(20)') AS DataSetData
    , g.value('(id/text())[1]', 'INT') AS GeneralDataID 
    , g.value('(text/text())[1]', 'VARCHAR(30)') AS GeneralDataText
    , sp.value('(id/text())[1]', 'INT') AS SpecialDataID 
    , sp.value('(text/text())[1]', 'VARCHAR(30)') AS SpecialDataTest
FROM @xml.nodes('/xml') AS t(c)
    OUTER APPLY c.nodes('generalData') AS general(g)
    OUTER APPLY c.nodes('specialData') AS special(sp)
WHERE g.value('(id/text())[1]', 'INT') = sp.value('(id/text())[1]', 'INT');

Выход

+-------------+---------------+-----------------+---------------+---------------------+
| DataSetData | GeneralDataID | GeneralDataText | SpecialDataID |   SpecialDataTest   |
+-------------+---------------+-----------------+---------------+---------------------+
| ABC         |           123 | text data       |           123 | special data text   |
| ABC         |           456 | text data 2     |           456 | special data text 2 |
+-------------+---------------+-----------------+---------------+---------------------+
1 голос
/ 04 марта 2020

Извините, что добавил это как другой ответ, но я не хочу добавлять к другому ответу. Уже достаточно большой: -)

Сочетание Ицхака и моего стало еще быстрее:

- это дополнительный код, который нужно поместить в сравнение производительности

DECLARE @t DATETIME2=SYSUTCDATETIME();

SELECT TheXml.value('(/xml/dataSetData/text/text())[1]', 'VARCHAR(20)') AS DataSetData
     ,B.*
    , sp.value('(id/text())[1]', 'INT') AS SpecialDataID 
    , sp.value('(text/text())[1]', 'VARCHAR(30)') AS SpecialDataTest
INTO dbo.TestResult4
FROM dbo.TestXml
CROSS APPLY TheXml.nodes('/xml/generalData') AS A(g)
CROSS APPLY(SELECT g.value('(id/text())[1]', 'INT') AS GeneralDataID 
                 , g.value('(text/text())[1]', 'VARCHAR(30)') AS GeneralDataText) B
OUTER APPLY TheXml.nodes('/xml/specialData[id=sql:column("B.GeneralDataID")]') AS special(sp);

SELECT DATEDIFF(MILLISECOND,@t,SYSUTCDATETIME());

Идея вкратце:

  • Мы читаем <dataSetData> напрямую (без повторов)
  • Мы используем APPLY .nodes(), чтобы получить все <generalData>
  • Мы используем APPLY SELECT для извлечения значений <generalData> элементов как реальных столбцов .
  • Мы используем еще один APPLY .nodes() для извлечения соответствующих <specialData> элементов

Одно из преимуществ этого решения: если для каждого элемента общих данных может быть несколько записей специальных данных, это тоже сработает.

Это самый быстрый в моем случае тест (~ 300 мс).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...