214 votes

Existe-t-il un moyen de créer une fonction SQL Server pour "joindre" plusieurs lignes d'une sous-requête dans un seul champ délimité ?

Pour illustrer, supposons que j'ai deux tables comme suit :

VehicleID Name
1         Chuck
2         Larry

LocationID VehicleID City
1          1         New York
2          1         Seattle
3          1         Vancouver
4          2         Los Angeles
5          2         Houston

Je veux écrire une requête qui renvoie les résultats suivants :

VehicleID Name    Locations
1         Chuck   New York, Seattle, Vancouver
2         Larry   Los Angeles, Houston

Je sais que cela peut être fait en utilisant des curseurs côté serveur, par exemple :

DECLARE @VehicleID int
DECLARE @VehicleName varchar(100)
DECLARE @LocationCity varchar(100)
DECLARE @Locations varchar(4000)
DECLARE @Results TABLE
(
  VehicleID int
  Name varchar(100)
  Locations varchar(4000)
)

DECLARE VehiclesCursor CURSOR FOR
SELECT
  [VehicleID]
, [Name]
FROM [Vehicles]

OPEN VehiclesCursor

FETCH NEXT FROM VehiclesCursor INTO
  @VehicleID
, @VehicleName
WHILE @@FETCH_STATUS = 0
BEGIN

  SET @Locations = ''

  DECLARE LocationsCursor CURSOR FOR
  SELECT
    [City]
  FROM [Locations]
  WHERE [VehicleID] = @VehicleID

  OPEN LocationsCursor

  FETCH NEXT FROM LocationsCursor INTO
    @LocationCity
  WHILE @@FETCH_STATUS = 0
  BEGIN
    SET @Locations = @Locations + @LocationCity

    FETCH NEXT FROM LocationsCursor INTO
      @LocationCity
  END
  CLOSE LocationsCursor
  DEALLOCATE LocationsCursor

  INSERT INTO @Results (VehicleID, Name, Locations) SELECT @VehicleID, @Name, @Locations

END     
CLOSE VehiclesCursor
DEALLOCATE VehiclesCursor

SELECT * FROM @Results

Cependant, comme vous pouvez le constater, cela nécessite une grande quantité de code. Ce que j'aimerais, c'est une fonction générique qui me permettrait de faire quelque chose comme ceci :

SELECT VehicleID
     , Name
     , JOIN(SELECT City FROM Locations WHERE VehicleID = Vehicles.VehicleID, ', ') AS Locations
FROM Vehicles

Est-ce possible ? Ou quelque chose de similaire ?

2 votes

Une réponse similaire avec une réponse plus complète stackoverflow.com/a/17591536/1587302

0 votes

@Narkha parfait ! Je n'avais pas vu celui-là dans ma recherche. Je vais supprimer ma question maintenant. Je vous remercie de votre attention.

286voto

Mun Points 6844

Si vous utilisez SQL Server 2005, vous pouvez utiliser la commande FOR XML PATH.

SELECT [VehicleID]
     , [Name]
     , (SELECT CAST(City + ', ' AS VARCHAR(MAX)) 
         FROM [Location] 
         WHERE (VehicleID = Vehicle.VehicleID) 
         FOR XML PATH ('')
      ) AS Locations
FROM [Vehicle]

C'est beaucoup plus facile que d'utiliser un curseur, et cela semble fonctionner assez bien.

13 votes

Cela fonctionnera bien avec ces données, mais si vos données comportent des caractères spéciaux xml (par exemple <, >, &), ils seront remplacés (<, etc.).

4 votes

@James Vous pourriez utiliser un CTE pour accomplir ceci : WITH MyCTE(VehicleId, Name, Locations) AS ( SELECT [VehicleID] , [Name] , (SELECT CAST(City + ', ' AS VARCHAR(MAX)) FROM [Location] WHERE (VehicleID = Vehicle.VehicleID) FOR XML PATH ('') ) AS Locations FROM [Vehicle] ) SELECT VehicleId, Name, REPLACE(Locations, ',', CHAR(10)) AS Locations FROM MyCTE

1 votes

Je me suis un peu embrouillé en modifiant ce code, j'ai donc posté le message suivant ma propre question

89voto

Mike Powell Points 2913

Notez que le code de Matt ci-dessus entraînera une virgule supplémentaire à la fin de la chaîne de caractères ; l'utilisation de COALESCE (ou ISNULL d'ailleurs) comme indiqué dans le lien du message de Lance utilise une méthode similaire mais ne vous laisse pas avec une virgule supplémentaire à supprimer. Pour être complet, voici le code correspondant au lien de Lance sur sqlteam.com :

DECLARE @EmployeeList varchar(100)
SELECT @EmployeeList = COALESCE(@EmployeeList + ', ', '') + 
    CAST(EmpUniqueID AS varchar(5))
FROM SalesCallsEmployees
WHERE SalCal_UniqueID = 1

7 votes

Pas de virgule supplémentaire, ce qui est bien, mais aussi beaucoup plus facile à lire et à comprendre, à mon avis, que la solution acceptée. Merci beaucoup !

4 votes

Ce n'est pas une solution fiable .

4 votes

@lukasLansky c'est fiable tant que vous ne vous souciez pas de l'ordre.

50voto

Matt Hamilton Points 98268

Je ne pense pas qu'il y ait un moyen de le faire dans une seule requête, mais vous pouvez jouer des tours comme celui-ci avec une variable temporaire :

declare @s varchar(max)
set @s = ''
select @s = @s + City + ',' from Locations

select @s

C'est certainement moins de code que de passer sur un curseur, et probablement plus efficace.

12 votes

Je suis presque sûr que tu peux enlever le "probablement" de la dernière ligne.

1 votes

"Je ne crois pas qu'il y ait un moyen de le faire dans une seule requête". Si, il y en a un. SQL Server 2005 avait à la fois FOR XML et les CTE.

2 votes

Ce n'est pas fiable, cela dépend du plan d'exécution, des lignes peuvent être perdues. Voir KB .

25voto

ZunTzu Points 196

Dans une seule requête SQL, sans utiliser la clause FOR XML.
Une expression de table commune est utilisée pour concaténer récursivement les résultats.

-- rank locations by incrementing lexicographical order
WITH RankedLocations AS (
  SELECT
    VehicleID,
    City,
    ROW_NUMBER() OVER (
        PARTITION BY VehicleID 
        ORDER BY City
    ) Rank
  FROM
    Locations
),
-- concatenate locations using a recursive query
-- (Common Table Expression)
Concatenations AS (
  -- for each vehicle, select the first location
  SELECT
    VehicleID,
    CONVERT(nvarchar(MAX), City) Cities,
    Rank
  FROM
    RankedLocations
  WHERE
    Rank = 1

  -- then incrementally concatenate with the next location
  -- this will return intermediate concatenations that will be 
  -- filtered out later on
  UNION ALL

  SELECT
    c.VehicleID,
    (c.Cities + ', ' + l.City) Cities,
    l.Rank
  FROM
    Concatenations c -- this is a recursion!
    INNER JOIN RankedLocations l ON
        l.VehicleID = c.VehicleID 
        AND l.Rank = c.Rank + 1
),
-- rank concatenation results by decrementing length 
-- (rank 1 will always be for the longest concatenation)
RankedConcatenations AS (
  SELECT
    VehicleID,
    Cities,
    ROW_NUMBER() OVER (
        PARTITION BY VehicleID 
        ORDER BY Rank DESC
    ) Rank
  FROM 
    Concatenations
)
-- main query
SELECT
  v.VehicleID,
  v.Name,
  c.Cities
FROM
  Vehicles v
  INNER JOIN RankedConcatenations c ON 
    c.VehicleID = v.VehicleID 
    AND c.Rank = 1

4 votes

Merci pour cela. C'est l'une des rares solutions à ce problème qui n'utilise pas de variables, de fonctions, la clause FOR XML ou le code CLR. Cela signifie que j'ai pu adapter votre solution pour résoudre Défi 4 du TSQL pour débutants - Concaténation de valeurs de plusieurs lignes .

1 votes

Merci. Je dois convertir une série de fragments de code SQL exprimés sous forme de rangées distinctes de phrases booléennes en une seule expression de code complexe, et je suis impatient d'essayer votre méthode.

2 votes

Cette solution présente-t-elle des avantages en termes de performances par rapport aux autres solutions ?

24voto

John B Points 459

De ce que je peux voir FOR XML (comme indiqué précédemment) est la seule façon de procéder si vous souhaitez également sélectionner d'autres colonnes (ce qui est le cas de la plupart des gens) comme le fait le PO. Utilisation de COALESCE(@var... ne permet pas d'inclure d'autres colonnes.

Mise à jour : Merci à programmingsolutions.net il existe un moyen de supprimer la virgule de fin. En la transformant en une virgule de tête et en utilisant la fonction STUFF de MSSQL, vous pouvez remplacer le premier caractère (virgule de tête) par une chaîne vide comme ci-dessous :

stuff(
    (select ',' + Column 
     from Table
         inner where inner.Id = outer.Id 
     for xml path('')
), 1,1,'') as Values

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X