web-dev-qa-db-fra.com

Concaténation sur plusieurs colonnes

Comment concaténer plusieurs colonnes en une seule ligne? Par exemple:

id   name    car
1    sam     dodge
1    ram     maserati
1    john    benz
1    NULL    mazda
2    kirk    lexus
2    Jim     rolls
1            GMC

L'ensemble de résultats attendu est:

ID  name               car
1   sam,ram,john       dodge,maserati,benz,mazda,GMC
2   kirk,jim           lexus,rolls

En utilisant une solution j'ai trouvé sur Stack Overflow:

SELECT * FROM (
SELECT t.id,stuff([m].query('/name').value('/', 'varchar(max)'),1,1,'') AS [SomeField_Combined1],
stuff([m].query('/car').value('/', 'varchar(max)'),1,1,'') AS [SomeField_Combined2]
FROM dbo.test t
OUTER apply(SELECT (

SELECT id, ','+name AS name
,','+car AS car
FROM test WHERE test.id=t.id
FOR XML PATH('') ,type)
             AS  M) A)S
GROUP BY id,somefield_combined1,somefield_combined2 

Y a-t-il de meilleures solutions? La sélection interne provient d'une jointure multi-table coûteuse (et non du "test" de table unique illustré ci-dessus). La requête est dans un TVF en ligne, donc je ne peux pas utiliser une table temporaire.

De plus, s'il y a une colonne vide, les résultats produiront des virgules supplémentaires comme

ID  name                car
1   sam,ram,john,,      dodge,maserati,benz,mazda,GMC
2   kirk,jim           lexus,rolls

Est-il un moyen d'empêcher cela?

6
Biju jose

J'ai effectué quelques tests en utilisant un peu plus de 6 mil de rangées. Avec un index sur la colonne ID.

Voici ce que j'ai trouvé.

Votre requête initiale:

SELECT * FROM (
    SELECT t.id,
            stuff([M].query('/name').value('/', 'varchar(max)'),1,1,'') AS [SomeField_Combined1],
            stuff([M].query('/car').value('/', 'varchar(max)'),1,1,'') AS [SomeField_Combined2]
    FROM dbo.test t
    OUTER APPLY(SELECT (
                    SELECT id, ','+name AS name
                    ,','+car AS car
                    FROM test WHERE test.id=t.id
                    FOR XML PATH('') ,type)
                 AS  M) 
            M ) S
GROUP BY id, SomeField_Combined1, SomeField_Combined2 

Celui-ci a duré environ 23 minutes.

J'ai exécuté cette version qui est la version que j'ai apprise en premier. À certains égards, il semble que cela devrait prendre plus de temps, mais ce n'est pas le cas.

SELECT test.id,
    STUFF((SELECT ', ' + ThisTable.name
            FROM   test ThisTable
            WHERE  test.id = ThisTable.id
            AND    ThisTable.name <> ''
            FOR XML PATH ('')),1,2,'') AS ConcatenatedSomeField,
    STUFF((SELECT ', ' + car
            FROM   test ThisTable
            WHERE  test.id = ThisTable.id
            AND    ThisTable.car <> ''
            FOR XML PATH ('')),1,2,'') AS ConcatenatedSomeField2
FROM test 
GROUP BY id

Cette version a fonctionné en un peu plus de 2 minutes.

5
Kenneth Fisher

Un agrégat CLR sera presque certainement le moyen le plus rapide de le faire. Mais peut-être que vous ne voulez pas en utiliser un pour une raison quelconque ...

Vous dites que la source de ceci est une requête coûteuse.

Je matérialiserais cela en un #temp table d'abord pour vous assurer qu'elle n'est évaluée qu'une seule fois.

CREATE TABLE #test
(
ID INT,
name NVARCHAR(128),
car  NVARCHAR(128)
);

CREATE CLUSTERED INDEX ix ON #test(ID);

Le plan d'exécution que j'obtiens pour la requête dans la question fait d'abord la concaténation pour chaque ligne de la requête externe, puis supprime les doublons par id, SomeField_Combined1, SomeField_Combined2.

C'est incroyablement inutile. La réécriture suivante évite cela.

SELECT t.id,
       stuff([M].query('/name').value('/', 'varchar(max)'), 1, 1, '') AS [SomeField_Combined1],
       stuff([M].query('/car').value('/', 'varchar(max)'), 1, 1, '')  AS [SomeField_Combined2]
FROM   (SELECT DISTINCT id
        FROM   #test) t
       OUTER APPLY(SELECT (SELECT id,
                                  ',' + name AS name,
                                  ',' + car  AS car
                           FROM   #test
                           WHERE  #test.id = t.id
                           FOR XML PATH(''), type) AS M) M 

Cependant pour les données de test suivantes (1000 identifiants avec 2156 lignes par identifiant pour moi)

INSERT INTO #test
SELECT v.number, o.name, o.type_desc
FROM   sys.all_objects o
       INNER JOIN master..spt_values v
         ON v.type = 'P' AND v.number BETWEEN 1 AND 1000 

J'ai toujours trouvé la solution de Kenneth avec deux XML PATH les appels sont beaucoup plus rapides et nécessitent moins de ressources.

+-----------------+--------------------+------------------------+------------------+---------------------+-------------------------+-----------------------------+
|                 | CPU Time (Seconds) | Elapsed Time (Seconds) | #test Scan Count | #test Logical Reads | Worktable logical reads | Worktable lob logical reads |
+-----------------+--------------------+------------------------+------------------+---------------------+-------------------------+-----------------------------+
| Single XML PATH | 51.077             | 15.521                 | 1,005            | 60,165              | 51,161                  | 1,329,207                   |
| Double XML PATH | 3.1720             | 3.010                  | 2,005            | 92,088              | 14,951                  |   233,681                   |
+-----------------+--------------------+------------------------+------------------+---------------------+-------------------------+-----------------------------+

Pour chaque id distinct dans #test il effectue deux opérations au lieu d'une mais cette opération est nettement moins chère que de construire le XML et de le réanalyser.

6
Martin Smith

Comme l'a déjà souligné Martin Smith, un agrégat CLR est probablement votre meilleur pari. Encore une fois, le stockage de vos résultats dans une table temporaire est une bonne idée.

Voici une autre implémentation T-SQL possible qui utilise UNPIVOT/PIVOT:

IF OBJECT_ID('tempdb..#test') IS NOT NULL 
    DROP TABLE #test;

CREATE TABLE #test (
    id int,
    name varchar(128),
    car varchar(128)
)

CREATE CLUSTERED INDEX ix ON #test(ID);

INSERT INTO #test
SELECT v.number, o.name, o.type_desc
FROM   sys.all_objects o
       INNER JOIN master..spt_values v
         ON v.type = 'P' AND v.number BETWEEN 1 AND 1000 

;WITH info(col) AS (
    SELECT 'car'
    UNION ALL
    SELECT 'name'
)
SELECT *
FROM info
CROSS JOIN (
    SELECT DISTINCT id
    FROM #test
) AS ids
CROSS APPLY (
    SELECT v = STUFF((
        SELECT ',' + value AS [text()]
        FROM #test
        UNPIVOT (value FOR col IN (name,car)) AS u
        WHERE col = info.col
            AND id = ids.id
            AND value <> ''
        FOR XML PATH(''), type
    ).value('.','varchar(max)'),1,1,SPACE(0))
) AS ca(val)
PIVOT (MIN(val) FOR col IN (car,name)) AS p;

Il fonctionne à peu près en même temps que la solution de Kenneth.

1
spaghettidba

Essaye ça

Utilisez la fonction Right pour supprimer la virgule au lieu des fonctions xml

Utilisez les instructions case pour éviter la virgule pour les espaces vides

SELECT t.id, 
       RIGHT(A.NAME, Len(A.NAME) - 1) AS NAME, 
       RIGHT(A.car, Len(A.car) - 1)   AS car 
FROM   dbo.test t 
       OUTER apply(SELECT (SELECT id, 
                                  CASE WHEN NAME<>'' THEN ',' ELSE '' END + NAME  AS NAME, 
                                  CASE WHEN car<>'' THEN ',' ELSE '' END + car AS car 
                           FROM   test 
                           WHERE  test.id = t.id 
                           FOR xml path(''), type) AS M) A 
GROUP  BY id, 
          RIGHT(A.NAME, Len(A.NAME) - 1), 
          RIGHT(A.car, Len(A.car) - 1) 

Remarque: Ici Group by peut également être remplacé par distinct puisque nous n'utilisons aucune fonction aggregate

0
Pரதீப்