¿Cómo crear una función de SQL Server para "unir" varias filas de una subconsulta en un único campo delimitado?[duplicar]

StackOverflow https://stackoverflow.com/questions/6899

Pregunta

Para ilustrar, supongamos que tengo dos tablas como sigue:

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

Quiero escribir una consulta para devolver los siguientes resultados:

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

Sé que esto se puede hacer usando cursores del lado del servidor, es decir:

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

Sin embargo, como puedes ver, esto requiere una gran cantidad de código.Lo que me gustaría es una función genérica que me permitiera hacer algo como esto:

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

es posible?¿O algo similar?

¿Fue útil?

Solución

Si está utilizando SQL Server 2005, puede utilizar el comando 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]

Es mucho más fácil que usar un cursor y parece funcionar bastante bien.

Otros consejos

Tenga en cuenta que El código de Matt. dará como resultado una coma adicional al final de la cadena;usar COALESCE (o ISNULL para el caso) como se muestra en el enlace en la publicación de Lance usa un método similar pero no te deja con una coma adicional para eliminar.En aras de la integridad, aquí está el código relevante del enlace de Lance en sqlteam.com:

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

No creo que haya una manera de hacerlo dentro de una consulta, pero puedes hacer trucos como este con una variable temporal:

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

select @s

Definitivamente es menos código que caminar sobre un cursor y probablemente más eficiente.

En una única consulta SQL, sin utilizar la cláusula FOR XML.
Se utiliza una expresión de tabla común para concatenar recursivamente los resultados.

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

Por lo que puedo ver FOR XML (como se publicó anteriormente) es la única forma de hacerlo si también desea seleccionar otras columnas (que supongo que la mayoría lo haría) como lo hace el OP.Usando COALESCE(@var... no permite la inclusión de otras columnas.

Actualizar:Gracias a solucionesdeprogramacion.net hay una manera de eliminar la coma "final".Convirtiéndolo en una coma inicial y usando el STUFF función de MSSQL puede reemplazar el primer carácter (coma inicial) con una cadena vacía como se muestra a continuación:

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

En Servidor SQL 2005

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

En SQL Server 2016

puedes usar el PARA la sintaxis JSON

es decir.

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

Y el resultado será

Id  Emails
1   abc@gmail.com
2   NULL
3   def@gmail.com, xyz@gmail.com

Esto funcionará incluso si sus datos contienen caracteres XML no válidos

el '"},{"":"' es seguro porque si sus datos contienen '"},{"":"', se escapará a "},{\"_\":\"

Puedes reemplazar ', ' con cualquier separador de cadena


Y en SQL Server 2017, Azure SQL Database

Puedes usar el nuevo Función STRING_AGG

El siguiente código funcionará para 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

Encontré una solución creando la siguiente función:

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

Uso:

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

NOTA DE VERSIÓN:Debe utilizar SQL Server 2005 o superior con el nivel de compatibilidad establecido en 90 o superior para esta solución.

Mira esto artículo de MSDN para ver el primer ejemplo de creación de una función agregada definida por el usuario que concatena un conjunto de valores de cadena tomados de una columna de una tabla.

Mi humilde recomendación sería omitir la coma adjunta para que pueda usar su propio delimitador ad hoc, si lo hubiera.

Haciendo referencia a la versión C# del Ejemplo 1:

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

Y

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

De esa manera, cuando utilice su agregado personalizado, puede optar por utilizar su propio delimitador, o ninguno, como por ejemplo:

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

NOTA: Tenga cuidado con la cantidad de datos que intenta procesar en su conjunto.Si intenta concatenar miles de filas o muchos tipos de datos muy grandes, es posible que obtenga un error de .NET Framework que indique "el búfer es insuficiente".

Con las otras respuestas, la persona que lee la respuesta debe conocer la tabla de vehículos y crear la tabla de vehículos y los datos para probar una solución.

A continuación se muestra un ejemplo que utiliza la tabla "Information_Schema.Columns" de SQL Server.Al utilizar esta solución, no es necesario crear tablas ni agregar datos.Este ejemplo crea una lista separada por comas de nombres de columnas para todas las tablas de la base de datos.

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 

La respuesta de Mun no funcionó para mí, así que hice algunos cambios en esa respuesta para que funcionara.Espero que esto ayude a alguien.Usando 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]

Prueba esta consulta

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

Si está ejecutando SQL Server 2005, puede escribir un función agregada CLR personalizada para manejar esto.

Versión 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());
    }
}
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top