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