Mensimulasikan fungsi group_concat MySQL di Microsoft SQL Server 2005?

347

Saya mencoba untuk memigrasi aplikasi berbasis MySQL ke Microsoft SQL Server 2005 (bukan karena pilihan, tapi itu hidup).

Dalam aplikasi asli, kami menggunakan hampir semua pernyataan yang sesuai dengan ANSI-SQL, dengan satu pengecualian signifikan - kami menggunakan group_concatfungsi MySQL cukup sering.

group_concat, omong-omong, apakah ini: diberi tabel, katakanlah, nama dan proyek karyawan ...

SELECT empName, projID FROM project_members;

pengembalian:

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

... dan inilah yang Anda dapatkan dengan group_concat:

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

pengembalian:

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

Jadi yang ingin saya ketahui adalah: Apakah mungkin untuk menulis, katakanlah, fungsi yang didefinisikan pengguna dalam SQL Server yang mengemulasi fungsionalitas group_concat?

Saya hampir tidak punya pengalaman menggunakan UDF, prosedur tersimpan, atau semacamnya, langsung saja SQL, jadi tolong sesat di sisi terlalu banyak penjelasan :)

DanM
sumber
Ini adalah pertanyaan lama, tapi saya suka solusi CLR yang diberikan di sini .
Diego
kemungkinan duplikat dari Bagaimana cara Membuat Daftar yang Dipisahkan dengan Koma menggunakan SQL Query? - Posting itu lebih luas jadi saya akan memilih yang sebagai kanonik
TMS
kemungkinan duplikat fungsi SQL group_concat di SQL Server
Trikaldarshi
Bagaimana Anda tahu urutan daftar harus dibangun, misalnya Anda menunjukkan A100 / B391 / X010 tetapi mengingat tidak ada pemesanan tersirat dalam database relasional itu bisa dengan mudah menjadi X010 / A100 / B391 atau kombinasi lainnya.
Steve Ford

Jawaban:

174

Tidak ada cara NYATA yang mudah untuk melakukan ini. Banyak ide di luar sana.

Yang terbaik yang saya temukan :

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;

Atau versi yang berfungsi dengan benar jika data mungkin berisi karakter seperti <

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) 
BradC
sumber
1
Contoh ini berhasil untuk saya, tetapi saya mencoba melakukan agregasi lain dan tidak berhasil, memberi saya kesalahan: "nama korelasi 'pre_trimmed' ditentukan beberapa kali dalam klausa FROM."
PhilChuang
7
'pre_trimmed' hanyalah alias untuk subquery. Alias ​​diperlukan untuk subkueri dan harus unik, jadi untuk subkueri lain ubahlah menjadi sesuatu yang unik ...
Koen
2
dapatkah Anda menunjukkan contoh tanpa nama_kabel sebagai nama kolom yang membingungkan.
S.Mason
169

Saya mungkin agak terlambat ke pesta tetapi metode ini bekerja untuk saya dan lebih mudah daripada metode COALESCE.

SELECT STUFF(
             (SELECT ',' + Column_Name 
              FROM Table_Name
              FOR XML PATH (''))
             , 1, 1, '')
Scott
sumber
1
Ini hanya menunjukkan cara menggabungkan nilai - group_concat menggabungkannya menurut kelompok, yang lebih menantang (dan apa yang dibutuhkan OP). Lihat jawaban yang diterima untuk SO 15154644 untuk bagaimana melakukan ini - klausa WHERE adalah tambahan penting
DJDave
@DJDave merujuk pada jawaban ini . Lihat juga jawaban yang diterima untuk pertanyaan serupa .
John Cummings
51

Mungkin sudah terlambat untuk mendapatkan manfaat sekarang, tetapi bukankah ini cara termudah untuk melakukan sesuatu?

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
J Hardiman
sumber
Menarik. Saya sudah menyelesaikan proyek ini, tetapi saya akan mencoba metode ini. Terima kasih!
DanM
7
Trik yang bagus - satu-satunya masalah adalah nama keluarga dengan spasi yang akan menggantikan ruang dengan pemisah.
Mark Elliot
Saya sendiri pernah mengalami masalah seperti itu, Mark. Sayangnya, sampai MSSQL mendapatkan waktu dan memperkenalkan GROUP_CONCAT, ini adalah yang paling sedikit dari metode overhead-intensif yang saya dapat buat untuk apa yang diperlukan di sini.
J Hardiman
Terima kasih untuk ini! Berikut ini SQL Fiddle yang menunjukkan bahwa ia berfungsi: sqlfiddle.com/#!6/c5d56/3
melarikan diri
42

SQL Server 2017 memperkenalkan fungsi agregat baru

STRING_AGG ( expression, separator).

Menggabungkan nilai ekspresi string dan menempatkan nilai pemisah di antara mereka. Pemisah tidak ditambahkan pada akhir string.

Elemen gabungan dapat dipesan dengan menambahkan WITHIN GROUP (ORDER BY some_expression)

Untuk versi 2005-2016 saya biasanya menggunakan metode XML dalam jawaban yang diterima.

Namun ini bisa gagal dalam beberapa keadaan. mis. jika data yang akan digabungkan berisi CHAR(29)Anda lihat

UNTUK XML tidak dapat mengelompokkan data ... karena berisi karakter (0x001D) yang tidak diizinkan dalam XML.

Metode yang lebih kuat yang dapat menangani semua karakter adalah dengan menggunakan agregat CLR. Namun menerapkan pemesanan pada elemen gabungan lebih sulit dengan pendekatan ini.

Metode penetapan ke variabel tidak dijamin dan harus dihindari dalam kode produksi.

Martin Smith
sumber
Ini juga tersedia sekarang di Azure SQL: azure.microsoft.com/en-us/roadmap/...
Simon_Weaver
34

Silahkan lihat di GROUP_CONCAT proyek pada Github, saya pikir saya tidak persis apa yang Anda cari:

Proyek ini berisi sekumpulan fungsi Agregat yang ditentukan pengguna SQLCLR (SQLCLR UDAs) yang secara kolektif menawarkan fungsionalitas yang mirip dengan fungsi MySQL GROUP_CONCAT. Ada beberapa fungsi untuk memastikan kinerja terbaik berdasarkan fungsionalitas yang diperlukan ...

MaxiWheat
sumber
2
@ MaxiWheat: banyak orang tidak membaca pertanyaan atau menjawab dengan hati-hati sebelum mengklik suara. Ini mempengaruhi posting pemilik secara langsung karena kesalahan mereka.
Steve Lam
Bagus sekali. Satu-satunya fitur yang saya lewatkan adalah kemampuan untuk mengurutkan pada kolom yang dapat disukai oleh MySQL group_concat ():GROUP_CONCAT(klascode,'(',name,')' ORDER BY klascode ASC SEPARATOR ', ')
Jan
10

Untuk menggabungkan semua nama manajer proyek dari proyek yang memiliki beberapa manajer proyek, tulis:

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
Cmaly
sumber
9

Dengan kode di bawah ini Anda harus mengatur PermissionLevel = Eksternal pada properti proyek Anda sebelum Anda menyebarkan, dan mengubah database untuk mempercayai kode eksternal (pastikan untuk membaca di tempat lain tentang risiko keamanan dan alternatif [seperti sertifikat]) dengan menjalankan "ALTER DATABASE database_name SET TERPERCAYA DIRI ".

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

Saya telah menguji ini menggunakan kueri yang terlihat seperti:

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 

Dan hasil: A, B, C, D

GregTSmith
sumber
9

Mencoba ini tetapi untuk keperluan saya di MS SQL Server 2005 berikut ini yang paling berguna, yang saya temukan di xaprb

declare @result varchar(8000);

set @result = '';

select @result = @result + name + ' '

from master.dbo.systypes;

select rtrim(@result);

@ Markus seperti yang Anda sebutkan itu adalah karakter ruang yang menyebabkan masalah bagi saya.

isoughtajam
sumber
Saya berpikir bahwa mesin tidak benar-benar menjamin pesanan apa pun dengan metode ini, karena variabel dihitung sebagai aliran data tergantung pada rencana exec. Sepertinya ini berfungsi sebagian besar waktu sejauh ini.
phil_w
6

Tentang jawaban J Hardiman, bagaimana:

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

Omong-omong, apakah penggunaan "Nama Keluarga" salah ketik atau apakah saya tidak memahami konsep di sini?

Ngomong-ngomong, terima kasih banyak teman-teman cuz itu menyelamatkan saya cukup lama :)

user422190
sumber
1
Jawaban yang agak tidak ramah jika Anda bertanya kepada saya dan sama sekali tidak membantu sebagai jawaban.
Tim Meers
1
hanya melihat itu sekarang ... Saya tidak bermaksud dengan cara yang jahat, pada saat itu saya sangat frustrasi dengan server sql (masih ada). jawaban dari posting ini sebenarnya sangat membantu; EDIT: mengapa itu tidak membantu btw? itu melakukan trik untuk saya
user422190
1

Untuk rekan Google saya di luar sana, inilah solusi plug-and-play yang sangat sederhana yang bekerja untuk saya setelah berjuang dengan solusi yang lebih kompleks untuk sementara waktu:

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

Perhatikan bahwa saya harus mengonversi ID menjadi VARCHAR untuk menggabungkannya sebagai string. Jika Anda tidak harus melakukan itu, ini versi yang lebih sederhana:

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

Semua kredit untuk ini masuk ke sini: https://social.msdn.microsoft.com/Forums/sqlserver/en-US/9508abc2-46e7-4186-b57f-7f368374e084/replicating-groupconcat-function-of-mysql-in- sql-server? forum = transactsql

Krock
sumber