Stuff和“ For Xml Path”在SQL Server中的工作方式


367

表是:

+----+------+
| Id | Name |
+----+------+    
| 1  | aaa  |
| 1  | bbb  |
| 1  | ccc  |
| 1  | ddd  |
| 1  | eee  |
+----+------+

要求的输出:

+----+---------------------+
| Id |        abc          |
+----+---------------------+ 
|  1 | aaa,bbb,ccc,ddd,eee |
+----+---------------------+

查询:

SELECT ID, 
    abc = STUFF(
                 (SELECT ',' + name FROM temp1 FOR XML PATH ('')), 1, 1, ''
               ) 
FROM temp1 GROUP BY id

此查询工作正常。但是我只需要解释一下它是如何工作的,或者有其他或简短的方法可以做到这一点。

我对此感到非常困惑。



1
我为此制作了一个SqlFiddle页面,以查看它在现实生活中的工作。希望它能帮助别人。
Sabuncu

1
^ ID在不同实体的不同表中,也许是唯一的,并且此表存储着属于它们的事物。
尼克·罗兰多

如果某些行具有不同的ID,则此查询不起作用。例如,如果'DDD'和'EEE'所具有的ID 2
KevinVictor

10
是我每月访问此页面的时间,以查看我出了什么问题。
泰勒·阿克莱

Answers:


683

下面是它的工作原理:

1.使用FOR XML获取XML元素字符串

将FOR XML PATH添加到查询的末尾,可以将查询的结果作为XML元素输出,并且PATH参数中包含元素名称。例如,如果我们要运行以下语句:

SELECT ',' + name 
              FROM temp1
              FOR XML PATH ('')

通过传入一个空白字符串(FOR XML PATH('')),我们得到以下内容:

,aaa,bbb,ccc,ddd,eee

2.使用STUFF删除逗号开头

STUFF语句实际上是将一个字符串“塞”入另一个字符串,替换第一个字符串中的字符,但是,我们只是在使用它来删除结果值列表的第一个字符。

SELECT abc = STUFF((
            SELECT ',' + NAME
            FROM temp1
            FOR XML PATH('')
            ), 1, 1, '')
FROM temp1

的参数STUFF是:

  • 要“填充”的字符串(在本例中为名称的完整列表,并带有前导逗号)
  • 开始删除和插入字符的位置(1,我们正在填充空白字符串)
  • 要删除的字符数(1,即前导逗号)

因此,我们最终得到:

aaa,bbb,ccc,ddd,eee

3.加入ID以获取完整列表

接下来,我们将其加入到临时表的ID列表中,以获取具有名称的ID列表:

SELECT ID,  abc = STUFF(
             (SELECT ',' + name 
              FROM temp1 t1
              WHERE t1.id = t2.id
              FOR XML PATH (''))
             , 1, 1, '') from temp1 t2
group by id;

我们得到了结果:

-----------------------------------
| Id        | Name                |
|---------------------------------|
| 1         | aaa,bbb,ccc,ddd,eee |
-----------------------------------

希望这可以帮助!


57
您应该为Microsoft的文档团队工作(如果有的话)
Fandango68年

55
@ Fandango68,@ FutbolFan-他不能为Microsoft的文档团队工作。他的解释太清楚太直接了。;-)
克里斯(Chris)

1
@ChrisProsser我同意。通过LISTAGG在Oracle 11gR2中引入功能,Oracle 在这方面领先于Microsoft 。在确实不得不使用它的日子里,我确实错过了该功能。techonthenet.com/oracle/functions/listagg.php
FutbolFan

2
你好。在步骤1中,如果您这样做:从temp1 FOR XML PATH('')中选择名称... ...得到<name> aaa </ name> <name> bbb </ name> ... etc ...我没有首先要意识到这一点...将其更改为SELECT''+ name ... etc ...会删除标签。
KevinVictor '18

1
@ChrisProsser-Sybase ASA已有list数十年的功能。不幸的是,微软改为基于Sybase的ASE建立了SQLServer,直到去年才开始使用列表功能。我同意-这令人难以置信。然后,他们这样做,他们叫它string_agg。我以为list很明显。
youcantryreachingme

75

本文介绍了各种在SQL中串联字符串的方法,包括对代码进行改进的版本,该版本不对连接的值进行XML编码。

SELECT ID, abc = STUFF
(
    (
        SELECT ',' + name
        FROM temp1 As T2
        -- You only want to combine rows for a single ID here:
        WHERE T2.ID = T1.ID
        ORDER BY name
        FOR XML PATH (''), TYPE
    ).value('.', 'varchar(max)')
, 1, 1, '')
FROM temp1 As T1
GROUP BY id

要了解发生了什么,请从内部查询开始:

SELECT ',' + name
FROM temp1 As T2
WHERE T2.ID = 42 -- Pick a random ID from the table
ORDER BY name
FOR XML PATH (''), TYPE

因为您正在指定FOR XML,所以您将获得一行,其中包含代表所有行的XML片段。

由于您尚未为第一列指定列别名,因此每一行都将包装在XML元素中,其名称在后面的方括号中指定FOR XML PATH。例如,如果您有FOR XML PATH ('X'),您将获得一个看起来像这样的XML文档:

<X>,aaa</X>
<X>,bbb</X>
...

但是,由于您尚未指定元素名称,因此只获取值列表:

,aaa,bbb,...

.value('.', 'varchar(max)')简单地检索从所得XML片段的值,而无需编码XML的任何“特殊”字符。现在,您将看到一个类似于以下内容的字符串:

',aaa,bbb,...'

STUFF然后,该函数删除前导逗号,为您提供最终结果,如下所示:

'aaa,bbb,...'

乍一看似乎很令人困惑,但是与其他一些选项相比,它确实确实表现不错。


2
查询中Type的用途是什么。我认为这是为了定义,XML路径的结果将存储在值中(如果出错,请不确定解释)。
Puneet Chawla

8
@PuneetChawla:TYPE指令告诉SQL使用返回数据xml类型。没有它,数据将以形式返回nvarchar(max)。如果该name列中有特殊字符,则在此处用于避免XML编码问题。
理查德·迪明

2
@barlop:如SimpleTalk文章所述,如果您删除TYPE.value('.', 'varchar(max)'),则可以在结果中得到XML编码的实体。
理查德·迪明

1
@RichardDeeming是指数据是否包含或可能包含尖括号?
barlop

1
但是,由于您尚未指定元素名称,因此只获得了一个值列表,这就是我所缺少的见解。谢谢。
亚当

44

PATH模式用于从SELECT查询生成XML

1. SELECT   
       ID,  
       Name  
FROM temp1
FOR XML PATH;  

Ouput:
<row>
<ID>1</ID>
<Name>aaa</Name>
</row>

<row>
<ID>1</ID>
<Name>bbb</Name>
</row>

<row>
<ID>1</ID>
<Name>ccc</Name>
</row>

<row>
<ID>1</ID>
<Name>ddd</Name>
</row>

<row>
<ID>1</ID>
<Name>eee</Name>
</row>

输出是以元素为中心的XML,其中结果行集中的每个列值都包装在行元素中。因为SELECT子句没有为列名指定任何别名,所以生成的子元素名称与SELECT子句中的对应列名相同。

对于行集中的每一行,都会添加一个标签。

2.
SELECT   
       ID,  
       Name  
FROM temp1
FOR XML PATH('');

Ouput:
<ID>1</ID>
<Name>aaa</Name>
<ID>1</ID>
<Name>bbb</Name>
<ID>1</ID>
<Name>ccc</Name>
<ID>1</ID>
<Name>ddd</Name>
<ID>1</ID>
<Name>eee</Name>

对于步骤2:如果指定长度为零的字符串,则不产生包装元素。

3. 

    SELECT   

           Name  
    FROM temp1
    FOR XML PATH('');

    Ouput:
    <Name>aaa</Name>
    <Name>bbb</Name>
    <Name>ccc</Name>
    <Name>ddd</Name>
    <Name>eee</Name>

4. SELECT   
        ',' +Name  
FROM temp1
FOR XML PATH('')

Ouput:
,aaa,bbb,ccc,ddd,eee

在步骤4中,我们将这些值串联起来。

5. SELECT ID,
    abc = (SELECT   
            ',' +Name  
    FROM temp1
    FOR XML PATH('') )
FROM temp1

Ouput:
1   ,aaa,bbb,ccc,ddd,eee
1   ,aaa,bbb,ccc,ddd,eee
1   ,aaa,bbb,ccc,ddd,eee
1   ,aaa,bbb,ccc,ddd,eee
1   ,aaa,bbb,ccc,ddd,eee


6. SELECT ID,
    abc = (SELECT   
            ',' +Name  
    FROM temp1
    FOR XML PATH('') )
FROM temp1 GROUP by iD

Ouput:
ID  abc
1   ,aaa,bbb,ccc,ddd,eee

在步骤6中,我们将日期按ID分组。

STUFF(source_string,start,length,add_string)参数或参数source_string要修改的源字符串。start在source_string中的位置以删除长度字符,然后插入add_string。length要从source_string中删除的字符数。add_string在起始位置插入到source_string中的字符序列。

SELECT ID,
    abc = 
    STUFF (
        (SELECT   
                ',' +Name  
        FROM temp1
        FOR XML PATH('')), 1, 1, ''
    )
FROM temp1 GROUP by iD

Output:
-----------------------------------
| Id        | Name                |
|---------------------------------|
| 1         | aaa,bbb,ccc,ddd,eee |
-----------------------------------

1
您输入“在第4步中,我们将这些值连接起来。” 但尚不清楚为什么/如何将','指定为as的列与('')后xml路径相结合,导致发生串联
barlop

在步骤4中,执行任何字符串操作都将使用指定的包装元素,在这种情况下,该包装元素为空白('')。
vCillusion

1
对于想知道第4点以及为什么<Name>消失的任何人。这是因为在用逗号连接Name之后,不再有列,而只有值,因此SQL Server不知道应该为xml标记使用什么名称。例如,此查询SELECT 'a' FROM some_table FOR XML PATH('')将产生:'aaaaaaa'。但是,如果将指定列名:SELECT 'a' AS Col FROM some_table FOR XML PATH('')您将得到结果:<Col>a</Col><Col>a</Col><Col>a</Col>
anth

23

Azure SQL数据库和SQL Server(从2017年开始)中有非常新的功能来处理此确切方案。我相信这将成为您尝试使用XML / STUFF方法完成的本机官方方法。例:

select id, STRING_AGG(name, ',') as abc
from temp1
group by id

STRING_AGG- https: //msdn.microsoft.com/zh-CN/library/mt790580.aspx

编辑:当我最初发布此内容时,我提到了SQL Server 2016,因为我认为我在要包含的潜在功能上看到了这一点。我或者记错了或者更改了某些内容,感谢建议的修复版本的编辑。另外,我印象深刻,并且没有完全意识到将我吸引到最终选择权的多步骤审核过程。


3
STRING_AGG不在SQL Server 2016中。据说它在“ vNext”中。
N8allan

糟糕,我的意思不是要覆盖@lostmylogin的编辑,对此感到抱歉……那是谁真正推动了更正编辑。
布赖恩·乔登

5

在中for xml path,如果我们定义任何类似的值,[ for xml path('ENVLOPE') ]则这些标记将与每一行一起添加:

<ENVLOPE>
</ENVLOPE>

2
SELECT ID, 
    abc = STUFF(
                 (SELECT ',' + name FROM temp1 FOR XML PATH ('')), 1, 1, ''
               ) 
FROM temp1 GROUP BY id

在上面的查询中,这里的STUFF函数仅用于(,)从生成的xml字符串中删除第一个逗号,(,aaa,bbb,ccc,ddd,eee)然后它将变为(aaa,bbb,ccc,ddd,eee)

FOR XML PATH('')简单地将列数据转换为(,aaa,bbb,ccc,ddd,eee)字符串,但是在PATH中我们传递了'',因此它不会创建XML标签。

最后,我们使用ID列对记录进行了分组。


2

我进行了调试,最后以正常方式将“填充”查询返回给它。

只是

select * from myTable for xml path('myTable')

给我表的内容,以便从我调试的触发器写入日志表。


1
Declare @Temp As Table (Id Int,Name Varchar(100))
Insert Into @Temp values(1,'A'),(1,'B'),(1,'C'),(2,'D'),(2,'E'),(3,'F'),(3,'G'),(3,'H'),(4,'I'),(5,'J'),(5,'K')
Select X.ID,
stuff((Select ','+ Z.Name from @Temp Z Where X.Id =Z.Id For XML Path('')),1,1,'')
from @Temp X
Group by X.ID

-1

STUFF((从表T中选择SELECT','+ CAST(T.ID),其中T.ID = 1 FOR XML PATH('')),1,1,'')AS名称


-3

我经常使用where子句

SELECT 
TapuAda=STUFF(( 
SELECT ','+TBL.TapuAda FROM (
SELECT TapuAda FROM T_GayrimenkulDetay AS GD 
INNER JOIN dbo.T_AktiviteGayrimenkul AS AG ON  D.GayrimenkulID=AG.GayrimenkulID WHERE 
AG.AktiviteID=262
) AS TBL FOR XML PATH ('')
),1,1,'')

2
我看不出这是什么答案,请您解释一下。
Gar
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.