Como criar uma função SQL Server para “juntar” múltiplas linhas de uma subconsulta em um único campo delimitado?[duplicado]

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

Pergunta

Para ilustrar, suponha que eu tenha duas tabelas como segue:

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

Quero escrever uma consulta para retornar os seguintes resultados:

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

Eu sei que isso pode ser feito usando cursores do lado do servidor, ou seja:

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

No entanto, como você pode ver, isso requer uma grande quantidade de código.O que eu gostaria é de uma função genérica que me permitisse fazer algo assim:

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

Isso é possível?Ou algo semelhante?

Foi útil?

Solução

Se estiver usando o SQL Server 2005, você poderá usar o 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]

É muito mais fácil do que usar um cursor e parece funcionar bastante bem.

Outras dicas

Observe que Código de Matt resultará em uma vírgula extra no final da string;usar COALESCE (ou ISNULL), conforme mostrado no link na postagem de Lance, usa um método semelhante, mas não deixa uma vírgula extra para remover.Para completar, aqui está o código relevante do link de Lance em sqlteam.com:

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

Não acredito que haja uma maneira de fazer isso em uma consulta, mas você pode fazer truques como este com uma variável temporária:

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

select @s

Definitivamente, é menos código do que passar sobre o cursor e provavelmente mais eficiente.

Em uma única consulta SQL, sem utilizar a cláusula FOR XML.
Uma Expressão de Tabela Comum é usada para concatenar recursivamente os 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

Pelo que posso ver FOR XML (conforme postado anteriormente) é a única maneira de fazer isso se você quiser selecionar também outras colunas (o que eu acho que a maioria faria) como o OP faz.Usando COALESCE(@var... não permite a inclusão de outras colunas.

Atualizar:Graças a www.programmingsolutions.net existe uma maneira de remover a vírgula "final" para.Transformando-o em uma vírgula inicial e usando o STUFF função do MSSQL você pode substituir o primeiro caractere (vírgula inicial) por uma string vazia como abaixo:

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

Em Servidor SQL 2005

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

No SQL Server 2016

você pode usar o Sintaxe FOR JSON

ou seja

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

E o resultado será

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

Isso funcionará mesmo que seus dados contenham caracteres XML inválidos

o '"},{"":"' é seguro porque se seus dados contiverem '"},{"":"', será escapado para "},{\"_\":\"

Você pode substituir ',' por qualquer separador de string


E no SQL Server 2017, Banco de Dados SQL do Azure

Você pode usar o novo Função STRING_AGG

O código abaixo 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

Encontrei uma solução criando a seguinte função:

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 DA VERSÃO:Você deve usar o SQL Server 2005 ou superior com nível de compatibilidade definido como 90 ou superior para esta solução.

Veja isso Artigo MSDN para o primeiro exemplo de criação de uma função agregada definida pelo usuário que concatena um conjunto de valores de string obtidos de uma coluna em uma tabela.

Minha humilde recomendação seria omitir a vírgula anexada para que você possa usar seu próprio delimitador ad-hoc, se houver.

Referindo-se à versão C# do Exemplo 1:

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

E

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

Dessa forma, ao usar seu agregado personalizado, você pode optar por usar seu próprio delimitador ou nenhum, como:

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

OBSERVAÇÃO: Tenha cuidado com a quantidade de dados que você tenta processar em seu agregado.Se você tentar concatenar milhares de linhas ou muitos tipos de dados muito grandes, poderá receber um erro do .NET Framework informando "[o] buffer é insuficiente".

Com as demais respostas, quem lê a resposta deve conhecer a tabela de veículos e criar a tabela de veículos e os dados para testar uma solução.

Abaixo está um exemplo que usa a tabela "Information_Schema.Columns" do SQL Server.Ao usar esta solução, nenhuma tabela precisa ser criada ou adicionado dados.Este exemplo cria uma lista separada por vírgulas de nomes de colunas para todas as tabelas do banco de dados.

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 

A resposta de Mun não funcionou para mim, então fiz algumas alterações nessa resposta para que funcionasse.Espero que isso ajude alguém.Usando o 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]

Tente 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

Se você estiver executando o SQL Server 2005, poderá escrever um função agregada CLR personalizada para lidar com isso.

Versão 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 em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top