Vous simulez la fonction group_concat MySQL dans Microsoft SQL Server 2005?


347

J'essaie de migrer une application basée sur MySQL vers Microsoft SQL Server 2005 (pas par choix, mais c'est la vie).

Dans l'application d'origine, nous avons utilisé presque entièrement des instructions conformes à ANSI-SQL, à une exception près: nous avons utilisé la group_concatfonction de MySQL assez fréquemment.

group_concat, soit dit en passant, à partir d'un tableau, disons, des noms des employés et des projets ...

SELECT empName, projID FROM project_members;

Retour:

ANDY   |  A100
ANDY   |  B391
ANDY   |  X010
TOM    |  A100
TOM    |  A510

... et voici ce que vous obtenez avec group_concat:

SELECT 
    empName, group_concat(projID SEPARATOR ' / ') 
FROM 
    project_members 
GROUP BY 
    empName;

Retour:

ANDY   |  A100 / B391 / X010
TOM    |  A100 / A510

Donc, ce que je voudrais savoir, c'est: est-il possible d'écrire, par exemple, une fonction définie par l'utilisateur dans SQL Server qui émule la fonctionnalité de group_concat?

Je n'ai presque aucune expérience en utilisant des FDU, des procédures stockées ou quelque chose comme ça, juste du SQL direct, alors veuillez vous tromper du côté de trop d'explications :)



C'est une vieille question, mais j'aime la solution CLR donnée ici .
Diego

doublon possible de Comment créer une liste séparée par des virgules à l'aide d'une requête SQL? - ce poste est plus large, donc je choisirais celui-ci comme canonique
TMS


Comment savez-vous dans quel ordre la liste doit être construite, par exemple, vous montrez A100 / B391 / X010 mais étant donné qu'il n'y a pas de commande implicite dans une base de données relationnelle, cela pourrait tout aussi bien être X010 / A100 / B391 ou toute autre combinaison.
Steve Ford

Réponses:


174

Pas de VRAI moyen facile de le faire. Beaucoup d'idées cependant.

Le meilleur que j'ai trouvé :

SELECT table_name, LEFT(column_names , LEN(column_names )-1) AS column_names
FROM information_schema.columns AS extern
CROSS APPLY
(
    SELECT column_name + ','
    FROM information_schema.columns AS intern
    WHERE extern.table_name = intern.table_name
    FOR XML PATH('')
) pre_trimmed (column_names)
GROUP BY table_name, column_names;

Ou une version qui fonctionne correctement si les données peuvent contenir des caractères tels que <

WITH extern
     AS (SELECT DISTINCT table_name
         FROM   INFORMATION_SCHEMA.COLUMNS)
SELECT table_name,
       LEFT(y.column_names, LEN(y.column_names) - 1) AS column_names
FROM   extern
       CROSS APPLY (SELECT column_name + ','
                    FROM   INFORMATION_SCHEMA.COLUMNS AS intern
                    WHERE  extern.table_name = intern.table_name
                    FOR XML PATH(''), TYPE) x (column_names)
       CROSS APPLY (SELECT x.column_names.value('.', 'NVARCHAR(MAX)')) y(column_names) 

1
Cet exemple a fonctionné pour moi, mais j'ai essayé de faire une autre agrégation et cela n'a pas fonctionné, m'a donné une erreur: "le nom de corrélation 'pre_trimmed' est spécifié plusieurs fois dans une clause FROM."
PhilChuang

7
'pre_trimmed' est juste un alias pour la sous-requête. Les alias sont requis pour les sous-requêtes et doivent être uniques, donc pour une autre sous-requête, changez-le en quelque chose d'unique ...
Koen

2
pouvez-vous montrer un exemple sans nom_table comme nom de colonne, c'est déroutant.
S.Mason

169

Je suis peut-être un peu en retard à la fête mais cette méthode fonctionne pour moi et est plus facile que la méthode COALESCE.

SELECT STUFF(
             (SELECT ',' + Column_Name 
              FROM Table_Name
              FOR XML PATH (''))
             , 1, 1, '')

1
Cela montre seulement comment concaténer des valeurs - group_concat les concatient par groupe, ce qui est plus difficile (et ce que l'OP semble exiger). Voir la réponse acceptée au SO 15154644 pour savoir comment procéder - la clause WHERE est l'ajout critique
DJDave

@DJDave faisait référence à cette réponse . Voir également la réponse acceptée à une question similaire .
John Cummings

51

Peut-être trop tard pour en bénéficier maintenant, mais n'est-ce pas la façon la plus simple de faire les choses?

SELECT     empName, projIDs = replace
                          ((SELECT Surname AS [data()]
                              FROM project_members
                              WHERE  empName = a.empName
                              ORDER BY empName FOR xml path('')), ' ', REQUIRED SEPERATOR)
FROM         project_members a
WHERE     empName IS NOT NULL
GROUP BY empName

Intéressant. J'ai déjà terminé le projet, mais je vais essayer cette méthode. Merci!
DanM

7
Belle astuce - le seul problème est que pour les noms de famille avec des espaces, il remplacera l'espace par le séparateur.
Mark Elliot

J'ai moi-même rencontré un tel problème, Mark. Malheureusement, jusqu'à ce que MSSQL arrive avec le temps et présente GROUP_CONCAT, c'est la moindre des méthodes intensives que j'ai pu trouver pour ce qui est nécessaire ici.
J Hardiman

Merci pour cela! Voici un SQL Fiddle montrant qu'il fonctionne: sqlfiddle.com/#!6/c5d56/3
fui le

42

SQL Server 2017 introduit une nouvelle fonction d'agrégation

STRING_AGG ( expression, separator).

Concatène les valeurs des expressions de chaîne et place des valeurs de séparateur entre elles. Le séparateur n'est pas ajouté à la fin de la chaîne.

Les éléments concaténés peuvent être commandés en ajoutant WITHIN GROUP (ORDER BY some_expression)

Pour les versions 2005-2016 j'utilise généralement la méthode XML dans la réponse acceptée.

Cela peut cependant échouer dans certaines circonstances. par exemple, si les données à concaténer contiennent CHAR(29)vous voyez

FOR XML n'a pas pu sérialiser les données ... car elles contiennent un caractère (0x001D) qui n'est pas autorisé en XML.

Une méthode plus robuste qui peut gérer tous les caractères serait d'utiliser un agrégat CLR. Cependant, appliquer un ordre aux éléments concaténés est plus difficile avec cette approche.

La méthode d'affectation à une variable est pas garantie et doit être évitée dans le code de production.


Ceci est également disponible maintenant dans Azure SQL: azure.microsoft.com/en-us/roadmap/…
Simon_Weaver

34

Jetez un œil au projet GROUP_CONCAT sur Github, je pense que je fais exactement ce que vous recherchez:

Ce projet contient un ensemble de fonctions d'agrégation définies par l'utilisateur SQLCLR (UDA SQLCLR) qui offrent collectivement des fonctionnalités similaires à la fonction MySQL GROUP_CONCAT. Il existe plusieurs fonctions pour assurer les meilleures performances en fonction des fonctionnalités requises ...


2
@MaxiWheat: beaucoup de gars ne lisent pas attentivement la question ou la réponse avant de cliquer sur voter. Cela affecte directement la publication du propriétaire en raison de son erreur.
Steve Lam

Fonctionne très bien. La fonction que je manque est la possibilité de trier sur une colonne MySQL group_concat () peut aimer:GROUP_CONCAT(klascode,'(',name,')' ORDER BY klascode ASC SEPARATOR ', ')
Jan

10

Pour concaténer tous les noms de chef de projet des projets qui ont plusieurs chefs de projet, écrivez:

SELECT a.project_id,a.project_name,Stuff((SELECT N'/ ' + first_name + ', '+last_name FROM projects_v 
where a.project_id=project_id
 FOR
 XML PATH(''),TYPE).value('text()[1]','nvarchar(max)'),1,2,N''
) mgr_names
from projects_v a
group by a.project_id,a.project_name

9

Avec le code ci-dessous, vous devez définir PermissionLevel = External sur les propriétés de votre projet avant de déployer et modifier la base de données pour approuver le code externe (assurez-vous de lire ailleurs sur les risques de sécurité et les alternatives [comme les certificats]) en exécutant "ALTER DATABASE database_name SET TRUSTWORTHY ON ".

using System;
using System.Collections.Generic;
using System.Data.SqlTypes;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using Microsoft.SqlServer.Server;

[Serializable]
[SqlUserDefinedAggregate(Format.UserDefined,
MaxByteSize=8000,
IsInvariantToDuplicates=true,
IsInvariantToNulls=true,
IsInvariantToOrder=true,
IsNullIfEmpty=true)]
    public struct CommaDelimit : IBinarySerialize
{


[Serializable]
 private class StringList : List<string>
 { }

 private StringList List;

 public void Init()
 {
  this.List = new StringList();
 }

 public void Accumulate(SqlString value)
 {
  if (!value.IsNull)
   this.Add(value.Value);
 }

 private void Add(string value)
 {
  if (!this.List.Contains(value))
   this.List.Add(value);
 }

 public void Merge(CommaDelimit group)
 {
  foreach (string s in group.List)
  {
   this.Add(s);
  }
 }

 void IBinarySerialize.Read(BinaryReader reader)
 {
    IFormatter formatter = new BinaryFormatter();
    this.List = (StringList)formatter.Deserialize(reader.BaseStream);
 }

 public SqlString Terminate()
 {
  if (this.List.Count == 0)
   return SqlString.Null;

  const string Separator = ", ";

  this.List.Sort();

  return new SqlString(String.Join(Separator, this.List.ToArray()));
 }

 void IBinarySerialize.Write(BinaryWriter writer)
 {
  IFormatter formatter = new BinaryFormatter();
  formatter.Serialize(writer.BaseStream, this.List);
 }
    }

J'ai testé cela en utilisant une requête qui ressemble à:

SELECT 
 dbo.CommaDelimit(X.value) [delimited] 
FROM 
 (
  SELECT 'D' [value] 
  UNION ALL SELECT 'B' [value] 
  UNION ALL SELECT 'B' [value] -- intentional duplicate
  UNION ALL SELECT 'A' [value] 
  UNION ALL SELECT 'C' [value] 
 ) X 

Et donne: A, B, C, D


9

J'ai essayé cela, mais pour mes besoins dans MS SQL Server 2005, ce qui suit était le plus utile, que j'ai trouvé sur xaprb

declare @result varchar(8000);

set @result = '';

select @result = @result + name + ' '

from master.dbo.systypes;

select rtrim(@result);

@Mark comme vous l'avez mentionné, c'est le caractère spatial qui m'a causé des problèmes.


Je pense que le moteur ne garantit vraiment aucun ordre avec cette méthode, car les variables sont calculées comme des flux de données en fonction du plan d'exécution. Cela semble fonctionner la plupart du temps jusqu'à présent.
phil_w

6

À propos de la réponse de J Hardiman, que diriez-vous:

SELECT empName, projIDs=
  REPLACE(
    REPLACE(
      (SELECT REPLACE(projID, ' ', '-somebody-puts-microsoft-out-of-his-misery-please-') AS [data()] FROM project_members WHERE empName=a.empName FOR XML PATH('')), 
      ' ', 
      ' / '), 
    '-somebody-puts-microsoft-out-of-his-misery-please-',
    ' ') 
  FROM project_members a WHERE empName IS NOT NULL GROUP BY empName

Soit dit en passant, l'utilisation de "Nom de famille" est-elle une faute de frappe ou est-ce que je ne comprends pas un concept ici?

Quoi qu'il en soit, merci beaucoup les gars car cela m'a fait gagner du temps :)


1
Réponse plutôt inamicale si vous me le demandez et pas du tout utile comme réponse.
Tim Meers

1
ne voyant que maintenant ... Je ne le pensais pas de façon méchante, à l'époque j'étais très frustré avec le serveur sql (je le suis toujours). les réponses de ce post étaient vraiment utiles en fait; EDIT: pourquoi cela ne vous a-t-il pas été utile? ça a fait l'affaire pour moi
user422190

1

Pour mes collègues Googlers, voici une solution plug-and-play très simple qui a fonctionné pour moi après avoir lutté avec les solutions les plus complexes pendant un certain temps:

SELECT
distinct empName,
NewColumnName=STUFF((SELECT ','+ CONVERT(VARCHAR(10), projID ) 
                     FROM returns 
                     WHERE empName=t.empName FOR XML PATH('')) , 1 , 1 , '' )
FROM 
returns t

Notez que j'ai dû convertir l'ID en VARCHAR afin de le concaténer sous forme de chaîne. Si vous n'avez pas à le faire, voici une version encore plus simple:

SELECT
distinct empName,
NewColumnName=STUFF((SELECT ','+ projID
                     FROM returns 
                     WHERE empName=t.empName FOR XML PATH('')) , 1 , 1 , '' )
FROM 
returns t

Tout le mérite revient à ici: https://social.msdn.microsoft.com/Forums/sqlserver/en-US/9508abc2-46e7-4186-b57f-7f368374e084/replicating-groupconcat-function-of-mysql-in- sql-server? forum = transactsql

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.