Como criar uma função SQL Server para “juntar” múltiplas linhas de uma subconsulta em um único campo delimitado?[duplicado]
-
08-06-2019 - |
Pergunta
Essa pergunta já tem resposta aqui:
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?
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());
}
}