web-dev-qa-db-fra.com

Simulation de la fonction MySQL group_concat dans Microsoft SQL Server 2005?

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

Dans l'application d'origine, nous utilisions presque entièrement des instructions conformes à ANSI-SQL, à une exception près - nous utilisions assez souvent la fonction group_concat de MySQL.

group_concat à propos, fait ceci: à partir d'un tableau avec, disons, les noms des employés et les projets ...

SELECT empName, projID FROM project_members;

résultats:

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;

résultats:

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

Ce que j'aimerais savoir, c'est: Est-il possible d'écrire une fonction définie par l'utilisateur dans SQL Server qui émule la fonctionnalité de group_concat?

Je n'ai presque aucune expérience de l'utilisation de fonctions définies par l'utilisateur, de procédures stockées ou de tout autre chose du genre, juste du SQL direct, alors veuillez vous tromper du côté des explications excessives :)

338
DanM

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) 
167
BradC

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

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

Peut-être trop tard pour en tirer profit maintenant, mais n'est-ce pas la meilleure façon 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
48
J Hardiman

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

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 toutefois é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 pouvant traiter tous les caractères consisterait à utiliser un agrégat CLR. Cependant, appliquer une commande aux éléments concaténés est plus difficile avec cette approche.

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

41
Martin Smith

Regardez le 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 SQLCLR définies par l'utilisateur (UQ SQLCLR) qui offrent collectivement des fonctionnalités similaires à la fonction MySQL GROUP_CONCAT. Il existe plusieurs fonctions pour assurer les meilleures performances en fonction de la fonctionnalité requise ...

34
MaxiWheat

Pour concaténer tous les noms de gestionnaires de projets de projets ayant plusieurs gestionnaires de projets, é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
10
Cmaly

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 faire confiance au code externe (assurez-vous de lire ailleurs sur les risques de sécurité et les alternatives [comme les certificats]) en exécutant "ALTER DATABASE nom_base_données 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
GregTSmith

Essayé ces mais pour mes besoins dans MS SQL Server 2005 ce qui suit était le plus utile, que j'ai trouvé à 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 d'espace qui m'a causé des problèmes.

9
isoughtajam

A 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

À propos, est-ce que l'utilisation de "Nom de famille" est une faute de frappe ou est-ce que je ne comprends pas un concept ici?

Quoi qu'il en soit, merci beaucoup les gars parce que ça m'a fait gagner du temps :)

6
user422190

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

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 je devais convertir l'ID en VARCHAR afin de le concaténer en tant que 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

Tous les crédits pour cela vont 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

0
krock