【问题标题】:SQL - How to extract multiple attributes from XML field within a tableSQL - 如何从表中的 XML 字段中提取多个属性
【发布时间】:2020-12-21 07:32:38
【问题描述】:

我正在尝试对一个表运行 SQL 查询,该表具有一个包含 XML 数据的字段,但该 XML 包含多个需要转换为一个字段的值。请注意,该字段是 XML 内容,但实际字段类型设置为 nvarchar(max),而不是 xml。

编辑:版本是SQL Server 2014 Express Edition

我有一张这样的桌子: [有市场的客户名单]

我想提取同一行中的“marketCode”值(逗号分隔):

|CompanyCode|CompanyName|MarketCode,MarketCode,MarketCode,etc.|Phone|

示例的预期输出(见截图):

|ABC123|JOHN DEERE|AA,BB,CC,DD|555-123-000|
|DEF456|NEW HOLLLAND|AA,FF,GG,HH,KK|555-456-0000|

【问题讨论】:

  • XML 支持高度特定于供应商 - 所以请添加一个标签来指定您是否使用mysqlpostgresqlsql-serveroracledb2 - 或完全不同的东西。
  • 请向我们展示您的尝试...

标签: sql sql-server xml tsql pivot


【解决方案1】:

样本数据

下次请提供文字而不是图片;-)

Markets 定义为nvarchar(max)。数据以 unicode 形式插入(带有 N 前缀)。

create table Company2
(
    Code nvarchar(6),
    Name nvarchar(11),
    Markets nvarchar(max),
    Phone nvarchar(12)
);

insert into Company2 (Code, Name, Markets, Phone) values
(N'ABC123',
 N'JOHN DEERE',
 N'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  <license>
    <company companyCode="ABC123">
      <markets>
        <market marketCode="AA"/>
        <market marketCode="BB"/>
        <market marketCode="CC"/>
        <market marketCode="DD"/>
      </markets>
    </company>
  </license>',
 N'555-123-0000'),
(N'DEF456',
 N'NEW HOLLAND',
 N'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  <license>
    <company companyCode="DEF456">
      <markets>
        <market marketCode="AA"/>
        <market marketCode="FF"/>
        <market marketCode="GG"/>
        <market marketCode="HH"/>
        <market marketCode="KK"/>
      </markets>
    </company>
  </license>',
 N'555-456-0000');

解决方案

无法将Markets 直接转换为XML,因为nvarchar(max) 编码与数据中的“utf-8”冲突。我将转换移动到一个单独的公用表表达式(CTE,cte_convert),从nvarchar(max)varchar(max)XML

下一个 CTE (cte_parse) 现在可以使用 c.MarketsXML.nodes() 将 XML 中的 &lt;market&gt; 节点提取到新列 m.Market 中。从该列中提取@marketCode 属性作为所需值。

然后使用带有for xml path('') 的子查询来连接这些值。

with cte_convert as
(
    select c.Code, c.Name, convert(XML, convert(varchar(max), c.Markets)) as MarketsXML, c.Phone
    from Company2 c
),
cte_parse as
(
    select c.Code, c.Name, m.Market.value('@marketCode', 'nvarchar(10)') as MarketCode, c.Phone
    from cte_convert c
    outer apply c.MarketsXML.nodes('/license/company/markets/market') as m(Market)
)
select  cp.Code,
        cp.Name,
        stuff(( select ',' + cp2.MarketCode as MC
                from cte_parse cp2
                where cp2.Code = cp.Code
                for xml path(''), type).value('.', 'nvarchar(max)'),1,1,'') as MarketCodes,
        cp.Phone
from cte_parse cp
group by cp.Code, cp.Name, cp.Phone;

结果

Code   Name        MarketCodes    Phone
------ ----------- -------------- ------------
ABC123 JOHN DEERE  AA,BB,CC,DD    555-123-0000
DEF456 NEW HOLLAND AA,FF,GG,HH,KK 555-456-0000

此原始解决方案使用从 SQL Server 2017 开始可用的 string_agg() 函数。

样本数据

备注:Markets 列定义为XML 以反映其内容。

create table Company
(
    Code nvarchar(6),
    Name nvarchar(11),
    Markets XML,
    Phone nvarchar(12)
);

insert into Company (Code, Name, Markets, Phone) values
('ABC123',
 'JOHN DEERE',
 '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  <license>
    <company companyCode="ABC123">
      <markets>
        <market marketCode="AA"/>
        <market marketCode="BB"/>
        <market marketCode="CC"/>
        <market marketCode="DD"/>
      </markets>
    </company>
  </license>',
  '555-123-0000'),
('DEF456',
 'NEW HOLLAND',
 '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  <license>
    <company companyCode="DEF456">
      <markets>
        <market marketCode="AA"/>
        <market marketCode="FF"/>
        <market marketCode="GG"/>
        <market marketCode="HH"/>
        <market marketCode="KK"/>
      </markets>
    </company>
  </license>',
  '555-456-0000');

解决方案

with cte_parse as
(
    select c.Code, c.Name, m.Market.value('@marketCode', 'nvarchar(10)') as MarketCode, c.Phone
    from Company c
    outer apply c.Markets.nodes('/license/company/markets/market') as m(Market)
)
select cp.Code, cp.Name, string_agg(cp.MarketCode, ',') as MarketCodes, cp.Phone
from cte_parse cp
group by cp.Code, cp.Name, cp.Phone;

Fiddle

【讨论】:

  • 如果市场列是 nvarchar 但包含一个 XML 字符串,用 convert(xml, market).value 包装它...除此之外,很好地使用 string_agg!
  • @Mitz,因为 xml 声明 (&lt;?xml blah blah ?&gt;) 中有encoding="UTF-8",所以这不起作用...要么使用VARCHAR (1-byte编码)与utf-8NVARCHAR(2 字节编码)与utf-16(或ucs-2)一起使用。最好是 1) 以本机类型存储 XML 或至少 2) 存储没有声明的 XML...
  • @Shnugo 你是对的。如果可以控制插入,那是最好的选择。在某些情况下,我无法控制插入,不得不使用 replace(...); 手动删除声明。但是我所有的 xml 文件都是
  • @Sander,感谢您的详细回复。所以看起来我的 SQL 版本是 SQL Server 2014 Express Edition,而 STRING_AGG 只适用于 SQL 2017 及更高版本。是否有不同的方式来执行 STRING_AGG 函数?另外,XML 的原生字段实际上是 NVARCHAR(max),而不是 XML,所以我认为这也需要是 CAST?
  • @JustinS,提供的新解决方案没有string_agg() 函数,数据类型为nvarchar(max) 和所需的转换。
【解决方案2】:
declare @t table(CompanyMarkets nvarchar(max));
insert into @t(CompanyMarkets)
values(N'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<license>
<company companyCode="ABC123">
<markets>
<market marketCode="AA"/>
<market marketCode="BB"/>
<market marketCode="CC"/>
<market marketCode="DD"/>
</markets>
</company>
</license>');

select
    --remove encoding ...
    try_cast(replace(CompanyMarkets, 'encoding="UTF-8"', '') as xml),
    --... or transform any prolog to a weird&harmless processing instruction 
    try_cast(concat(case when CompanyMarkets like N'<?xml%' then '<?x ' end, CompanyMarkets) as xml) 
from @t;


select *,
    try_cast(concat(case when CompanyMarkets like N'<?xml%' then '<?x ' end, CompanyMarkets) as xml).query('
    for $i in (data(/license/company/markets/market/@marketCode)[1], (for $k in data(/license/company/markets/market/@marketCode)[position()>1] return concat(",", $k)))
    return text {$i}
    ').value('.', 'nvarchar(max)'),
    
    --?? no spaces in marketCodes
    replace(
    try_cast(concat(case when CompanyMarkets like N'<?xml%' then '<?x ' end, CompanyMarkets) as xml).query('data(/license/company/markets/market/@marketCode)').value('.', 'nvarchar(max)'),
    ' ', ','),
    
    stuff(
    try_cast(concat(case when CompanyMarkets like N'<?xml%' then '<?x ' end, CompanyMarkets) as xml).query('
    for $i in data(/license/company/markets/market/@marketCode) return text {concat(",", $i)}
    ').value('.', 'nvarchar(max)'), 1, 1, '')

from @t;

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-04-06
    相关资源
    最近更新 更多