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

时间:2020-03-05 18:38:47  来源:igfitidea点击:

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

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

这可能吗?还是类似的东西?

解决方案

回答

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

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

select @s

它绝对比在游标上移动更少的代码,并且可能更有效。

回答

如果使用的是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的代码将在字符串末尾产生一个逗号。如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

回答

如果运行的是SQL Server 2005,则可以编写自定义CLR聚合函数来处理此问题。

转换:

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

回答

下面的代码将适用于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或者更高版本,并且"兼容级别"设置为90或者更高。

请参阅此MSDN文章,以获取创建用户定义的聚合函数的第一个示例,该函数将从表中的列获取的一组字符串值连接在一起。

我谦虚的建议是忽略添加的逗号,以便我们可以使用自己的临时定界符(如果有)。

参考示例1的Cversion:

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 dbo.CONCATENATE(column1 + '|') from table1

注意:请小心我们尝试处理的聚合中的数据量。如果尝试连接成千上万的行或者许多非常大的数据类型,则可能会收到.NET Framework错误,指出"缓冲区不足。"

回答

从我可以看到的" FOR XML"(如前所述)中,如果我们还希望像OP一样选择其他列(我想大多数人会这样做),则是这样做的唯一方法。
使用COALESCE(@var ...不允许包含其他列。

更新:
多亏了programmingsolutions.net,有一种方法可以删除"尾随"逗号。
通过将其设置为前导逗号并使用MSSQL的" STUFF"功能,我们可以将第一个字符(前导逗号)替换为空字符串,如下所示:

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

回答

在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语法

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

结果将成为

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

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

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

我们可以将","替换为任何字符串分隔符

在SQL Server 2017中,Azure SQL数据库

我们可以使用新的STRING_AGG函数