如何创建SQL Server函数以将子查询中的多行“连接”到单个分隔字段?

如何创建SQL Server函数以将子查询中的多行“连接”到单个分隔字段?

How to create a SQL Server function to “join” multiple rows from a subquery into a single delimited field?

本问题已经有最佳答案,请猛点这里访问。

举例来说,假设我有两个表,如下所示:

1
2
3
4
5
6
7
8
9
10
VehicleID Name
1         Chuck
2         Larry

LocationID VehicleID City
1          1         NEW York
2          1         Seattle
3          1         Vancouver
4          2         Los Angeles
5          2         Houston

我想编写一个查询来返回以下结果:

1
2
3
VehicleID Name    Locations
1         Chuck   NEW York, Seattle, Vancouver
2         Larry   Los Angeles, Houston

我知道这可以使用服务器端光标来完成,即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
DECLARE @VehicleID INT
DECLARE @VehicleName VARCHAR(100)
DECLARE @LocationCity VARCHAR(100)
DECLARE @Locations VARCHAR(4000)
DECLARE @Results TABLE
(
  VehicleID INT
  Name VARCHAR(100)
  Locations VARCHAR(4000)
)

DECLARE VehiclesCursor CURSOR FOR
SELECT
  [VehicleID]
, [Name]
FROM [Vehicles]

OPEN VehiclesCursor

FETCH NEXT FROM VehiclesCursor INTO
  @VehicleID
, @VehicleName
WHILE @@FETCH_STATUS = 0
BEGIN

  SET @Locations = ''

  DECLARE LocationsCursor CURSOR FOR
  SELECT
    [City]
  FROM [Locations]
  WHERE [VehicleID] = @VehicleID

  OPEN LocationsCursor

  FETCH NEXT FROM LocationsCursor INTO
    @LocationCity
  WHILE @@FETCH_STATUS = 0
  BEGIN
    SET @Locations = @Locations + @LocationCity

    FETCH NEXT FROM LocationsCursor INTO
      @LocationCity
  END
  CLOSE LocationsCursor
  DEALLOCATE LocationsCursor

  INSERT INTO @Results (VehicleID, Name, Locations) SELECT @VehicleID, @Name, @Locations

END    
CLOSE VehiclesCursor
DEALLOCATE VehiclesCursor

SELECT * FROM @Results

但是,正如您所看到的,这需要大量的代码。我想要的是一个通用函数,它允许我执行如下操作:

1
2
3
4
SELECT VehicleID
     , Name
     , JOIN(SELECT City FROM Locations WHERE VehicleID = Vehicles.VehicleID, ', ') AS Locations
FROM Vehicles

这有可能吗?或者类似的东西?


如果您使用的是SQL Server 2005,那么可以使用for xml path命令。

1
2
3
4
5
6
7
SELECT [VehicleID]
     , [Name]
     , (STUFF((SELECT CAST(', ' + [City] AS VARCHAR(MAX))
         FROM [Location]
         WHERE (VehicleID = Vehicle.VehicleID)
         FOR XML PATH ('')), 1, 2, '')) AS Locations
FROM [Vehicle]

它比使用光标容易得多,而且似乎工作得相当好。


请注意,Matt的代码将在字符串末尾产生一个额外的逗号;使用coalesce(或isnull),如Lance文章中的链接所示,使用类似的方法,但不会给您留下额外的逗号来删除。为了完整起见,下面是来自sqlteam.com上Lance链接的相关代码:

1
2
3
4
5
DECLARE @EmployeeList VARCHAR(100)
SELECT @EmployeeList = COALESCE(@EmployeeList + ', ', '') +
    CAST(EmpUniqueID AS VARCHAR(5))
FROM SalesCallsEmployees
WHERE SalCal_UniqueID = 1


我不认为有一种方法可以在一个查询中完成这项工作,但是您可以用一个临时变量来玩这样的把戏:

1
2
3
4
5
DECLARE @s VARCHAR(MAX)
SET @s = ''
SELECT @s = @s + City + ',' FROM Locations

SELECT @s

它的代码绝对比浏览光标少,而且可能更高效。


In a single SQL query, without using the FOR XML clause.
A Common Table Expression is used to recursively concatenate the results.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
-- rank locations by incrementing lexicographical order
WITH RankedLocations AS (
  SELECT
    VehicleID,
    City,
    ROW_NUMBER() OVER (
        PARTITION BY VehicleID
        ORDER BY City
    ) Rank
  FROM
    Locations
),
-- concatenate locations using a recursive query
-- (Common Table Expression)
Concatenations AS (
  -- for each vehicle, select the first location
  SELECT
    VehicleID,
    CONVERT(nvarchar(MAX), City) Cities,
    Rank
  FROM
    RankedLocations
  WHERE
    Rank = 1

  -- then incrementally concatenate with the next location
  -- this will return intermediate concatenations that will be
  -- filtered out later on
  UNION ALL

  SELECT
    c.VehicleID,
    (c.Cities + ', ' + l.City) Cities,
    l.Rank
  FROM
    Concatenations c -- this is a recursion!
    INNER JOIN RankedLocations l ON
        l.VehicleID = c.VehicleID
        AND l.Rank = c.Rank + 1
),
-- rank concatenation results by decrementing length
-- (rank 1 will always be for the longest concatenation)
RankedConcatenations AS (
  SELECT
    VehicleID,
    Cities,
    ROW_NUMBER() OVER (
        PARTITION BY VehicleID
        ORDER BY Rank DESC
    ) Rank
  FROM
    Concatenations
)
-- main query
SELECT
  v.VehicleID,
  v.Name,
  c.Cities
FROM
  Vehicles v
  INNER JOIN RankedConcatenations c ON
    c.VehicleID = v.VehicleID
    AND c.Rank = 1

从我所看到的来看,如果您想像OP那样选择其他列(我猜大多数情况下都是这样),那么FOR XML是唯一的方法。使用COALESCE(@var...不允许包含其他列。

更新:由于ProgrammingSolutions.net,有一种方法可以删除"尾随"逗号。通过将其设为前导逗号并使用mssql的STUFF函数,您可以将第一个字符(前导逗号)替换为空字符串,如下所示:

1
2
3
4
5
6
stuff(
    (SELECT ',' + COLUMN
     FROM TABLE
         INNER WHERE INNER.Id = OUTER.Id
     FOR xml path('')
), 1,1,'') AS VALUES

在SQL Server 2005中

1
2
3
SELECT Stuff(
  (SELECT N', ' + Name FROM Names FOR XML PATH(''),TYPE)
  .value('text()[1]','nvarchar(max)'),1,2,N'')

在SQL Server 2016中

可以使用for json语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SELECT per.ID,
Emails = JSON_VALUE(
   REPLACE(
     (SELECT _ = em.Email FROM Email em WHERE em.Person = per.ID FOR JSON PATH)
    ,'
<div class="suo-content">[collapse title=""]<ul><li>我拿出了这一块:<wyn>TYPE ).value('
text()[1]', 'nvarchar(MAX)')</wyn>,它仍然很管用…不知道该怎么办。</li><li>假设要解码XML,如果[City]具有类似字符的&amp;<>,则输出将变为&amp;amp;&amp;lt;&amp;gt;,如果您确定[City]没有这些特殊字符,则可以安全地删除它。——张士骏</li><li>+ 1。这个答案被低估了。您应该编辑它以提到这是唯一不会转义特殊字符(如&amp;<>等)的答案之一。此外,如果我们使用:<wyn>.value('.', 'nvarchar(MAX)')</wyn>,结果会不会相同?</li><li>嗨,宝达,结果是一样的,但是我测试的时候,使用"text()[1]"而不是"."时性能更好,没有什么区别。</li></ul>[/collapse]</div><p><center>[wp_ad_camp_2]</center></p><hr><P>以下代码适用于SQL Server 2000/2005/2008</P>[cc lang="sql"]CREATE FUNCTION fnConcatVehicleCities(@VehicleId SMALLINT)
RETURNS VARCHAR(1000) AS
BEGIN
  DECLARE @csvCities VARCHAR(1000)
  SELECT @csvCities = COALESCE(@csvCities + '
, ', '') + COALESCE(City,'')
  FROM Vehicles
  WHERE VehicleId = @VehicleId
  return @csvCities
END

-- //Once the User defined function is created then run the below sql

SELECT VehicleID
     , dbo.fnConcatVehicleCities(VehicleId) AS Locations
FROM Vehicles
GROUP BY VehicleID

我通过创建以下函数找到了一个解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CREATE FUNCTION [dbo].[JoinTexts]
(
  @delimiter VARCHAR(20) ,
  @whereClause VARCHAR(1)
)
RETURNS VARCHAR(MAX)
AS
BEGIN
    DECLARE @Texts VARCHAR(MAX)

    SELECT  @Texts = COALESCE(@Texts + @delimiter, '') + T.Texto
    FROM    SomeTable AS T
    WHERE   T.SomeOtherColumn = @whereClause

    RETURN @Texts
END
GO

用途:

1
SELECT dbo.JoinTexts(' , ', 'Y')

孟的回答对我不起作用,所以我对那个答案做了一些修改,让它起作用。希望这能帮助别人。使用SQL Server 2012:

1
2
3
4
5
6
7
SELECT [VehicleID]
     , [Name]
     , STUFF((SELECT DISTINCT ',' + CONVERT(VARCHAR,City)
         FROM [Location]
         WHERE (VehicleID = Vehicle.VehicleID)
         FOR XML PATH ('')), 1, 2, '') AS Locations
FROM [Vehicle]

对于其他答案,阅读答案的人必须了解车辆表,并创建车辆表和数据以测试解决方案。

下面是一个使用SQL Server"Information_schema.columns"表的示例。通过使用此解决方案,不需要创建表或添加数据。此示例为数据库中的所有表创建一个以逗号分隔的列名列表。

1
2
3
4
5
6
7
8
9
10
11
SELECT
    TABLE_NAME
    ,STUFF((
        SELECT ',' + Column_Name
        FROM INFORMATION_SCHEMA.Columns COLUMNS
        WHERE TABLES.Table_Name = COLUMNS.Table_Name
        ORDER BY Column_Name
        FOR XML PATH ('')), 1, 1, ''
    )COLUMNS
FROM INFORMATION_SCHEMA.Columns TABLES
GROUP BY TABLE_NAME

版本说明:对于此解决方案,必须使用兼容级别设置为90或更高的SQL Server 2005或更高版本。

有关创建用户定义聚合函数的第一个示例,该函数连接从表中的列中获取的一组字符串值,请参阅此msdn文章。

我谦虚的建议是去掉附加的逗号,这样您就可以使用自己的特别分隔符(如果有的话)。

参考实施例1的C版本:

1
2
CHANGE:  this.intermediateResult.Append(VALUE.Value).Append(',');
    TO:  this.intermediateResult.Append(VALUE.Value);

1
2
CHANGE:  output = this.intermediateResult.ToString(0, this.intermediateResult.Length - 1);
    TO:  output = this.intermediateResult.ToString();

这样,当使用自定义聚合时,您可以选择使用自己的分隔符,或者完全不使用分隔符,例如:

1
SELECT dbo.CONCATENATE(column1 + '|') FROM table1

注意:请注意您试图在聚合中处理的数据量。如果尝试连接数千行或许多非常大的数据类型,则可能会出现.NET框架错误,说明"缓冲区不足"。


尝试这个查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SELECT v.VehicleId, v.Name, ll.LocationList
FROM Vehicles v
LEFT JOIN
    (SELECT
     DISTINCT
        VehicleId,
        REPLACE(
            REPLACE(
                REPLACE(
                    (
                        SELECT City AS c
                        FROM Locations x
                        WHERE x.VehicleID = l.VehicleID FOR XML PATH('')
                    ),    
                    '</c><c>',', '
                 ),
             '<c>',''
            ),
        '</c>', ''
        ) AS LocationList
    FROM Locations l
) ll ON ll.VehicleId = v.VehicleId

如果您运行的是SQL Server2005,那么可以编写一个自定义的clr聚合函数来处理这个问题。

C版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
USING System;
USING System.Data;
USING System.Data.SqlClient;
USING System.Data.SqlTypes;
USING System.Text;
USING Microsoft.SqlServer.Server;
[Serializable]
[Microsoft.SqlServer.Server.SqlUserDefinedAggregate(Format.UserDefined,MaxByteSize=8000)]
public class CSV:IBinarySerialize
{
    private StringBuilder RESULT;
    public void Init() {
        this.Result = NEW StringBuilder();
    }

    public void Accumulate(SqlString VALUE) {
        IF (VALUE.IsNull) RETURN;
        this.Result.Append(VALUE.Value).Append(",");
    }
    public void MERGE(CSV GROUP) {
        this.Result.Append(GROUP.Result);
    }
    public SqlString Terminate() {
        RETURN NEW SqlString(this.Result.ToString());
    }
    public void READ(System.IO.BinaryReader r) {
        this.Result = NEW StringBuilder(r.ReadString());
    }
    public void WRITE(System.IO.BinaryWriter w) {
        w.Write(this.Result.ToString());
    }
}


推荐阅读

    excel怎么用乘法函数

    excel怎么用乘法函数,乘法,函数,哪个,excel乘法函数怎么用?1、首先用鼠标选中要计算的单元格。2、然后选中单元格后点击左上方工具栏的fx公

    苹果电脑如何连接和使用扫描仪

    苹果电脑如何连接和使用扫描仪,,扫描仪的使用 用苹果扫描图像。 连接扫描仪:在打开苹果电脑之前,你需要连接扫描仪和安装扫描仪驱动程序。扫

    excel中乘法函数是什么?

    excel中乘法函数是什么?,乘法,函数,什么,打开表格,在C1单元格中输入“=A1*B1”乘法公式。以此类推到多个单元。1、A1*B1=C1的Excel乘法公式

    标准差excel用什么函数?

    标准差excel用什么函数?,函数,标准,什么,在数据单元格的下方输入l标准差公式函数公式“=STDEVPA(C2:C6)”。按下回车,求出标准公差值。详细

    1394连接是什么1394网络适配器知识

    1394连接是什么1394网络适配器知识,,今天有网友在QQ群中问了这样一个问题:1394连接是什么?。由于笔者对1394连接不清楚,通过百度搜索与谷歌

    如何创建宽带连接(图形)

    如何创建宽带连接(图形),,很多时候,由于计算机的使用不当,计算机网络连接遭到破坏。此时,我们需要自己创建宽带连接。下面我们将教你如何创建宽