XML 路径聚合:同一子集上多个“选择”的一个子查询
XML path aggregation: one subquery for multiple `selects` on same subset
当使用XML路径进行聚合时,很多时候我需要基于同一集合的不同字符串。考虑这个例子(最后的脚本):
+-----------+-----------+------------+
| firstname | thing | val |
+-----------+-----------+------------+
| mike | Tesla | 50000 |
| mike | Mona Lisa | 3000000000 |
+-----------+-----------+------------+
我要返回这个结果集:
+-----------+---------------------------------------------+------------------------------------------------------------------------+
| firstname | what_I_say_to_friends | what_I_say_to_finance |
+-----------+---------------------------------------------+------------------------------------------------------------------------+
| Mike | My friend Mike owns a Tesla and a Mona Lisa | My friend Mike owns a Tesla priced 50000 and a Mona Lisa priced 3e+009 |
+-----------+---------------------------------------------+------------------------------------------------------------------------+
我知道如何使用两个不同的 xml-path 子查询来做到这一点。但是,由于唯一改变的是要显示的数据的选择(select),而不是基础行,有没有办法用单个 xml 路径来做到这一点?
数据构造&双xml查询:
create table friend ( firstname nvarchar(50) )
insert friend values ('Mike')
create table owns
(
firstname nvarchar(50)
,thing nvarchar(50)
,val float
)
insert owns values
('mike','Tesla',50000),('mike','Mona Lisa',3000000000)
select
f.firstname
,'My friend '+f.firstname+' owns a '+q1.collection_no_value as what_I_say_to_friends
,'My friend '+f.firstname+' owns a '+q2.collection_with_value as what_I_say_to_finance
from
friend f
cross apply
(
select
stuff
(
(
select ' and a ' + o.thing
from owns o
where o.firstname=f.firstname
FOR XML PATH(''), TYPE
).value('.','nvarchar(max)')
,1,7,''
) as collection_no_value
) as q1
cross apply
(
select
stuff
(
(
select ' and a ' + o.thing+' priced '+convert(nvarchar(max),val)
from owns o
where o.firstname=f.firstname
FOR XML PATH(''), TYPE
).value('.','nvarchar(max)')
,1,7, ''
) as collection_with_value
) as q2
如果没有额外的子查询,您无法完全做到这一点,但是您可以避免一次又一次地查询相同的 table。
您需要做的就是在一个子查询中将数据放入单个 XML blob,然后在其他每个子查询中将其查询回来:
select
f.firstname
,'My friend '+f.firstname+' owns a '+q1.collection_no_value as what_I_say_to_friends
,'My friend '+f.firstname+' owns a '+q2.collection_with_value as what_I_say_to_finance
from
friend f
cross apply
(
select (
select o.thing, o.price
from owns o
where o.firstname = f.firstname
FOR XML PATH('row'), TYPE
)
) x(XmlBlob)
cross apply
(
select
stuff
(
(
select ' and a ' + x2.rw.value('(thing/text())[1]','nvarchar(max)')
from x.XmlBlob.nodes('/row') x2(rw)
FOR XML PATH(''), TYPE
).value('text()[1]','nvarchar(max)')
,1,7,''
) as collection_no_value
) as q1
cross apply
(
select
stuff
(
(
select ' and a ' + x2.rw.value('(thing/text())[1]','nvarchar(max)') + ' priced ' + x2.rw.value('(price/text())[1]','nvarchar(max)')
from x.XmlBlob.nodes('/row') x2(rw)
FOR XML PATH(''), TYPE
).value('text()[1]','nvarchar(max)')
,1,7, ''
) as collection_with_value
) as q2
如你所见,其实更啰嗦。另一方面,如果创建 blob 的子查询非常复杂,那么 它可能会更高效,因为子查询只执行一次。
你也可以在SQL Server 2016
中创建一个JSON数组达到同样的效果
select
f.firstname
,'My friend '+f.firstname+' owns a '+q1.collection_no_value as what_I_say_to_friends
,'My friend '+f.firstname+' owns a '+q2.collection_with_value as what_I_say_to_finance
from
friend f
cross apply
(
select (
select o.thing, o.price
from owns o
where o.firstname = f.firstname
FOR JSON PATH
)
) j(JsonBlob)
cross apply
(
select
stuff
(
(
select ' and a ' + JSON_VALUE(j2.value, '$.thing')
from OPENJSON(j.JsonBlob) j2
FOR XML PATH(''), TYPE
).value('text()[1]','nvarchar(max)')
,1,7,''
) as collection_no_value
) as q1
cross apply
(
select
stuff
(
(
select ' and a ' + JSON_VALUE(j2.value, '$.thing') + ' priced ' + JSON_VALUE(j2.value, '$.price')
from OPENJSON(j.JsonBlob) j2
FOR XML PATH(''), TYPE
).value('text()[1]','nvarchar(max)')
,1,7, ''
) as collection_with_value
) as q2
显然,在 SQL Server 2017+ 中,您可以只使用 STRING_AGG
:
select
f.firstname
,'My friend '+f.firstname+' owns a ' + STRING_AGG(CAST(o.thing AS nvarchar(max)), ' and a ') as what_I_say_to_friends
,'My friend '+f.firstname+' owns a ' + STRING_AGG(o.thing + ' priced ' + convert(nvarchar(max), o.price), ' and a ') as what_I_say_to_finance
from
friend f
group by f.firstname
甚至这种方式,基于纯 XQuery。
SQL
-- DDL and sample data population, start
DECLARE @owns TABLE (firstname nvarchar(50), thing nvarchar(50), val float);
INSERT @owns VALUES
('mike','Tesla',50000),
('mike','Mona Lisa',3000000000);
-- DDL and sample data population, end
DECLARE @separator VARCHAR(10) = ' and a ';
WITH rs AS
(
SELECT firstname
, (
SELECT *
FROM @owns AS c
WHERE c.firstname = p.firstname
FOR XML PATH('r'), TYPE, ROOT('root')
) AS xmldata
FROM @owns AS p
GROUP BY p.firstname
)
SELECT *
, 'My friend ' + firstname + ' owns a ' + xmldata.query('
for $x in /root/r
return if ($x is (/root/r[position() = last()])[1]) then string($x/thing[1])
else concat(($x/thing/text())[1], sql:variable("@separator"))
').value('text()[1]', 'VARCHAR(MAX)') AS what_I_say_to_friends
, 'My friend ' + firstname + ' owns a ' + xmldata.query('
for $x in /root/r
let $token := concat(string($x/thing[1]), " priced ", string($x/val[1]))
return if ($x is (/root/r[position() = last()])[1]) then $token
else concat($token, sql:variable("@separator"))
').value('text()[1]', 'VARCHAR(MAX)') AS what_I_say_to_finance
FROM rs;
当使用XML路径进行聚合时,很多时候我需要基于同一集合的不同字符串。考虑这个例子(最后的脚本):
+-----------+-----------+------------+
| firstname | thing | val |
+-----------+-----------+------------+
| mike | Tesla | 50000 |
| mike | Mona Lisa | 3000000000 |
+-----------+-----------+------------+
我要返回这个结果集:
+-----------+---------------------------------------------+------------------------------------------------------------------------+
| firstname | what_I_say_to_friends | what_I_say_to_finance |
+-----------+---------------------------------------------+------------------------------------------------------------------------+
| Mike | My friend Mike owns a Tesla and a Mona Lisa | My friend Mike owns a Tesla priced 50000 and a Mona Lisa priced 3e+009 |
+-----------+---------------------------------------------+------------------------------------------------------------------------+
我知道如何使用两个不同的 xml-path 子查询来做到这一点。但是,由于唯一改变的是要显示的数据的选择(select),而不是基础行,有没有办法用单个 xml 路径来做到这一点?
数据构造&双xml查询:
create table friend ( firstname nvarchar(50) )
insert friend values ('Mike')
create table owns
(
firstname nvarchar(50)
,thing nvarchar(50)
,val float
)
insert owns values
('mike','Tesla',50000),('mike','Mona Lisa',3000000000)
select
f.firstname
,'My friend '+f.firstname+' owns a '+q1.collection_no_value as what_I_say_to_friends
,'My friend '+f.firstname+' owns a '+q2.collection_with_value as what_I_say_to_finance
from
friend f
cross apply
(
select
stuff
(
(
select ' and a ' + o.thing
from owns o
where o.firstname=f.firstname
FOR XML PATH(''), TYPE
).value('.','nvarchar(max)')
,1,7,''
) as collection_no_value
) as q1
cross apply
(
select
stuff
(
(
select ' and a ' + o.thing+' priced '+convert(nvarchar(max),val)
from owns o
where o.firstname=f.firstname
FOR XML PATH(''), TYPE
).value('.','nvarchar(max)')
,1,7, ''
) as collection_with_value
) as q2
如果没有额外的子查询,您无法完全做到这一点,但是您可以避免一次又一次地查询相同的 table。
您需要做的就是在一个子查询中将数据放入单个 XML blob,然后在其他每个子查询中将其查询回来:
select
f.firstname
,'My friend '+f.firstname+' owns a '+q1.collection_no_value as what_I_say_to_friends
,'My friend '+f.firstname+' owns a '+q2.collection_with_value as what_I_say_to_finance
from
friend f
cross apply
(
select (
select o.thing, o.price
from owns o
where o.firstname = f.firstname
FOR XML PATH('row'), TYPE
)
) x(XmlBlob)
cross apply
(
select
stuff
(
(
select ' and a ' + x2.rw.value('(thing/text())[1]','nvarchar(max)')
from x.XmlBlob.nodes('/row') x2(rw)
FOR XML PATH(''), TYPE
).value('text()[1]','nvarchar(max)')
,1,7,''
) as collection_no_value
) as q1
cross apply
(
select
stuff
(
(
select ' and a ' + x2.rw.value('(thing/text())[1]','nvarchar(max)') + ' priced ' + x2.rw.value('(price/text())[1]','nvarchar(max)')
from x.XmlBlob.nodes('/row') x2(rw)
FOR XML PATH(''), TYPE
).value('text()[1]','nvarchar(max)')
,1,7, ''
) as collection_with_value
) as q2
如你所见,其实更啰嗦。另一方面,如果创建 blob 的子查询非常复杂,那么 它可能会更高效,因为子查询只执行一次。
你也可以在SQL Server 2016
中创建一个JSON数组达到同样的效果select
f.firstname
,'My friend '+f.firstname+' owns a '+q1.collection_no_value as what_I_say_to_friends
,'My friend '+f.firstname+' owns a '+q2.collection_with_value as what_I_say_to_finance
from
friend f
cross apply
(
select (
select o.thing, o.price
from owns o
where o.firstname = f.firstname
FOR JSON PATH
)
) j(JsonBlob)
cross apply
(
select
stuff
(
(
select ' and a ' + JSON_VALUE(j2.value, '$.thing')
from OPENJSON(j.JsonBlob) j2
FOR XML PATH(''), TYPE
).value('text()[1]','nvarchar(max)')
,1,7,''
) as collection_no_value
) as q1
cross apply
(
select
stuff
(
(
select ' and a ' + JSON_VALUE(j2.value, '$.thing') + ' priced ' + JSON_VALUE(j2.value, '$.price')
from OPENJSON(j.JsonBlob) j2
FOR XML PATH(''), TYPE
).value('text()[1]','nvarchar(max)')
,1,7, ''
) as collection_with_value
) as q2
显然,在 SQL Server 2017+ 中,您可以只使用 STRING_AGG
:
select
f.firstname
,'My friend '+f.firstname+' owns a ' + STRING_AGG(CAST(o.thing AS nvarchar(max)), ' and a ') as what_I_say_to_friends
,'My friend '+f.firstname+' owns a ' + STRING_AGG(o.thing + ' priced ' + convert(nvarchar(max), o.price), ' and a ') as what_I_say_to_finance
from
friend f
group by f.firstname
甚至这种方式,基于纯 XQuery。
SQL
-- DDL and sample data population, start
DECLARE @owns TABLE (firstname nvarchar(50), thing nvarchar(50), val float);
INSERT @owns VALUES
('mike','Tesla',50000),
('mike','Mona Lisa',3000000000);
-- DDL and sample data population, end
DECLARE @separator VARCHAR(10) = ' and a ';
WITH rs AS
(
SELECT firstname
, (
SELECT *
FROM @owns AS c
WHERE c.firstname = p.firstname
FOR XML PATH('r'), TYPE, ROOT('root')
) AS xmldata
FROM @owns AS p
GROUP BY p.firstname
)
SELECT *
, 'My friend ' + firstname + ' owns a ' + xmldata.query('
for $x in /root/r
return if ($x is (/root/r[position() = last()])[1]) then string($x/thing[1])
else concat(($x/thing/text())[1], sql:variable("@separator"))
').value('text()[1]', 'VARCHAR(MAX)') AS what_I_say_to_friends
, 'My friend ' + firstname + ' owns a ' + xmldata.query('
for $x in /root/r
let $token := concat(string($x/thing[1]), " priced ", string($x/val[1]))
return if ($x is (/root/r[position() = last()])[1]) then $token
else concat($token, sql:variable("@separator"))
').value('text()[1]', 'VARCHAR(MAX)') AS what_I_say_to_finance
FROM rs;