为了说明这一点,假设我有如下两个表:
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]
这比使用光标容易得多,而且似乎工作得相当好。
更新
对于那些在新版本的SQL Server中仍然使用这种方法的人来说,还有另一种方法,它更简单,性能更好
自SQL Server 2017以来已经可用的STRING_AGG方法。
SELECT [VehicleID]
,[Name]
,(SELECT STRING_AGG([City], ', ')
FROM [Location]
WHERE VehicleID = V.VehicleID) AS Locations
FROM [Vehicle] V
这还允许将不同的分隔符指定为第二个参数,比前一种方法提供了更多的灵活性。
版本注意:此解决方案必须使用SQL Server 2005或更高版本,并将兼容性级别设置为90或更高版本。
请参阅这篇MSDN文章,了解创建用户定义聚合函数的第一个示例,该函数连接从表中的列获取的一组字符串值。
我的建议是去掉附加的逗号,这样您就可以使用自己的特殊分隔符(如果有的话)。
参考示例1的c#版本:
change: this.intermediateResult.Append(value.Value).Append(',');
to: this.intermediateResult.Append(value.Value);
And
change: output = this.intermediateResult.ToString(0, this.intermediateResult.Length - 1);
to: output = this.intermediateResult.ToString();
这样,当你使用你的自定义聚合时,你可以选择使用你自己的分隔符,或者根本不使用,例如:
SELECT dbo.CONCATENATE(column1 + '|') from table1
注意:要注意您试图在聚合中处理的数据量。如果你试图连接数千行或许多非常大的数据类型,你可能会得到一个。net Framework错误,说明“[t]他缓冲区不足。”
SQL Server 2005
SELECT Stuff(
(SELECT N', ' + Name FROM Names FOR XML PATH(''),TYPE)
.value('text()[1]','nvarchar(max)'),1,2,N'')
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
结果就会变成
Id Emails
1 abc@gmail.com
2 NULL
3 def@gmail.com, xyz@gmail.com
即使数据包含无效的XML字符,这也可以工作
的 '"},{"":"' 你是安全的,因为如果数据包含吗 '"},{"":"', 它将逃到 "},{\"_\":\"
你可以用任何字符串分隔符替换','
在SQL Server 2017, Azure SQL数据库
您可以使用新的STRING_AGG函数
在单个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