Simulation der mySQL-Funktion group_concat in Microsoft SQL Server 2005?


347

Ich versuche, eine MySQL-basierte App auf Microsoft SQL Server 2005 zu migrieren (nicht nach Wahl, aber so ist das Leben).

In der ursprünglichen App haben wir fast ausschließlich ANSI-SQL-kompatible Anweisungen verwendet, mit einer wesentlichen Ausnahme: Wir haben die MySQL- group_concatFunktion ziemlich häufig verwendet.

group_concattut dies übrigens: eine Tabelle mit beispielsweise Mitarbeiternamen und Projekten ...

SELECT empName, projID FROM project_members;

kehrt zurück:

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

... und Folgendes erhalten Sie mit group_concat:

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

kehrt zurück:

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

Ich möchte also Folgendes wissen: Ist es möglich, beispielsweise eine benutzerdefinierte Funktion in SQL Server zu schreiben, die die Funktionalität von emuliert group_concat?

Ich habe fast keine Erfahrung mit UDFs, gespeicherten Prozeduren oder Ähnlichem, nur mit direktem SQL. Bitte irren Sie sich auf der Seite zu vieler Erklärungen :)



Dies ist eine alte Frage, aber ich mag die hier gegebene CLR-Lösung .
Diego

Mögliches Duplikat von Wie erstelle ich eine durch Kommas getrennte Liste mithilfe einer SQL-Abfrage? - Dieser Beitrag ist breiter, also würde ich diesen als kanonisch wählen
TMS


Woher wissen Sie, in welcher Reihenfolge die Liste erstellt werden soll, z. B. zeigen Sie A100 / B391 / X010 an, aber da in einer relationalen Datenbank keine implizite Reihenfolge vorliegt, kann es sich genauso gut um X010 / A100 / B391 oder eine andere Kombination handeln.
Steve Ford

Antworten:


174

Kein wirklich einfacher Weg, dies zu tun. Es gibt jedoch viele Ideen.

Das Beste, das ich gefunden habe :

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;

Oder eine Version, die korrekt funktioniert, wenn die Daten Zeichen wie enthalten können <

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
Dieses Beispiel hat bei mir funktioniert, aber ich habe versucht, eine andere Aggregation durchzuführen, und es hat nicht funktioniert. Ich habe einen Fehler erhalten: "Der Korrelationsname 'pre_trimmed' wird in einer FROM-Klausel mehrmals angegeben."
PhilChuang

7
'pre_trimmed' ist nur ein Alias ​​für die Unterabfrage. Aliase sind für Unterabfragen erforderlich und müssen eindeutig sein. Ändern Sie sie für eine andere Unterabfrage in etwas Einzigartiges ...
Koen

2
Können Sie ein Beispiel ohne Tabellennamen als Spaltennamen anzeigen, der verwirrend ist?
Mason

169

Ich bin vielleicht etwas zu spät zur Party, aber diese Methode funktioniert für mich und ist einfacher als die COALESCE-Methode.

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

1
Dies zeigt nur, wie Werte konkatiert werden - group_concat konatiert sie nach Gruppen, was eine größere Herausforderung darstellt (und was das OP anscheinend erfordert). Siehe die akzeptierte Antwort auf SO 15154644, wie dies zu tun ist - die WHERE-Klausel ist die kritische Ergänzung
DJDave


51

Möglicherweise zu spät, um jetzt von Nutzen zu sein, aber ist dies nicht der einfachste Weg, Dinge zu tun?

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

Interessant. Ich habe das vorliegende Projekt bereits abgeschlossen, aber ich werde diese Methode ausprobieren. Vielen Dank!
DanM

7
Netter Trick - das einzige Problem ist, dass bei Nachnamen mit Leerzeichen das Leerzeichen durch das Trennzeichen ersetzt wird.
Mark Elliot

Ich bin selbst auf ein solches Problem gestoßen, Mark. Bis MSSQL mit der Zeit geht und GROUP_CONCAT einführt, ist dies leider die am wenigsten Overhead-intensive Methode, die ich für das finden konnte, was hier benötigt wird.
J Hardiman

Danke dafür! Hier ist eine SQL-Geige, die zeigt, wie sie funktioniert: sqlfiddle.com/#!6/c5d56/3
floh

42

SQL Server 2017 führt eine neue Aggregatfunktion ein

STRING_AGG ( expression, separator).

Verkettet die Werte von Zeichenfolgenausdrücken und platziert Trennzeichen zwischen ihnen. Das Trennzeichen wird am Ende der Zeichenfolge nicht hinzugefügt.

Die verketteten Elemente können durch Anhängen sortiert werden WITHIN GROUP (ORDER BY some_expression)

Für die Versionen 2005-2016 verwende ich normalerweise die XML-Methode in der akzeptierten Antwort.

Dies kann jedoch unter bestimmten Umständen fehlschlagen. zB wenn die zu verkettenden Daten enthalten CHAR(29), sehen Sie

FOR XML konnte die Daten nicht serialisieren ... da sie ein Zeichen (0x001D) enthalten, das in XML nicht zulässig ist.

Eine robustere Methode, die mit allen Zeichen umgehen kann, wäre die Verwendung eines CLR-Aggregats. Bei diesem Ansatz ist es jedoch schwieriger, eine Reihenfolge auf die verketteten Elemente anzuwenden.

Die Zuordnung zu einer Variablen ist nicht garantiert und sollte im Produktionscode vermieden werden.


Dies ist jetzt auch in Azure SQL verfügbar: azure.microsoft.com/en-us/roadmap/…
Simon_Weaver

34

Schauen Sie sich das GROUP_CONCAT- Projekt auf Github an. Ich glaube, ich mache genau das, wonach Sie suchen:

Dieses Projekt enthält eine Reihe von benutzerdefinierten SQLCLR-Aggregatfunktionen (SQLCLR-UDAs), die zusammen ähnliche Funktionen wie die MySQL GROUP_CONCAT-Funktion bieten. Es gibt mehrere Funktionen, um die beste Leistung basierend auf den erforderlichen Funktionen sicherzustellen ...


2
@MaxiWheat: Viele Leute lesen Fragen oder Antworten nicht sorgfältig, bevor sie auf "Abstimmen" klicken. Dies wirkt sich direkt auf den Beitrag des Eigentümers aufgrund seines Fehlers aus.
Steve Lam

Funktioniert super. Die einzige Funktion, die mir fehlt, ist die Möglichkeit, nach einer Spalte zu sortieren, die MySQL group_concat () gefallen kann:GROUP_CONCAT(klascode,'(',name,')' ORDER BY klascode ASC SEPARATOR ', ')
Januar

10

So verketten Sie alle Projektmanagernamen aus Projekten mit mehreren Projektmanagern:

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

Mit dem folgenden Code müssen Sie PermissionLevel = External für Ihre Projekteigenschaften vor der Bereitstellung festlegen und die Datenbank so ändern, dass sie externem Code vertraut (lesen Sie an anderer Stelle Informationen zu Sicherheitsrisiken und Alternativen [wie Zertifikaten]), indem Sie "ALTER DATABASE database_name SET" ausführen VERTRAUENSWERT ".

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);
 }
    }

Ich habe dies mit einer Abfrage getestet, die wie folgt aussieht:

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 

Und ergibt: A, B, C, D.


9

Versuchte diese, aber für meine Zwecke in MS SQL Server 2005 war das Folgende am nützlichsten, das ich bei xaprb gefunden habe

declare @result varchar(8000);

set @result = '';

select @result = @result + name + ' '

from master.dbo.systypes;

select rtrim(@result);

@Mark wie du erwähnt hast, war es der Space-Charakter, der mir Probleme bereitete.


Ich denke, dass die Engine mit dieser Methode keine Reihenfolge wirklich garantiert, da die Variablen je nach Ausführungsplan als Datenfluss berechnet werden. Bisher scheint es jedoch die meiste Zeit zu funktionieren.
phil_w

6

Über J Hardimans Antwort, wie wäre es mit:

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

Ist die Verwendung von "Nachname" übrigens ein Tippfehler oder verstehe ich hier kein Konzept?

Wie auch immer, vielen Dank, denn es hat mir ziemlich viel Zeit gespart :)


1
Eher unfreundliche Antwort, wenn Sie mich fragen und als Antwort überhaupt nicht hilfreich.
Tim Meers

1
Ich sehe das jetzt nur ... Ich habe es nicht gemein gemeint, zu der Zeit war ich sehr frustriert mit SQL Server (bin es immer noch). Die Antworten aus diesem Beitrag waren wirklich hilfreich. EDIT: Warum war es übrigens nicht hilfreich? es hat den Trick für mich
getan

1

Für meine Googlerkollegen gibt es hier eine sehr einfache Plug-and-Play-Lösung, die für mich funktioniert hat, nachdem ich eine Weile mit den komplexeren Lösungen zu kämpfen hatte:

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

Beachten Sie, dass ich die ID in eine VARCHAR konvertieren musste, um sie als Zeichenfolge zu verketten. Wenn Sie das nicht tun müssen, ist hier eine noch einfachere Version:

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

Alle Gutschriften hierfür finden Sie hier: https://social.msdn.microsoft.com/Forums/sqlserver/en-US/9508abc2-46e7-4186-b57f-7f368374e084/replicating-groupconcat-function-of-mysql-in- sql-server? forum = transactsql

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.