如何创build一个SQL Server函数来将多个行从一个子查询连接到单个分隔的字段?

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

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 

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

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

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

 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 

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

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

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

如果您使用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] 

这比使用游标要容易得多,而且似乎工作得很好。

请注意, Matt的代码会在string的末尾添加一个额外的逗号。 使用COALESCE(或ISNULL为这件事)如在兰斯的post中的链接所示使用类似的方法,但不留给你一个额外的逗号删除。 为了完整起见,以下是来自sqlteam.com上Lance链接的相关代码:

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

我不相信有一种方法可以在一个查询中完成,但是您可以使用临时variables来玩这种技巧:

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

这肯定比在游标上行走的代码要less,而且效率可能更高。

在单个SQL查询中,不使用FOR XML子句。
公用表expression式用于recursion地连接结果。

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

从我所能看到的FOR XML (如之前发布的)是唯一的方法来做到这一点,如果你想也select其他列(我想最想的)作为OP。 使用COALESCE(@var...不允许包含其他列。

更新:由于programmingsolutions.net有一种方法来删除“尾”逗号。 通过将它变成一个逗号并使用MSSQL的STUFF函数,可以用空stringreplace第一个字符(前导逗号),如下所示:

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

下面的代码将适用于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 

在SQL Server 2005+中:

 SELECT [VehicleID] , [Name] , [Locations] = Isnull( Stuff( ( SELECT N', ' + [City] FROM [Locations] WHERE VehicleID = a.VehicleID FOR XML PATH(''),TYPE ).value('text()[1]', 'nvarchar(max)') , 1, 2, N''), N'') FROM [Vehicle] a 

我已经通过创build以下functionfind了解决scheme:

 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 

用法:

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

版本注意:对于此解决scheme,您必须使用兼容性级别设置为90或更高的SQL Server 2005或更高版本。

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

我不起眼的build议是省略附加的逗号,所以你可以使用自己的临时分隔符(如果有的话)。

参考例1的C#版本:

 change: this.intermediateResult.Append(value.Value).Append(','); to: this.intermediateResult.Append(value.Value); 

 change: output = this.intermediateResult.ToString(0, this.intermediateResult.Length - 1); to: output = this.intermediateResult.ToString(); 

这样,当你使用自定义的聚合,你可以select使用自己的分隔符,或根本没有,例如:

 SELECT dbo.CONCATENATE(column1 + '|') from table1 

注意:请注意您尝试在您的聚合中处理的数据量。 如果您尝试连接成千上万行或者很多非常大的数据types,则可能会出现.NET Framework错误,指出“缓冲区不足”。

有了其他答案,阅读答案的人必须了解车辆表并创build车辆表和数据以testing解决scheme。

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

 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 

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] 

如果你正在运行Sql Server 2005,你可以写一个自定义的聚合函数来处理这个。

C#版本:

 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()); } } 

试试这个查询

 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