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

Tem*_*lar 193 sql sql-server string-concatenation

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

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
Run Code Online (Sandbox Code Playgroud)

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

VehicleID Name    Locations
1         Chuck   New York, Seattle, Vancouver
2         Larry   Los Angeles, Houston
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

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

SELECT VehicleID
     , Name
     , JOIN(SELECT City FROM Locations WHERE VehicleID = Vehicles.VehicleID, ', ') AS Locations
FROM Vehicles
Run Code Online (Sandbox Code Playgroud)

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

Mun*_*Mun 253

如果您使用的是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]
Run Code Online (Sandbox Code Playgroud)

它比使用游标容易得多,并且似乎运行得相当好.

  • 这将适用于此数据,但如果您的数据可能包含xml特殊字符(例如<,>,&),则它们将被替换(<等等) (13认同)
  • @James您可以使用CTE来完成此任务:WITH MyCTE(VehicleId,Name,Locations)AS(SELECT [VehicleID],[Name],(SELECT CAST(City +','AS VARCHAR(MAX))FROM [Location] WHERE(VehicleID = Vehicle.VehicleID)FOR XML PATH(''))AS位置FROM [Vehicle])SELECT VehicleId,Name,REPLACE(Locations,',',CHAR(10))AS位置来自MyCTE (4认同)

Mik*_*ell 86

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

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

  • 在我看来,没有额外的逗号,这很好,但也更容易阅读和理解,而不是加入的解决方案.非常感谢! (7认同)
  • 这不是[可靠的解决方案](https://www.simple-talk.com/sql/t-sql-programming/concatenating-row-values-in-transact-sql/#_Toc205129492). (4认同)
  • 只要你不关心订单,@ lukasLansky就可靠 (4认同)

Mat*_*ton 45

我不相信有一种方法可以在一个查询中完成它,但你可以用一个临时变量来玩这样的技巧:

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

select @s
Run Code Online (Sandbox Code Playgroud)

它肯定比走过光标更少的代码,可能更有效.

  • 我相当肯定你可以把"可能"排在最后一行. (11认同)
  • 它不可靠,取决于执行计划,行可能会丢失.请参阅[KB](https://connect.microsoft.com/SQLServer/feedback/details/345947/n-varchar-building-from-resultset-fails-when-order-by-is-added). (2认同)
  • 这项技术或功能叫做什么?当一个`SELECT @s = @s`变量赋值包含它的现有值,并为结果集中的每一行再次赋值? (2认同)

Joh*_*n B 23

从我所看到的FOR XML(如前面所述)是唯一的方法,如果你想像OP一样选择其他列(我猜的最多).使用COALESCE(@var...不允许包含其他列.

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

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


Zun*_*Tzu 23

在单个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
Run Code Online (Sandbox Code Playgroud)

  • 谢谢你.这是此问题的少数解决方案之一,它不使用变量,函数,FOR XML子句或CLR代码.这意味着我能够调整您的解决方案以解决[TSQL初学者挑战4 - 从多行连接值](http://beyondrelational.com/blogs/tcb/archive/2010/03/29/TSQL-Beginners-Challenge- 4-级联值从 - 多rows.aspx). (4认同)
  • @PeonProgrammer不,它对于大型结果集非常糟糕,并且很可能会给出错误,"在语句完成之前,最大递归100已经用完了." (你可以通过在结尾指定`OPTION(MAXRECURSION 0)`来解决这个问题,但是你的查询可能只需要永远运行. (3认同)
  • 这是否比其他解决方案具有性能优势? (2认同)

Ste*_*ong 17

SQL Server 2005中

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

在SQL Server 2016中

您可以使用FOR JSON语法

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
Run Code Online (Sandbox Code Playgroud)

结果将成为

Id  Emails
1   abc@gmail.com
2   NULL
3   def@gmail.com, xyz@gmail.com
Run Code Online (Sandbox Code Playgroud)

这甚至会使您的数据包含无效的XML字符

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

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


在SQL Server 2017中,Azure SQL数据库

您可以使用新的STRING_AGG功能

  • 应该解码xml,如果[City]有像&<>的字符,输出将变为&amp; &LT; &GT; ,如果你确定[City]没有那些特殊的字符,那么删除它是安全的. - 史蒂文冲 (5认同)
  • +1.这个答案被低估了.你应该编辑它,提到这是唯一不会逃避特殊字符的答案之一,如&<>等.如果我们使用的话,结果也不会相同:`.value('.','nvarchar (最大)")`? (2认同)

Bin*_*ony 14

以下代码适用于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
Run Code Online (Sandbox Code Playgroud)

  • 你尝试过Varchar(最大)? (3认同)

Gil*_*Gil 7

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

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
Run Code Online (Sandbox Code Playgroud)

用法:

SELECT dbo.JoinTexts(' , ', 'Y')
Run Code Online (Sandbox Code Playgroud)

  • 很好的解决方案,因为可读性优于其他答案 +1 (2认同)

归档时间:

查看次数:

128633 次

最近记录:

7 年,3 月 前