使用 SQL 将 XML 结构转置/展平到列

Posted

技术标签:

【中文标题】使用 SQL 将 XML 结构转置/展平到列【英文标题】:Using SQL to transpose/flatten XML structure to columns 【发布时间】:2013-02-16 05:41:25 【问题描述】:

我正在使用 SQL Server (2008/2012),我知道从大量搜索中得到了类似的答案,但是我似乎无法找到适合我的案例的示例/指针。

我在 SQL Server 表中有一个 XML 列,其中包含这些数据:

<Items>
 <Item>
  <FormItem>
    <Text>FirstName</Text>
    <Value>My First Name</Value>
  </FormItem>
  <FormItem>
    <Text>LastName</Text>
    <Value>My Last Name</Value>
  </FormItem>
  <FormItem>
    <Text>Age</Text>
    <Value>39</Value>
  </FormItem>
 </Item>
 <Item>
  <FormItem>
    <Text>FirstName</Text>
    <Value>My First Name 2</Value>
  </FormItem>
  <FormItem>
    <Text>LastName</Text>
    <Value>My Last Name 2</Value>
  </FormItem>
  <FormItem>
    <Text>Age</Text>
    <Value>40</Value>
  </FormItem>
 </Item>
</Items>

因此,即使&lt;FormItem&gt; 的结构相同,我也可以拥有多个(通常不超过 20-30 个)表单项集..

我实际上是在尝试以以下格式从 SQL 返回查询,即基于 /FormItem/Text 的动态列:

FirstName         LastName         Age    ---> More columns as new `<FormItem>` are returned
My First Name     My Last Name     39          Whatever value etc..
My First Name 2   My Last Name 2   40          

所以,此刻我有以下几点:

select 
    Tab.Col.value('Text[1]','nvarchar(100)') as Question,
    Tab.Col.value('Value[1]','nvarchar(100)') as Answer
from
    @Questions.nodes('/Items/Item/FormItem') Tab(Col)

当然,这并没有将我的 XML 行转换为列,而且显然是用字段修复的。我一直在尝试各种“动态 SQL”方法,其中 SQL 执行不同选择(在我的情况下)@ 987654325@ 节点,然后使用某种 Pivot?但我似乎找不到神奇的组合来返回我需要的结果作为每一行的动态列集(&lt;Items&gt; 集合中的&lt;Item&gt;)。

看到这么多非常相似的例子,我确信它可以完成,但是我又一次找不到解决方案!

感谢您的帮助!

【问题讨论】:

【参考方案1】:

解析 XML 相当昂贵,因此您可以使用名称-值列表创建一个临时表,然后将其用作动态数据透视查询的源,而不是解析一次来构建动态查询和一次获取数据。 dense_rank 在那里创建 ID 以进行旋转。 要在动态查询中构建列列表,它使用for xml path('') 技巧。

此解决方案要求您的表具有主键 (ID)。如果您在变量中包含 XML,则可以稍微简化一下。

select dense_rank() over(order by ID, I.N) as ID,
       F.N.value('(Text/text())[1]', 'varchar(max)') as Name,
       F.N.value('(Value/text())[1]', 'varchar(max)') as Value
into #T
from YourTable as T
  cross apply T.XMLCol.nodes('/Items/Item') as I(N)
  cross apply I.N.nodes('FormItem') as F(N)

declare @SQL nvarchar(max)
declare @Col nvarchar(max)

select @Col = 
  (
  select distinct ','+quotename(Name)
  from #T
  for xml path(''), type
  ).value('substring(text()[1], 2)', 'nvarchar(max)')

set @SQL = 'select '+@Col+'
            from #T
            pivot (max(Value) for Name in ('+@Col+')) as P'

exec (@SQL)

drop table #T

SQL Fiddle

【讨论】:

谢谢!就像我需要的那样工作..实际上我在所示示例之上还有另一个级别,但这实际上是将 组合在一起..但关键部分是将变量 xml 节点 显示为列重复 的行.. 非常感谢您提供非常详细的示例!【参考方案2】:
select Tab.Col.value('(FormItem[Text = "FirstName"]/Value)[1]', 'varchar(32)') as FirstName, 
        Tab.Col.value('(FormItem[Text = "LastName"]/Value)[1]', 'varchar(32)') as LastName, 
        Tab.Col.value('(FormItem[Text = "Age"]/Value)[1]', 'int') as Age
from @Questions.nodes('/Items/Item') Tab(Col)

【讨论】:

谢谢,很好的例子,实际上以前没有见过这种技术,但是它不会将字段动态显示为列 - 如果您事先知道列的数量可能没问题,但是来自@Mikael 的上述示例满足了我的需要 - 不过,感谢您的回复,并且再次确定非常简单的干净示例。【参考方案3】:

我想添加我的“自己的答案”真的只是为了完整,可能会帮助其他人。但是它绝对是基于上面@Mikael 的巨大帮助!再说一次,这真的只是为了完整性-@Mikael 的所有荣誉。

基本上我最终得到了以下过程。我需要选择一些数据/过滤器,并获取一些连接数据,并允许对一些输入参数进行一些布尔过滤。然后进入下一部分,通过交叉应用创建我的关系数据和所需 xml 节点的临时表。最后一步是旋转结果/从选定的 XML 节点动态创建列..

CREATE PROCEDURE [dbo].[usp_RPT_ExtractFlattenentries]
    @CompanyID          int,
    @MainSelector       nvarchar(50) = null,
    @SecondarySelector      nvarchar(255) = null,
    @DateFrom           datetime = '01-jan-2012',
    @DateTo             datetime = '31-dec-2100',
    @SysReference       nvarchar(20) = null
AS
BEGIN
    SET NOCOUNT ON;

    --  Create the table var to hold the XML form data from the entries
    declare @FeedbackXml table (
        ID int identity primary key,
        XMLCol xml,
        CompanyName nvarchar(20),
        SysReference nvarchar(20),
        RecordDate datetime,
        EntryName  nvarchar(255),
        MainSelector nvarchar(50)
    )

    --  STEP 1: Get the raw submission data based on the params passed in
    --  *Note: The double casting is necessary as the "form" field is nvarchar (not varchar) and we need xml in UTF-8 format
    begin
        insert into @FeedbackXml
            (XMLCol, CompanyName, SysReference, RecordDate, EntryName, MainSelector)
        select cast(cast(e.form as nvarchar(max)) as xml), c.name, e.SysReference, e.RecordDate, e.name, e.wizard
        from 
            entries s
        left join
            companies o on e.companies = c.ID
        where 
            (@CompanyID = -1 or @CompanyID = e.companies)
        and
            (@MainSelector is null or @MainSelector = e.wizard)
        and
            (@SecondarySelector is null or @SecondarySelector = e.name)
        and
            (@SysReference is null or @SysReference = e.SysReference)
        and
            (e.RecordDate >= @DateFrom and e.RecordDate <= @DateTo)
    end

    --  STEP 2: Flatten the required XML structure to provide a base for the pivot, and include other fields we wish to output
    select dense_rank() over(order by ID) as ID,
            T.RecordDate, T.CompanyName, T.SysReference, T.EntryName, T.MainSelector,
            F.N.value('(FieldNameNode/text())[1]', 'nvarchar(max)') as FieldName,
            F.N.value('(FieldNameValue/text())[1]', 'nvarchar(max)') as FieldValue
    into #TempData
    from @FeedbackXml as T
        cross apply T.XMLCol.nodes('/root/companies/') as I(N) -- Xpath to the desired node start point
        cross apply I.N.nodes('company') as F(N) -- The actual node collection that forms the "field name" and "field value" data

    --  STEP 3: Pivot the #TempData table creating a dynamic column structure based on the selected XML nodes in step 2
    declare @SQL nvarchar(max)
    declare @Col nvarchar(max)

    select @Col = 
      (
      select distinct ','+quotename(FieldName)
      from #TempData
      for xml path(''), type
      ).value('substring(text()[1], 2)', 'nvarchar(max)')

    set @SQL = 'select CompanyName, SysReference, EntryName, MainSelector, RecordDate, '+@Col+'
                from #TempData
                pivot (max(FieldValue) for FieldName in ('+@Col+')) as P'

    exec (@SQL)
    drop table #TempData

END

再次,真的只是添加了这个答案,以从我的角度提供一个完整的画面,并可能对其他人有所帮助。

【讨论】:

以上是关于使用 SQL 将 XML 结构转置/展平到列的主要内容,如果未能解决你的问题,请参考以下文章

使用 flex 实现四列到列列表的反馈 - 第 2 轮

Excel从列到列的映射

PostgreSQL 交叉表转置行到列

Spark:行到列(如转置或枢轴)

Pandas 在行上设置多索引,然后转置到列

使用 pyspark 将结构数组旋转到列中 - 不爆炸数组