使用属性将列转换为xml集合

时间:2017-11-21 22:28:56

标签: sql-server xml tsql

我想为表中的每一行添加一行,但要尽可能高效地将列转换为xml集合。在下面的示例中,它是一个展平的表 - 但在现实世界中,列需要许多连接才能获得 - 导致许多读取。

例如:

declare @tbl table (
 Id int identity (1, 1) primary key
,PolicyNumber   varchar(100) not null
,InsuredName    varchar(100) not null
,EffectiveDate  datetime2 not null
,Premium        numeric(22, 7)
)

insert into @tbl (PolicyNumber, InsuredName, EffectiveDate, Premium)
values ('2017A-ALKJ02', 'Insured Number 1', '2017-01-01', 1000)
  ,('2017A-BSDSDFWEF2', 'Insured Number 2', '2017-06-01', 2000)

select Id
  ,(select [@name] = 'PolicyNumber', [@type] = 'string', [text()] = PolicyNumber from @tbl [inner] where [inner].Id = [outer].Id for xml path ('dt'))
  ,(select [@name] = 'InsuredName', [@type] = 'string', [text()] = [inner].InsuredName from @tbl [inner] where [inner].Id = [outer].Id for xml path ('dt'))
  ,(select [@name] = 'EffectiveDate', [@type] = 'datetime', [text()] = [inner].EffectiveDate from @tbl [inner] where [inner].Id = [outer].Id for xml path ('dt'))
  ,(select [@name] = 'Premium', [@type] = 'numeric', [text()] = [inner].Premium from @tbl [inner] where [inner].Id = [outer].Id for xml path ('dt'))
from @tbl [outer]

在各自的xml元素中产生各列,但我在每一行后都有它的主键和结构:

<dts>
  <dt name="PolicyNumber" type="string">2017A-ALKJ02</dt>
  <dt name="InsuredName" type="string">Insured Number 1</dt>
  <dt name="EffectiveDate" type="datetime">2017-01-01T00:00:00</dt>
  <dt name="Premium" type="numeric">1000.0000000</dt>
</dts>

我知道这可以通过许多子查询来实现,但有没有人知道一个简单的方法来获得一个足够智能的单个查询,以便将PK和所有单独的列转换为dts集合中的元素?

4 个答案:

答案 0 :(得分:1)

这可以使用不同的技术来解决。以下是使用UNPIVOT生成类型列的其中一个:

WITH DataSource AS
(
    SELECT [id]
          ,[column]
          ,[value]
          ,CASE [column]
                WHEN 'PolicyNumber' THEN 'string'
                WHEN 'InsuredName' THEN 'string'
                WHEN 'EffectiveDate' THEN 'datetime'
                WHEN 'Premium' THEN 'numeric'
           END AS [type]
    FROM 
    (
        SELECT Id
              ,PolicyNumber
              ,InsuredName
              ,CAST(EffectiveDate AS VARCHAR(100)) AS EffectiveDate 
              ,CAST(Premium AS VARCHAR(100)) AS Premium
        FROM @tbl
    ) DS
    UNPIVOT
    (
        [value] FOR [column] IN ([PolicyNumber], [InsuredName], [EffectiveDate], [Premium])
    ) UNPVT
)
SELECT DISTINCT [id]
               ,[Info]
FROM @tbl DS
CROSS APPLY
(
    SELECT [column]  "@name"
          ,[type] "@type"
          ,CASE WHEN [column] = 'EffectiveDate' THEN CONVERT(VARCHAR(32), CAST([value] AS DATETIME2), 126) ELSE [value] END "text()"
    FROM DataSource Info
    WHERE DS.[Id] = Info.[Id]
    FOR XML PATH('dt'), ROOT('dts')
) DSInfo (Info);

它会为每行提供这样的XML:

<dts>
    <dt name="PolicyNumber" type="string">2017A-ALKJ02</dt>
    <dt name="InsuredName" type="string">Insured Number 1</dt>
    <dt name="EffectiveDate" type="datetime">2017-01-01T00:00:00</dt>
    <dt name="Premium" type="numeric">1000.0000000</dt>
</dts>

答案 1 :(得分:1)

如果您事先了解所有元数据(列名称和类型),则可以非常简单地完成此操作:

declare @tbl table (
 Id int identity (1, 1) primary key
,PolicyNumber   varchar(100) not null
,InsuredName    varchar(100) not null
,EffectiveDate  datetime2 not null
,Premium        numeric(22, 7)
);

insert into @tbl (PolicyNumber, InsuredName, EffectiveDate, Premium)
values ('2017A-ALKJ02', 'Insured Number 1', '2017-01-01', 1000)
  ,('2017A-BSDSDFWEF2', 'Insured Number 2', '2017-06-01', 2000);

SELECT 'PolicyNumber' AS [dt/@name]
      ,'string' AS [dt/@type]
      ,PolicyNumber AS [dt]
      ,''
      ,'InsuredName' AS [dt/@name]
      ,'string' AS [dt/@type]
      ,InsuredName AS [dt]
      ,''
      ,'EffectiveDate' AS [dt/@name]
      ,'datetime' AS [dt/@type]
      ,EffectiveDate AS [dt]
      ,''
      ,'Premium' AS [dt/@name]
      ,'numeric' AS [dt/@type]
      ,Premium AS [dt]
FROM @tbl 
FOR XML PATH('dts'),ROOT('root')

结果

<root>
  <dts>
    <dt name="PolicyNumber" type="string">2017A-ALKJ02</dt>
    <dt name="InsuredName" type="string">Insured Number 1</dt>
    <dt name="EffectiveDate" type="datetime">2017-01-01T00:00:00</dt>
    <dt name="Premium" type="numeric">1000.0000000</dt>
  </dts>
  <dts>
    <dt name="PolicyNumber" type="string">2017A-BSDSDFWEF2</dt>
    <dt name="InsuredName" type="string">Insured Number 2</dt>
    <dt name="EffectiveDate" type="datetime">2017-06-01T00:00:00</dt>
    <dt name="Premium" type="numeric">2000.0000000</dt>
  </dts>
</root>

诀窍是无名的空&#34;列&#34;在<dt>个元素之间。引擎被告知:看,有一个新元素,先关闭一个元素并开始一个新元素!

否则你会收到错误......

更新:通用方法

这将提取所有元数据并构造与上面相同的语句,该语句使用EXEC执行:

CREATE TABLE tmpTbl (
 Id int identity (1, 1) primary key
,PolicyNumber   varchar(100) not null
,InsuredName    varchar(100) not null
,EffectiveDate  datetime2 not null
,Premium        numeric(22, 7)
);

insert into tmpTbl (PolicyNumber, InsuredName, EffectiveDate, Premium)
values ('2017A-ALKJ02', 'Insured Number 1', '2017-01-01', 1000)
  ,('2017A-BSDSDFWEF2', 'Insured Number 2', '2017-06-01', 2000);

DECLARE @cmd NVARCHAR(MAX)='SELECT ' +
STUFF(
(
    SELECT ',''' + c.COLUMN_NAME + ''' AS [dt/@name]' +
          ',''' + c.DATA_TYPE + ''' AS [dt/@type]' +
          ',' + QUOTENAME(c.COLUMN_NAME) + ' AS [dt]' + 
          ',''''' 
    FROM INFORMATION_SCHEMA.COLUMNS AS c WHERE TABLE_NAME='tmpTbl' 
    FOR XML PATH('')
),1,1,'') + 
'FROM tmpTbl FOR XML PATH(''dts''),ROOT(''root'')';
EXEC( @cmd);
GO
--cleanup (careful with real data)
--DROP TABLE tmpTbl;

如果你需要例如&#34; string&#34;而不是&#34; varchar&#34;您需要映射表或CASE表达式。

答案 2 :(得分:0)

在SQL Server中没有方便的方法来生成这种输出。一种可能的解决方案可能是FLWOR转换,但我怀疑它确实会非常复杂。

另一种方法是使用UNPIVOT,如下例所示,尽管它远非易于扩展:

select (
    select upt.ColumnName as [@name],
        isnull(dt.ColumnType, 'string') as [@type],
        upt.ColumnValue as [text()]
    from (
        select t.Id, t.PolicyNumber, t.InsuredName,
            convert(varchar(100), t.EffectiveDate, 126) as [EffectiveDate],
            cast(t.Premium as varchar(100)) as [Premium]
        from @tbl t
    ) sq
    unpivot (
        ColumnValue for ColumnName in (
            sq.PolicyNumber, sq.InsuredName, sq.EffectiveDate, sq.Premium
        )
    ) upt
        left join (values
            ('EffectiveDate', 'datetime'),
            ('Premium', 'numeric')
        ) dt (ColumnName, ColumnType) on upt.ColumnName = dt.ColumnName
    where upt.Id = t.Id
    for xml path('dt'), type
    )
from @tbl t
for xml path('dts'), type;

首先,您需要将列值转换为行,以便您的关系输出将开始类似于您所需的XML。为了使所有列适合相同的ColumnValue,您必须将它们转换为相同的数据类型。

其次,您必须提供type属性的数据。在上面的示例中,我使用了内联表构造函数,因为您无法动态地从TV列获取数据类型。如果实际数据驻留在静态表中,则可以尝试将其与系统元数据对象(例如INFORMATION_SCHEMA.COLUMNS)连接。虽然对于您所需的值,您可能还需要一个额外的映射表(例如,为varchar替换string)。

最后,为了为每个原始表行获取单个/dts元素,我再次将表格中的未标记数据加入。这允许生成所需的XML元素嵌套,因为root()子句不适用于此。

答案 3 :(得分:0)

感谢您的建议!我决定将查询扁平化为临时表是最好的方法,然后使用上面的通用方法加上空白列技巧满足需要。

if object_id('tempdb..#tmp') is not null
    drop table #tmp

create table #tmp (
     Id int identity (1, 1) primary key
    ,PolicyNumber   varchar(100) not null
    ,InsuredName    varchar(100) not null
    ,EffectiveDate  datetime2 not null
    ,Premium        numeric(22, 7)
);

insert into #tmp (PolicyNumber, InsuredName, EffectiveDate, Premium)
values ('2017A-ALKJ02', 'Insured Number 1', '2017-01-01', 1000)
  ,('2017A-BSDSDFWEF2', 'Insured Number 2', '2017-06-01', 2000);

DECLARE @cmd NVARCHAR(MAX)='
select [outer].Id
      ,convert(xml, (SELECT ' +
            STUFF(
            (
                SELECT ',[dt/@n] = ''' + c.name + '''' +
                      ',[dt/@t] = ''' + case when t.name = 'bit' then 'b'
                                             when t.name in ('date', 'smalldatetime', 'datetime2', 'datetime', 'datetimeoffset') then 'd'
                                             when t.name = 'bigint' then 'g'
                                             when t.name in ('tinyint', 'smallint', 'int', 'time', 'timestamp') then 'i'
                                             when t.name in ('real', 'smallmoney', 'money', 'float', 'decimal', 'numeric') then 'n'
                                             else 's'
                                        end + '''' +
                      ',[dt] = ' + QUOTENAME(c.name) +  
                      ',''''' 
                FROM tempdb.sys.columns c 
                  inner join sys.types t on c.system_type_id = t.system_type_id
                where object_id = object_id('tempdb..#tmp')
                FOR XML PATH('')
            ),1,1,'') + '
        FROM #tmp [inner]
        where [inner].Id = [outer].id
        for xml path (''dts'')))
from #tmp [outer]'

EXEC( @cmd);