Combinez plusieurs résultats dans une sous-requête en une seule valeur séparée par des virgules

84

J'ai deux tables:

TableA
------
ID,
Name

TableB
------
ID,
SomeColumn,
TableA_ID (FK for TableA)

La relation est une rangée de TableA- plusieurs TableB.

Maintenant, je veux voir un résultat comme celui-ci:

ID     Name      SomeColumn

1.     ABC       X, Y, Z (these are three different rows)
2.     MNO       R, S

Cela ne fonctionnera pas (plusieurs résultats dans une sous-requête):

SELECT ID,
       Name, 
       (SELECT SomeColumn FROM TableB WHERE F_ID=TableA.ID)
FROM TableA

C'est un problème trivial si je fais le traitement côté client. Mais cela signifie que je devrai exécuter des requêtes X sur chaque page, où X est le nombre de résultats de TableA.

Notez que je ne peux pas simplement faire un GROUP BY ou quelque chose de similaire, car il renverra plusieurs résultats pour les lignes de TableA.

Je ne sais pas si un UDF, utilisant COALESCE ou quelque chose de similaire pourrait fonctionner?

Donnie Thomas
la source

Réponses:

134

Même cela servira le but

Exemple de données

declare @t table(id int, name varchar(20),somecolumn varchar(MAX))
insert into @t
    select 1,'ABC','X' union all
    select 1,'ABC','Y' union all
    select 1,'ABC','Z' union all
    select 2,'MNO','R' union all
    select 2,'MNO','S'

Requete:

SELECT ID,Name,
    STUFF((SELECT ',' + CAST(T2.SomeColumn AS VARCHAR(MAX))
     FROM @T T2 WHERE T1.id = T2.id AND T1.name = T2.name
     FOR XML PATH('')),1,1,'') SOMECOLUMN
FROM @T T1
GROUP BY id,Name

Production:

ID  Name    SomeColumn
1   ABC     X,Y,Z
2   MNO     R,S
priyanka.sarkar
la source
13
Je ne sais pas pourquoi cela n'a pas été détecté car il résout le problème sans nécessiter une fonction utilisateur. Vous pouvez voir la même idée exprimée ici codecorner.galanter.net/2009/06/25/… qui précède cette réponse et pourrait donc être "l'original"
Paul D'Ambra
1
Même chose ici, je ne sais pas pourquoi ce n'est pas noté plus haut
Marcel
1
Salut Priyanka, pouvez-vous me dire si et pourquoi la clause "and t1.name = t2.name" est nécessaire ici?
Koen
2
C'est excellent. Je cherchais à optimiser une fonction UDF comme indiqué dans la réponse acceptée qui tuait mon serveur. Je suis passé d'une recherche de 102 secondes à moins de 1. La comparaison du plan d'exécution était de 78% -22% mais cela ne concerne pas le temps d'exécution ...
toxaq
Juste un rappel que vous aurez besoin de ce début ',' sinon vous vous retrouverez avec des chevrons dans votre sortie.
Tim Scarborough
45

1. Créez l'UDF:

CREATE FUNCTION CombineValues
(
    @FK_ID INT -- The foreign key from TableA which is used 
               -- to fetch corresponding records
)
RETURNS VARCHAR(8000)
AS
BEGIN
DECLARE @SomeColumnList VARCHAR(8000);

SELECT @SomeColumnList =
    COALESCE(@SomeColumnList + ', ', '') + CAST(SomeColumn AS varchar(20)) 
FROM TableB C
WHERE C.FK_ID = @FK_ID;

RETURN 
(
    SELECT @SomeColumnList
)
END

2. Utilisation dans la sous-requête:

SELECT ID, Name, dbo.CombineValues(FK_ID) FROM TableA

3. Si vous utilisez une procédure stockée, vous pouvez procéder comme suit:

CREATE PROCEDURE GetCombinedValues
 @FK_ID int
As
BEGIN
DECLARE @SomeColumnList VARCHAR(800)
SELECT @SomeColumnList =
    COALESCE(@SomeColumnList + ', ', '') + CAST(SomeColumn AS varchar(20)) 
FROM TableB
WHERE FK_ID = @FK_ID 

Select *, @SomeColumnList as SelectedIds
    FROM 
        TableA
    WHERE 
        FK_ID = @FK_ID 
END
Donnie Thomas
la source
1
Cela ressemble toujours à un hack. J'utilise toujours des sous-requêtes, donc il y a encore beaucoup de traitements supplémentaires en cours. Je suis sûr qu'il existe une meilleure solution (restructuration de la table, ou autre façon de voir le problème).
Donnie Thomas
1
Je n'appellerais pas ça un hack. C'est plus efficace qu'un curseur ne le serait, et il manque la surcharge qui serait nécessaire pour créer une table temporaire avec les données structurées comme vous le souhaitez.
Scott Lawrence
1
Dommage que les colonnes ne puissent pas être des paramètres. Dans l'état actuel des choses, vous devrez créer une fonction pour chaque relation enfantine!
John Paul Jones
1
Ce n'est pas grave - je dois combiner uniquement ces colonnes particulières. Les autres sont des jointures «traditionnelles».
Donnie Thomas
Je ne me souviens pas de la meilleure façon de faire cela sans cette méthode.
aF.
11

Je pense que vous êtes sur la bonne voie avec COALESCE. Voir ici pour un exemple de construction d'une chaîne délimitée par des virgules:

http://www.sqlteam.com/article/using-coalesce-to-build-comma-delimited-string

Ben Hoffstein
la source
2
Impressionnant! J'avais vu des liens sur COALESCE, mais ils impliquaient de créer des UDF avec des déclencheurs. Le lien que vous avez soumis a la clé, avec une seule instruction SELECT. J'ajoute une réponse avec la bonne solution, afin qu'elle soit plus facile à trouver pour les autres. Merci!
Donnie Thomas
1
Salut Ben, je pense que la réponse a besoin d'un peu plus de détails, à savoir comment créer l'UDF, etc. Une fois que j'aurai compris cela, j'ajouterai la solution en tant que réponse modifiable par la communauté. N'hésitez pas à le modifier, après quoi je l'accepterai comme réponse. Désolé pour la confusion.
Donnie Thomas
11

Dans MySQL, il existe une fonction group_concat qui retournera ce que vous demandez.

SELECT TableA.ID, TableA.Name, group_concat(TableB.SomeColumn) 
as SomColumnGroup FROM TableA LEFT JOIN TableB ON 
TableB.TableA_ID = TableA.ID
Jacob
la source
1
Cela aurait été parfait s'il y avait une fonction similaire dans SQL Server. Dans l'état actuel des choses, j'utilise la solution de Ben pour marteler ce que je veux.
Donnie Thomas
0

Vous devrez peut-être fournir plus de détails pour une réponse plus précise.

Étant donné que votre ensemble de données semble assez étroit, vous pouvez envisager d'utiliser simplement une ligne par résultat et d'effectuer le post-traitement chez le client.

Donc, si vous cherchez vraiment à faire faire le travail au serveur, renvoyez un jeu de résultats comme

ID       Name       SomeColumn
1        ABC        X
1        ABC        Y
1        ABC        Z
2        MNO        R
2        MNO        S

qui est bien sûr un simple INNER JOIN sur ID

Une fois que vous avez le résultat sur le client, maintenez une variable appelée CurrentName et utilisez-la comme déclencheur pour arrêter de collecter SomeColumn dans la chose utile que vous voulez qu'elle fasse.

Facture
la source
J'y ai pensé, mais je n'étais pas très sûr qu'il s'agisse d'une solution élégante - j'aimerais que SQL Server renvoie le jeu de résultats correctement construit, pas quelque chose qui devra être traité davantage. Souhaitez-vous des détails supplémentaires? J'ai simplifié la structure du tableau, mais je pense que vous l'avez.
Donnie Thomas
0

En supposant que vous n'avez que des clauses WHERE sur la table A, créez une procédure stockée ainsi:

SELECT Id, Name From tableA WHERE ...

SELECT tableA.Id AS ParentId, Somecolumn 
FROM tableA INNER JOIN tableB on TableA.Id = TableB.F_Id 
WHERE ...

Remplissez ensuite un DataSet ds avec. ensuite

ds.Relations.Add("foo", ds.Tables[0].Columns("Id"), ds.Tables[1].Columns("ParentId"));

Enfin, vous pouvez ajouter un répéteur dans la page qui met les virgules pour chaque ligne

 <asp:DataList ID="Subcategories" DataKeyField="ParentCatId" 
DataSource='<%# Container.DataItem.CreateChildView("foo") %>' RepeatColumns="1"
 RepeatDirection="Horizontal" ItemStyle-HorizontalAlign="left" ItemStyle-VerticalAlign="top" 
runat="server" >

De cette façon, vous le ferez côté client mais avec une seule requête, en passant un minimum de données entre la base de données et le frontend

Sklivvz
la source
0

J'ai essayé la solution mentionnée par priyanka.sarkar et je n'ai pas tout à fait fonctionné comme le PO l'a demandé. Voici la solution avec laquelle j'ai abouti:

SELECT ID, 
        SUBSTRING((
            SELECT ',' + T2.SomeColumn
            FROM  @T T2 
            WHERE WHERE T1.id = T2.id
            FOR XML PATH('')), 2, 1000000)
    FROM @T T1
GROUP BY ID
mrogunlana
la source
-1

Solution ci-dessous:

SELECT GROUP_CONCAT(field_attr_best_weekday_value)as RAVI
FROM content_field_attr_best_weekday LEFT JOIN content_type_attraction
    on content_field_attr_best_weekday.nid = content_type_attraction.nid
GROUP BY content_field_attr_best_weekday.nid

Utilisez ceci, vous pouvez également modifier les jointures

ravi
la source
-1
SELECT t.ID, 
       t.NAME, 
       (SELECT t1.SOMECOLUMN 
        FROM   TABLEB t1 
        WHERE  t1.F_ID = T.TABLEA.ID) 
FROM   TABLEA t; 

Cela fonctionnera pour la sélection dans une table différente en utilisant une sous-requête.

ATHAR
la source
-1

J'ai passé en revue toutes les réponses. Je pense que l'insertion de base de données devrait être comme:

ID     Name      SomeColumn
1.     ABC       ,X,Y Z (these are three different rows)
2.     MNO       ,R,S

La virgule doit être à la fin précédente et faire une recherche par like %,X,%

rsda
la source