Sqlserver
 sql >> Database >  >> RDS >> Sqlserver

Concatenazione di gruppi in SQL Server

La concatenazione di gruppi è un problema comune in SQL Server, senza funzionalità dirette e intenzionali per supportarlo (come XMLAGG in Oracle, STRING_AGG o ARRAY_TO_STRING(ARRAY_AGG()) in PostgreSQL e GROUP_CONCAT in MySQL). È stato richiesto, ma non ha ancora avuto successo, come evidenziato in questi elementi Connect:

  • Connetti #247118:SQL richiede la versione della funzione MySQL group_Concat (posticipata)
  • Connect #728969 :Funzioni di set ordinati – Clausola ALL'INTERNO DEL GRUPPO (chiusa perché non si risolve)

** AGGIORNAMENTO gennaio 2017 ** :STRING_AGG() sarà in SQL Server 2017; leggilo qui, qui e qui.

Cos'è la concatenazione raggruppata?

Per chi non lo sapesse, la concatenazione raggruppata è quando si desidera prendere più righe di dati e comprimerle in un'unica stringa (di solito con delimitatori come virgole, tabulazioni o spazi). Alcuni potrebbero chiamarlo "unione orizzontale". Un rapido esempio visivo che mostra come comprimere un elenco di animali domestici appartenenti a ciascun membro della famiglia, dalla fonte normalizzata all'output "appiattito":

Ci sono stati molti modi per risolvere questo problema nel corso degli anni; eccone solo alcuni, basati sui seguenti dati di esempio:

    CREATE TABLE dbo.FamilyMemberPets
    (
      Name SYSNAME,
      Pet SYSNAME,
      PRIMARY KEY(Name,Pet)
    );
     
    INSERT dbo.FamilyMemberPets(Name,Pet) VALUES
    (N'Madeline',N'Kirby'),
    (N'Madeline',N'Quigley'),
    (N'Henry',   N'Piglet'),
    (N'Lisa',    N'Snowball'),
    (N'Lisa',    N'Snowball II');

    Non dimostrerò un elenco esaustivo di ogni approccio di concatenazione di gruppo mai concepito, poiché voglio concentrarmi su alcuni aspetti del mio approccio consigliato, ma voglio sottolineare alcuni di quelli più comuni:

    UDF scalare
    CREATE FUNCTION dbo.ConcatFunction
    (
      @Name SYSNAME
    )
    RETURNS NVARCHAR(MAX)
    WITH SCHEMABINDING 
    AS 
    BEGIN
      DECLARE @s NVARCHAR(MAX);
     
      SELECT @s = COALESCE(@s + N', ', N'') + Pet
        FROM dbo.FamilyMemberPets
    	WHERE Name = @Name
    	ORDER BY Pet;
     
      RETURN (@s);
    END
    GO
     
    SELECT Name, Pets = dbo.ConcatFunction(Name)
      FROM dbo.FamilyMemberPets
      GROUP BY Name
      ORDER BY Name;

    Nota:c'è un motivo per cui non lo facciamo:

    SELECT DISTINCT Name, Pets = dbo.ConcatFunction(Name)
      FROM dbo.FamilyMemberPets
      ORDER BY Name;

    Con DISTINCT , la funzione viene eseguita per ogni singola riga, quindi i duplicati vengono rimossi; con GROUP BY , i duplicati vengono rimossi per primi.

    Common Language Runtime (CLR)

    Questo utilizza il GROUP_CONCAT_S funzione trovata su http://groupconcat.codeplex.com/:

    SELECT Name, Pets = dbo.GROUP_CONCAT_S(Pet, 1)
      FROM dbo.FamilyMemberPets
      GROUP BY Name
      ORDER BY Name;
    CTE ricorsivo

    Ci sono diverse variazioni su questa ricorsione; questo tira fuori una serie di nomi distinti come l'ancora:

    ;WITH x as 
    (
      SELECT Name, Pet = CONVERT(NVARCHAR(MAX), Pet),
        r1 = ROW_NUMBER() OVER (PARTITION BY Name ORDER BY Pet)
      FROM dbo.FamilyMemberPets
    ),
    a AS 
    (
      SELECT Name, Pet, r1 FROM x WHERE r1 = 1
    ),
    r AS
    (
      SELECT Name, Pet, r1 FROM a WHERE r1 = 1
      UNION ALL
      SELECT x.Name, r.Pet + N', ' + x.Pet, x.r1
        FROM x INNER JOIN r
    	ON r.Name = x.Name
    	AND x.r1 = r.r1 + 1
    )
    SELECT Name, Pets = MAX(Pet)
      FROM r
      GROUP BY Name 
      ORDER BY Name
      OPTION (MAXRECURSION 0);
    Cursore

    Non c'è molto da dire qui; i cursori di solito non sono l'approccio ottimale, ma questa potrebbe essere la tua unica scelta se sei bloccato su SQL Server 2000:

    DECLARE @t TABLE(Name SYSNAME, Pets NVARCHAR(MAX),
      PRIMARY KEY (Name));
     
    INSERT @t(Name, Pets)
      SELECT Name, N'' 
      FROM dbo.FamilyMemberPets GROUP BY Name;
     
    DECLARE @name SYSNAME, @pet SYSNAME, @pets NVARCHAR(MAX);
     
    DECLARE c CURSOR LOCAL FAST_FORWARD
      FOR SELECT Name, Pet 
      FROM dbo.FamilyMemberPets
      ORDER BY Name, Pet;
     
    OPEN c;
     
    FETCH c INTO @name, @pet;
     
    WHILE @@FETCH_STATUS = 0
    BEGIN
      UPDATE @t SET Pets += N', ' + @pet
        WHERE Name = @name;
     
      FETCH c INTO @name, @pet;
    END
     
    CLOSE c; DEALLOCATE c;
     
    SELECT Name, Pets = STUFF(Pets, 1, 1, N'') 
      FROM @t
      ORDER BY Name;
    GO
    Aggiornamento stravagante

    Alcune persone *adorano* questo approccio; Non capisco affatto l'attrazione.

    DECLARE @Name SYSNAME, @Pets NVARCHAR(MAX);
     
    DECLARE @t TABLE(Name SYSNAME, Pet SYSNAME, Pets NVARCHAR(MAX),
      PRIMARY KEY (Name, Pet));
     
    INSERT @t(Name, Pet)
      SELECT Name, Pet FROM dbo.FamilyMemberPets
      ORDER BY Name, Pet;
     
    UPDATE @t SET @Pets = Pets = COALESCE(
        CASE COALESCE(@Name, N'') 
          WHEN Name THEN @Pets + N', ' + Pet
          ELSE Pet END, N''), 
    	@Name = Name;
     
    SELECT Name, Pets = MAX(Pets)
      FROM @t
      GROUP BY Name
      ORDER BY Name;
    PER PERCORSO XML

    Abbastanza facilmente il mio metodo preferito, almeno in parte perché è l'unico modo per *garantire* l'ordine senza usare un cursore o CLR. Detto questo, questa è una versione molto grezza che non risolve un paio di altri problemi intrinseci di cui parlerò più avanti:

    SELECT Name, Pets = STUFF((SELECT N', ' + Pet 
      FROM dbo.FamilyMemberPets AS p2
       WHERE p2.name = p.name 
       ORDER BY Pet
       FOR XML PATH(N'')), 1, 2, N'')
    FROM dbo.FamilyMemberPets AS p
    GROUP BY Name
    ORDER BY Name;

Ho visto molte persone presumere erroneamente che il nuovo CONCAT() la funzione introdotta in SQL Server 2012 è stata la risposta a queste richieste di funzionalità. Quella funzione ha lo scopo di operare solo su colonne o variabili in una singola riga; non può essere utilizzato per concatenare valori tra righe.

Ulteriori informazioni su FOR XML PATH

FOR XML PATH('') di per sé non è abbastanza buono:ha problemi noti con l'entitizzazione XML. Ad esempio, se aggiorni uno dei nomi degli animali domestici per includere una parentesi HTML o una e commerciale:

UPDATE dbo.FamilyMemberPets
  SET Pet = N'Qui>gle&y'
  WHERE Pet = N'Quigley';

Questi vengono tradotti in entità sicure per XML da qualche parte lungo il percorso:

Qui>gle&y

Quindi uso sempre PATH, TYPE).value() , come segue:

SELECT Name, Pets = STUFF((SELECT N', ' + Pet 
  FROM dbo.FamilyMemberPets AS p2
   WHERE p2.name = p.name 
   ORDER BY Pet
   FOR XML PATH(N''), TYPE).value(N'.[1]', N'nvarchar(max)'), 1, 2, N'')
FROM dbo.FamilyMemberPets AS p
GROUP BY Name
ORDER BY Name;

Inoltre uso sempre NVARCHAR , perché non sai mai quando una colonna sottostante conterrà Unicode (o in seguito verrà modificata per farlo).

Potresti vedere le seguenti varietà all'interno di .value() , o anche altri:

... TYPE).value(N'.', ...
... TYPE).value(N'(./text())[1]', ...

Questi sono intercambiabili, tutti in definitiva rappresentano la stessa stringa; le differenze di prestazioni tra di loro (più sotto) erano trascurabili e forse del tutto non deterministiche.

Un altro problema che potresti incontrare sono alcuni caratteri ASCII che non è possibile rappresentare in XML; ad esempio, se la stringa contiene il carattere 0x001A (CHAR(26) ), riceverai questo messaggio di errore:

Msg 6841, livello 16, stato 1, riga 51
FOR XML non ha potuto serializzare i dati per il nodo 'NoName' perché contiene un carattere (0x001A) che non è consentito in XML. Per recuperare questi dati utilizzando FOR XML, convertirli in tipo di dati binary, varbinary o image e utilizzare la direttiva BINARY BASE64.

Questo mi sembra piuttosto complicato, ma spero che tu non debba preoccupartene perché non stai archiviando dati come questo o almeno non stai cercando di usarli in concatenazione di gruppi. Se lo sei, potresti dover ricorrere a uno degli altri approcci.

Prestazioni

I dati di esempio sopra riportati rendono facile dimostrare che tutti questi metodi fanno ciò che ci aspettiamo, ma è difficile confrontarli in modo significativo. Quindi ho popolato la tabella con un set molto più grande:

TRUNCATE TABLE dbo.FamilyMemberPets;
 
INSERT dbo.FamilyMemberPets(Name,Pet)
  SELECT o.name, c.name
  FROM sys.all_objects AS o
  INNER JOIN sys.all_columns AS c
  ON o.[object_id] = c.[object_id]
  ORDER BY o.name, c.name;

Per me, si trattava di 575 oggetti, con 7.080 righe totali; l'oggetto più largo aveva 142 colonne. Ora, ancora una volta, è vero, non ho deciso di confrontare ogni singolo approccio concepito nella storia di SQL Server; solo i pochi punti salienti che ho postato sopra. Ecco i risultati:

Potresti notare un paio di contendenti mancanti; l'UDF utilizzando DISTINCT e il CTE ricorsivo erano così fuori scala da distorcere la scala. Ecco i risultati di tutti e sette gli approcci in forma tabellare:

Approccio Durata
(millisecondi)
PER PERCORSO XML 108,58
CLR 80.67
Aggiornamento stravagante 278,83
UDF (GRUPPO PER) 452,67
UDF (DISTINTO) 5.893,67
Cursore 2.210,83
CTE ricorsiva 70.240,58

Durata media, in millisecondi, per tutti gli approcci

Si noti inoltre che le variazioni su FOR XML PATH sono stati testati in modo indipendente ma hanno mostrato differenze molto minori, quindi li ho semplicemente combinati per la media. Se vuoi davvero saperlo, il .[1] la notazione ha funzionato più velocemente nei miei test; YMMV.

Conclusione

Se non sei in un negozio in cui CLR è un ostacolo in alcun modo, e soprattutto se non hai a che fare solo con nomi semplici o altre stringhe, dovresti assolutamente prendere in considerazione il progetto CodePlex. Non provare a reinventare la ruota, non provare trucchi e hack non intuitivi per fare CROSS APPLY o altri costrutti funzionano solo un po' più velocemente degli approcci non CLR sopra. Prendi solo ciò che funziona e collegalo. E diamine, dal momento che ottieni anche il codice sorgente, puoi migliorarlo o estenderlo se lo desideri.

Se CLR è un problema, allora FOR XML PATH è probabilmente la tua migliore opzione, ma dovrai comunque fare attenzione ai personaggi difficili. Se sei bloccato su SQL Server 2000, la tua unica opzione fattibile è l'UDF (o codice simile non racchiuso in un UDF).

La prossima volta

Un paio di cose che voglio esplorare in un post successivo:rimuovere i duplicati dall'elenco, ordinare l'elenco in base a qualcosa di diverso dal valore stesso, casi in cui inserire uno di questi approcci in un UDF può essere doloroso e casi d'uso pratici per questa funzionalità.