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;