Come creare una funzione SQL Server per "unire" più righe da una sottoquery in un singolo campo delimitato?[duplicare]

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

Domanda

Per illustrare, supponiamo di avere due tabelle come 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

Voglio scrivere una query per restituire i seguenti risultati:

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

So che questo può essere fatto utilizzando i cursori lato server, ovvero:

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

Tuttavia, come puoi vedere, ciò richiede una grande quantità di codice.Quello che vorrei è una funzione generica che mi permetta di fare qualcosa del genere:

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

È possibile?O qualcosa di simile?

È stato utile?

Soluzione

Se utilizzi SQL Server 2005, puoi utilizzare il 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]

È molto più semplice che usare un cursore e sembra funzionare abbastanza bene.

Altri suggerimenti

Notare che Il codice di Matt risulterà in una virgola in più alla fine della stringa;l'uso di COALESCE (o ISNULL del resto) come mostrato nel collegamento nel post di Lance utilizza un metodo simile ma non ti lascia con una virgola in più da rimuovere.Per completezza, ecco il codice pertinente dal collegamento di Lance su sqlteam.com:

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

Non credo che ci sia un modo per farlo all'interno di una query, ma puoi giocare trucchi come questo con una variabile temporanea:

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

select @s

È decisamente meno codice che passare sopra un cursore e probabilmente più efficiente.

In una singola query SQL, senza utilizzare la clausola FOR XML.
Un'espressione di tabella comune viene utilizzata per concatenare ricorsivamente i risultati.

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

Da quello che posso vedere FOR XML (come pubblicato in precedenza) è l'unico modo per farlo se si desidera selezionare anche altre colonne (cosa che immagino la maggior parte farebbe) come fa l'OP.Utilizzando COALESCE(@var... non consente l'inclusione di altre colonne.

Aggiornamento:Grazie a programmingsolutions.net c'è un modo per rimuovere la virgola "finale" in.Trasformandolo in una virgola iniziale e utilizzando il STUFF funzione di MSSQL è possibile sostituire il primo carattere (virgola iniziale) con una stringa vuota come di seguito:

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

In SQLServer2005

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

Nell'SQL Server 2016

puoi usare il FOR sintassi JSON

cioè.

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 il risultato diventerà

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

Funzionerà anche se i tuoi dati contengono caratteri XML non validi

IL '"},{"":"' è sicuro perché se i tuoi dati contengono '"},{"":"', verrà scritto in "},{\"_\":\"

È possibile sostituire ', ' con qualsiasi separatore di stringa


E in SQL Server 2017, il database SQL di Azure

Puoi usare il nuovo Funzione STRING_AGG

Il codice seguente funzionerà per 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

Ho trovato una soluzione creando la seguente funzione:

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

Utilizzo:

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

NOTA SULLA VERSIONE:Per questa soluzione è necessario utilizzare SQL Server 2005 o versione successiva con il livello di compatibilità impostato su 90 o versione successiva.

Guarda questo Articolo MSDN per il primo esempio di creazione di una funzione di aggregazione definita dall'utente che concatena un insieme di valori stringa presi da una colonna in una tabella.

La mia umile raccomandazione sarebbe di tralasciare la virgola aggiunta in modo da poter utilizzare il proprio delimitatore ad hoc, se presente.

Facendo riferimento alla versione C# dell'esempio 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();

In questo modo, quando utilizzi il tuo aggregato personalizzato, puoi scegliere di utilizzare il tuo delimitatore o di non utilizzarlo affatto, ad esempio:

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

NOTA: Fai attenzione alla quantità di dati che tenti di elaborare nel tuo insieme.Se provi a concatenare migliaia di righe o molti tipi di dati molto grandi, potresti ricevere un errore .NET Framework che indica "[il] buffer è insufficiente".

Con le altre risposte, la persona che legge la risposta deve essere a conoscenza della tabella del veicolo e creare la tabella del veicolo e i dati per testare una soluzione.

Di seguito è riportato un esempio che utilizza la tabella "Information_Schema.Columns" di SQL Server.Utilizzando questa soluzione non è necessario creare tabelle o aggiungere dati.In questo esempio viene creato un elenco separato da virgole di nomi di colonne per tutte le tabelle nel database.

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 risposta di Mun non ha funzionato per me, quindi ho apportato alcune modifiche a quella risposta per farla funzionare.Spero che questo aiuti qualcuno.Utilizzando 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]

Prova questa query

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 stai utilizzando SQL Server 2005, puoi scrivere un file funzione di aggregazione CLR personalizzata per gestire questa cosa.

Versione 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());
    }
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top