Comment créer une fonction SQL Server pour « joindre » plusieurs lignes d'une sous-requête dans un seul champ délimité ?[dupliquer]

StackOverflow https://stackoverflow.com/questions/6899

Question

Pour illustrer, supposons que j'ai deux tableaux 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 souhaite écrire une requête pour renvoyer 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, c'est-à-dire :

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

Était-ce utile?

La solution

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

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

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

Autres conseils

Noter que Le code de Matt entraînera une virgule supplémentaire à la fin de la chaîne ;utiliser COALESCE (ou ISNULL d'ailleurs) comme indiqué dans le lien dans le message de Lance utilise une méthode similaire mais ne vous laisse pas une virgule supplémentaire à supprimer.Par souci d'exhaustivité, voici le code pertinent du 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

Je ne crois pas qu'il existe un moyen de le faire en 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 nettement moins de code que de passer sur un curseur, et probablement plus efficace.

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

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

D'après ce que je peux voir FOR XML (comme publié précédemment) est le seul moyen de le faire si vous souhaitez également sélectionner d'autres colonnes (ce que je suppose que la plupart le feraient) comme le fait l'OP.En utilisant COALESCE(@var... ne permet pas l'inclusion d'autres colonnes.

Mise à jour:Grâce à programmationsolutions.net il existe un moyen de supprimer la virgule « de fin ».En en faisant une virgule de début et en utilisant le STUFF fonction de MSSQL, vous pouvez remplacer le premier caractère (virgule de début) 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

Dans SQL Serveur 2005

SELECT Stuff(
  (SELECT N', ' + Name FROM Names FOR XML PATH(''),TYPE)
  .value('text()[1]','nvarchar(max)'),1,2,N'')

Dans SQL Server 2016

vous pouvez utiliser le POUR la syntaxe JSON

c'est à dire.

SELECT per.ID,
Emails = JSON_VALUE(
   REPLACE(
     (SELECT _ = em.Email FROM Email em WHERE em.Person = per.ID FOR JSON PATH)
    ,'"},{"_":"',', '),'$[0]._'
) 
FROM Person per

Et le résultat deviendra

Id  Emails
1   abc@gmail.com
2   NULL
3   def@gmail.com, xyz@gmail.com

Cela fonctionnera même si vos données contiennent des caractères XML invalides

le '"},{"":"' est sûr car si vos données contiennent '"},{"":"', il sera échappé vers "},{\"_\":\"

Vous pouvez remplacer ', ' par n'importe quel séparateur de chaîne


Et dans SQL Server 2017, Azure SQL Database

Vous pouvez utiliser le nouveau Fonction STRING_AGG

Le code ci-dessous fonctionnera pour SQL Server 2000/2005/2008

CREATE FUNCTION fnConcatVehicleCities(@VehicleId SMALLINT)
RETURNS VARCHAR(1000) AS
BEGIN
  DECLARE @csvCities VARCHAR(1000)
  SELECT @csvCities = COALESCE(@csvCities + ', ', '') + COALESCE(City,'')
  FROM Vehicles 
  WHERE VehicleId = @VehicleId 
  return @csvCities
END

-- //Once the User defined function is created then run the below sql

SELECT VehicleID
     , dbo.fnConcatVehicleCities(VehicleId) AS Locations
FROM Vehicles
GROUP BY VehicleID

J'ai trouvé une solution en créant la fonction suivante :

CREATE FUNCTION [dbo].[JoinTexts]
(
  @delimiter VARCHAR(20) ,
  @whereClause VARCHAR(1)
)
RETURNS VARCHAR(MAX)
AS 
BEGIN
    DECLARE @Texts VARCHAR(MAX)

    SELECT  @Texts = COALESCE(@Texts + @delimiter, '') + T.Texto
    FROM    SomeTable AS T
    WHERE   T.SomeOtherColumn = @whereClause

    RETURN @Texts
END
GO

Usage:

SELECT dbo.JoinTexts(' , ', 'Y')

REMARQUE SUR LA VERSION :Vous devez utiliser SQL Server 2005 ou version ultérieure avec un niveau de compatibilité défini sur 90 ou supérieur pour cette solution.

Regarde ça Article MSDN pour le premier exemple de création d'une fonction d'agrégation définie par l'utilisateur qui concatène un ensemble de valeurs de chaîne extraites d'une colonne d'un tableau.

Mon humble recommandation serait de laisser de côté la virgule ajoutée afin que vous puissiez utiliser votre propre délimiteur ad hoc, le cas échéant.

En référence à la version C# de l'exemple 1 :

change:  this.intermediateResult.Append(value.Value).Append(',');
    to:  this.intermediateResult.Append(value.Value);

Et

change:  output = this.intermediateResult.ToString(0, this.intermediateResult.Length - 1);
    to:  output = this.intermediateResult.ToString();

De cette façon, lorsque vous utilisez votre agrégat personnalisé, vous pouvez choisir d'utiliser votre propre délimiteur, ou aucun, comme :

SELECT dbo.CONCATENATE(column1 + '|') from table1

NOTE: Soyez prudent quant à la quantité de données que vous tentez de traiter dans votre agrégat.Si vous essayez de concaténer des milliers de lignes ou de nombreux types de données très volumineux, vous risquez d'obtenir une erreur .NET Framework indiquant "[l]e tampon est insuffisant".

Avec les autres réponses, la personne qui lit la réponse doit connaître la table des véhicules et créer la table des véhicules et les données pour tester une solution.

Vous trouverez ci-dessous un exemple utilisant la table « Information_Schema.Columns » de SQL Server.En utilisant cette solution, aucune table ne doit être créée ni aucune donnée ajoutée.Cet exemple crée une liste de noms de colonnes séparés par des virgules pour toutes les tables de la base de données.

SELECT
    Table_Name
    ,STUFF((
        SELECT ',' + Column_Name
        FROM INFORMATION_SCHEMA.Columns Columns
        WHERE Tables.Table_Name = Columns.Table_Name
        ORDER BY Column_Name
        FOR XML PATH ('')), 1, 1, ''
    )Columns
FROM INFORMATION_SCHEMA.Columns Tables
GROUP BY TABLE_NAME 

La réponse de Mun n'a pas fonctionné pour moi, j'ai donc apporté quelques modifications à cette réponse pour qu'elle fonctionne.J'espère que cela aide quelqu'un.Utilisation de SQL Server 2012 :

SELECT [VehicleID]
     , [Name]
     , STUFF((SELECT DISTINCT ',' + CONVERT(VARCHAR,City) 
         FROM [Location] 
         WHERE (VehicleID = Vehicle.VehicleID) 
         FOR XML PATH ('')), 1, 2, '') AS Locations
FROM [Vehicle]

Essayez cette requête

SELECT v.VehicleId, v.Name, ll.LocationList
FROM Vehicles v 
LEFT JOIN 
    (SELECT 
     DISTINCT
        VehicleId,
        REPLACE(
            REPLACE(
                REPLACE(
                    (
                        SELECT City as c 
                        FROM Locations x 
                        WHERE x.VehicleID = l.VehicleID FOR XML PATH('')
                    ),    
                    '</c><c>',', '
                 ),
             '<c>',''
            ),
        '</c>', ''
        ) AS LocationList
    FROM Locations l
) ll ON ll.VehicleId = v.VehicleId

Si vous exécutez SQL Server 2005, vous pouvez écrire un fonction d'agrégation CLR personnalisée pour gérer ça.

Version C# :

using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using System.Text;
using Microsoft.SqlServer.Server;
[Serializable]
[Microsoft.SqlServer.Server.SqlUserDefinedAggregate(Format.UserDefined,MaxByteSize=8000)]
public class CSV:IBinarySerialize
{
    private StringBuilder Result;
    public void Init() {
        this.Result = new StringBuilder();
    }

    public void Accumulate(SqlString Value) {
        if (Value.IsNull) return;
        this.Result.Append(Value.Value).Append(",");
    }
    public void Merge(CSV Group) {
        this.Result.Append(Group.Result);
    }
    public SqlString Terminate() {
        return new SqlString(this.Result.ToString());
    }
    public void Read(System.IO.BinaryReader r) {
        this.Result = new StringBuilder(r.ReadString());
    }
    public void Write(System.IO.BinaryWriter w) {
        w.Write(this.Result.ToString());
    }
}
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top