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那样选择其他列(我猜大多数情况下都是这样),那么
更新:由于ProgrammingSolutions.net,有一种方法可以删除"尾随"逗号。通过将其设为前导逗号并使用mssql的
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;&;lt;&;gt;,如果您确定[City]没有这些特殊字符,则可以安全地删除它。——张士骏</li><li>+ 1。这个答案被低估了。您应该编辑它以提到这是唯一不会转义特殊字符(如&;<>等)的答案之一。此外,如果我们使用:<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()); } } |