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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/6899/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-31 23:05:53  来源:igfitidea点击:

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

sqlsql-serverstring-concatenation

提问by Templar

To illustrate, assume that I have two tables as follows:

为了说明,假设我有两个表,如下所示:

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

I want to write a query to return the following results:

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

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

I know that this can be done using server side cursors, ie:

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

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

However, as you can see, this requires a great deal of code. What I would like is a generic function that would allow me to do something like this:

但是,如您所见,这需要大量代码。我想要的是一个通用函数,它可以让我做这样的事情:

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

Is this possible? Or something similar?

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

回答by Mun

If you're using SQL Server 2005, you could use the FOR XML PATH command.

如果您使用的是 SQL Server 2005,则可以使用 FOR XML PATH 命令。

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]

It's a lot easier than using a cursor, and seems to work fairly well.

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

回答by Mike Powell

Note that Matt's codewill result in an extra comma at the end of the string; using COALESCE (or ISNULL for that matter) as shown in the link in Lance's post uses a similar method but doesn't leave you with an extra comma to remove. For the sake of completeness, here's the relevant code from Lance's link on sqlteam.com:

请注意,Matt 的代码将导致在字符串末尾多出一个逗号;如 Lance 的帖子中的链接所示,使用 COALESCE(或 ISNULL)使用类似的方法,但不会给您留下一个额外的逗号来删除。为了完整起见,这里是 sqlteam.com 上 Lance 链接中的相关代码:

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

回答by Matt Hamilton

I don't belive there's a way to do it within one query, but you can play tricks like this with a temporary variable:

我不相信有一种方法可以在一个查询中做到这一点,但是您可以使用临时变量来玩这样的技巧:

declare @s varchar(max)
set @s = ''
select @s = @s + City + ',' from Locations

select @s

It's definitely less code than walking over a cursor, and probably more efficient.

这绝对比在游标上走动更少的代码,而且可能更有效。

回答by ZunTzu

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

在单个 SQL 查询中,不使用 FOR XML 子句。
公共表表达式用于递归连接结果。

-- 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

回答by John B

From what I can see FOR XML(as posted earlier) is the only way to do it if you want to also select other columns (which I'd guess most would) as the OP does. Using COALESCE(@var...does not allow inclusion of other columns.

从我所看到的FOR XML(如之前发布的)是唯一的方法,如果您还想像 OP 那样选择其他列(我猜大多数人会这样做)。使用COALESCE(@var...不允许包含其他列。

Update: Thanks to programmingsolutions.netthere is a way to remove the "trailing" comma to. By making it into a leading comma and using the STUFFfunction of MSSQL you can replace the first character (leading comma) with an empty string as below:

更新:感谢programmingsolutions.net,有一种方法可以删除“尾随”逗号。通过将其设为前导逗号并使用STUFFMSSQL的功能,您可以将第一个字符(前导逗号)替换为空字符串,如下所示:

stuff(
    (select ',' + Column 
     from Table
         inner where inner.Id = outer.Id 
     for xml path('')
), 1,1,'') as Values

回答by Steven Chong

In SQL Server 2005

SQL Server 2005 中

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


In SQL Server 2016

在 SQL Server 2016 中

you can use the FOR JSON syntax

您可以使用FOR JSON 语法

i.e.

IE

SELECT per.ID,
Emails = JSON_VALUE(
   REPLACE(
     (SELECT _ = em.Email FROM Email em WHERE em.Person = per.ID FOR JSON PATH)
    ,'"},{"_":"',', '),'$[0]._'
) 
FROM Person per

And the result will become

结果会变成

Id  Emails
1   [email protected]
2   NULL
3   [email protected], [email protected]

This will work even your data contains invalid XML characters

即使您的数据包含无效的 XML 字符,这也会起作用

the '"},{"":"' is safe because if you data contain '"},{"":"', it will be escaped to "},{\"_\":\"

'"},{" ":"' 是安全的,因为如果您的数据包含 '"},{"":"',它将被转义为 "},{\"_\":\"

You can replace ', ' with any string separator

您可以用任何字符串分隔符替换 ', '



And in SQL Server 2017, Azure SQL Database

在 SQL Server 2017 中,Azure SQL 数据库

You can use the new STRING_AGG function

您可以使用新的STRING_AGG 函数

回答by Binoj Antony

The below code will work for Sql Server 2000/2005/2008

以下代码适用于 Sql Server 2000/2005/2008

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

回答by Gil

I've found a solution by creating the following function:

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

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

Usage:

用法:

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

回答by nurseymybush

Mun's answer didn't work for me so I made some changes to that answer to get it to work. Hope this helps someone. Using SQL Server 2012:

Mun 的回答对我不起作用,因此我对该答案进行了一些更改以使其正常工作。希望这可以帮助某人。使用 SQL Server 2012:

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

回答by Mike Barlow - BarDev

With the other answers, the person reading the answer must be aware of the vehicle table and create the vehicle table and data to test a solution.

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

Below is an example that uses SQL Server "Information_Schema.Columns" table. By using this solution, no tables need to be created or data added. This example creates a comma separated list of column names for all tables in the database.

下面是一个使用 SQL Server“Information_Schema.Columns”表的示例。通过使用此解决方案,无需创建表或添加数据。本示例为数据库中的所有表创建一个逗号分隔的列名列表。

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