web-dev-qa-db-fra.com

Manière optimale de concaténer / d'agréger des chaînes

Je trouve un moyen d'agréger des chaînes de différentes lignes en une seule ligne. Je cherche à faire cela dans de nombreux endroits, alors avoir une fonction pour faciliter cela serait Nice. J'ai essayé des solutions en utilisant COALESCE et FOR XML, mais ils ne me la coupent pas.

L'agrégation de chaînes ferait quelque chose comme ceci:

id | Name                    Result: id | Names
-- - ----                            -- - -----
1  | Matt                            1  | Matt, Rocks
1  | Rocks                           2  | Stylus
2  | Stylus

J'ai jeté un oeil à fonctions d'agrégat définies par CLR en remplacement de COALESCE et FOR XML, mais apparemment SQL Azure ne le fait pas prend en charge les éléments définis par le CLR, ce qui est pénible pour moi car savoir que je pourrais l'utiliser résoudrait beaucoup de problèmes pour moi.

Existe-t-il une solution de contournement possible ou une méthode tout aussi optimale (qui pourrait ne pas être aussi optimale que CLR, mais je prendrai ce que je peux obtenir) que je peux utiliser pour agréger mes données?

89
matt

SOLUTION

La définition de optimal peut varier, mais voici comment concaténer des chaînes de différentes lignes à l'aide de Transact SQL standard, ce qui devrait fonctionner correctement dans Azure.

;WITH Partitioned AS
(
    SELECT 
        ID,
        Name,
        ROW_NUMBER() OVER (PARTITION BY ID ORDER BY Name) AS NameNumber,
        COUNT(*) OVER (PARTITION BY ID) AS NameCount
    FROM dbo.SourceTable
),
Concatenated AS
(
    SELECT 
        ID, 
        CAST(Name AS nvarchar) AS FullName, 
        Name, 
        NameNumber, 
        NameCount 
    FROM Partitioned 
    WHERE NameNumber = 1

    UNION ALL

    SELECT 
        P.ID, 
        CAST(C.FullName + ', ' + P.Name AS nvarchar), 
        P.Name, 
        P.NameNumber, 
        P.NameCount
    FROM Partitioned AS P
        INNER JOIN Concatenated AS C 
                ON P.ID = C.ID 
                AND P.NameNumber = C.NameNumber + 1
)
SELECT 
    ID,
    FullName
FROM Concatenated
WHERE NameNumber = NameCount

EXPLICATION

L'approche se résume à trois étapes:

  1. Numérotez les lignes en utilisant OVER et PARTITION en les regroupant et en les classant selon les besoins pour la concaténation. Le résultat est Partitioned CTE. Nous conservons le nombre de lignes dans chaque partition pour filtrer les résultats ultérieurement.

  2. À l'aide de CTE récursif (Concatenated), parcourez les numéros de ligne (NameNumber colonne) en ajoutant Name valeurs à FullName colonne.

  3. Filtrez tous les résultats sauf ceux avec le plus haut NameNumber.

N'oubliez pas que pour rendre cette requête prévisible, vous devez définir à la fois le regroupement (par exemple, dans votre scénario, les lignes avec le même ID sont concaténées) et le tri (j'ai supposé que vous triiez simplement la chaîne par ordre alphabétique). avant la concaténation).

J'ai rapidement testé la solution sur SQL Server 2012 avec les données suivantes:

INSERT dbo.SourceTable (ID, Name)
VALUES 
(1, 'Matt'),
(1, 'Rocks'),
(2, 'Stylus'),
(3, 'Foo'),
(3, 'Bar'),
(3, 'Baz')

Le résultat de la requête:

ID          FullName
----------- ------------------------------
2           Stylus
3           Bar, Baz, Foo
1           Matt, Rocks
62
Serge Belov

Les méthodes utilisant FOR XML PATH, comme ci-dessous, sont-elles vraiment si lentes? Itzik Ben-Gan écrit que cette méthode présente de bonnes performances dans son livre d'interrogation T-SQL (M. Ben-Gan est une source digne de confiance, à mon avis).

create table #t (id int, name varchar(20))

insert into #t
values (1, 'Matt'), (1, 'Rocks'), (2, 'Stylus')

select  id
        ,Names = stuff((select ', ' + name as [text()]
        from #t xt
        where xt.id = t.id
        for xml path('')), 1, 2, '')
from #t t
group by id
44
slachterman

Pour ceux d'entre nous qui ont trouvé cela et n'utilisez pas la base de données SQL Azure:

STRING_AGG() dans PostgreSQL, SQL Server 2017 et Azure SQL
https://www.postgresql.org/docs/current/static/functions-aggregate.html
https://docs.Microsoft.com/en-us/sql/t-sql/functions/string-agg-transact-sql

GROUP_CONCAT() dans MySQL
http://dev.mysql.com/doc/refman/5.7/en/group-by-functions.html#function_group-concat

(Merci à @Brianjorden et @milanio pour la mise à jour Azure)

Exemple de code:

select Id
, STRING_AGG(Name, ', ') Names 
from Demo
group by Id

Violon SQL: http://sqlfiddle.com/#!18/89251/1

27
Hrobky

Bien que @ réponse soit correcte, j'ai comparé la consommation de temps de son chemin avec xmlpath et j'ai trouvé que xmlpath était tellement plus rapide. Je vais écrire le code de comparaison et vous pouvez le vérifier vous-même. Ceci est la façon @serge:

DECLARE @startTime datetime2;
DECLARE @endTime datetime2;
DECLARE @counter INT;
SET @counter = 1;

set nocount on;

declare @YourTable table (ID int, Name nvarchar(50))

WHILE @counter < 1000
BEGIN
    insert into @YourTable VALUES (ROUND(@counter/10,0), CONVERT(NVARCHAR(50), @counter) + 'CC')
    SET @counter = @counter + 1;
END

SET @startTime = GETDATE()

;WITH Partitioned AS
(
    SELECT 
        ID,
        Name,
        ROW_NUMBER() OVER (PARTITION BY ID ORDER BY Name) AS NameNumber,
        COUNT(*) OVER (PARTITION BY ID) AS NameCount
    FROM @YourTable
),
Concatenated AS
(
    SELECT ID, CAST(Name AS nvarchar) AS FullName, Name, NameNumber, NameCount FROM Partitioned WHERE NameNumber = 1

    UNION ALL

    SELECT 
        P.ID, CAST(C.FullName + ', ' + P.Name AS nvarchar), P.Name, P.NameNumber, P.NameCount
    FROM Partitioned AS P
        INNER JOIN Concatenated AS C ON P.ID = C.ID AND P.NameNumber = C.NameNumber + 1
)
SELECT 
    ID,
    FullName
FROM Concatenated
WHERE NameNumber = NameCount

SET @endTime = GETDATE();

SELECT DATEDIFF(millisecond,@startTime, @endTime)
--Take about 54 milliseconds

Et c'est comme ça xmlpath:

DECLARE @startTime datetime2;
DECLARE @endTime datetime2;
DECLARE @counter INT;
SET @counter = 1;

set nocount on;

declare @YourTable table (RowID int, HeaderValue int, ChildValue varchar(5))

WHILE @counter < 1000
BEGIN
    insert into @YourTable VALUES (@counter, ROUND(@counter/10,0), CONVERT(NVARCHAR(50), @counter) + 'CC')
    SET @counter = @counter + 1;
END

SET @startTime = GETDATE();

set nocount off
SELECT
    t1.HeaderValue
        ,STUFF(
                   (SELECT
                        ', ' + t2.ChildValue
                        FROM @YourTable t2
                        WHERE t1.HeaderValue=t2.HeaderValue
                        ORDER BY t2.ChildValue
                        FOR XML PATH(''), TYPE
                   ).value('.','varchar(max)')
                   ,1,2, ''
              ) AS ChildValues
    FROM @YourTable t1
    GROUP BY t1.HeaderValue

SET @endTime = GETDATE();

SELECT DATEDIFF(millisecond,@startTime, @endTime)
--Take about 4 milliseconds
23
QMaster

Mise à jour: Ms SQL Server 2017+, base de données Azure SQL

Vous pouvez utiliser: STRING_AGG .

L'utilisation est assez simple pour la demande d'OP:

SELECT id, STRING_AGG(name, ', ') AS names
FROM some_table
GROUP BY id

en savoir plus

Eh bien, mon ancienne non-réponse a été supprimée à juste titre (à gauche ci-dessous), mais s'il arrive que quelqu'un atterrisse ici à l'avenir, il y a de bonnes nouvelles. Ils ont également implémenté STRING_AGG () dans la base de données Azure SQL. Cela devrait fournir la fonctionnalité exacte demandée à l'origine dans cet article avec un support natif et intégré. @hrobky l'a mentionné précédemment en tant que fonctionnalité SQL Server 2016 à l'époque.

--- Old Post: Pas assez de réputation ici pour répondre à @hrobky directement, mais STRING_AGG a fière allure, mais il n'est disponible que dans SQL Server 2016 vNext pour le moment. Espérons que cela suivra bientôt dans Azure SQL Datababse.

7
Brian Jorden

Vous pouvez utiliser + = pour concaténer des chaînes, par exemple:

declare @test nvarchar(max)
set @test = ''
select @test += name from names

si vous sélectionnez @test, tous les noms seront concaténés

2
jvc

J'ai trouvé que la réponse de Serge était très prometteuse, mais j'ai également rencontré des problèmes de performances avec son écriture. Cependant, lorsque je l'ai restructuré pour utiliser des tables temporaires et ne pas inclure de tables à double CTE, la performance est passée de 1 minute 40 secondes à une sous-seconde pour 1 000 enregistrements combinés. Pour ceux qui en ont besoin sans FOR XML sur les anciennes versions de SQL Server:

DECLARE @STRUCTURED_VALUES TABLE (
     ID                 INT
    ,VALUE              VARCHAR(MAX) NULL
    ,VALUENUMBER        BIGINT
    ,VALUECOUNT         INT
);

INSERT INTO @STRUCTURED_VALUES
SELECT   ID
        ,VALUE
        ,ROW_NUMBER() OVER (PARTITION BY ID ORDER BY VALUE) AS VALUENUMBER
        ,COUNT(*) OVER (PARTITION BY ID)    AS VALUECOUNT
FROM    RAW_VALUES_TABLE;

WITH CTE AS (
    SELECT   SV.ID
            ,SV.VALUE
            ,SV.VALUENUMBER
            ,SV.VALUECOUNT
    FROM    @STRUCTURED_VALUES SV
    WHERE   VALUENUMBER = 1

    UNION ALL

    SELECT   SV.ID
            ,CTE.VALUE + ' ' + SV.VALUE AS VALUE
            ,SV.VALUENUMBER
            ,SV.VALUECOUNT
    FROM    @STRUCTURED_VALUES SV
    JOIN    CTE 
        ON  SV.ID = CTE.ID
        AND SV.VALUENUMBER = CTE.VALUENUMBER + 1

)
SELECT   ID
        ,VALUE
FROM    CTE
WHERE   VALUENUMBER = VALUECOUNT
ORDER BY ID
;
1
Tom Halladay