Моделювання функції MySQL group_concat в Microsoft SQL Server 2005?


347

Я намагаюся перенести додаток на базі MySQL на Microsoft SQL Server 2005 (не за вибором, але це життя).

У оригінальному додатку ми майже повністю використовували твердження, сумісні з ANSI-SQL, за одним істотним винятком - group_concatфункцію MySQL ми використовували досить часто.

group_concatДо речі, робить це так: дається таблиця, скажімо, імен працівників та проектів ...

SELECT empName, projID FROM project_members;

повертає:

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

... і ось що ви отримуєте з group_concat:

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

повертає:

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

Тож, що я хотів би знати: це можливо написати, скажімо, визначену користувачем функцію на SQL Server, яка імітує функціональність group_concat?

У мене майже немає досвіду використання UDF, збережених процедур або чогось подібного, просто прямого використання SQL, тому, будь ласка, помиляйтесь на стороні занадто великого пояснення :)



Це старе запитання, але мені подобається дане тут рішення CLR .
Дієго

можливий дублікат Як я можу створити розділений комами список за допомогою SQL-запиту? - ця посада ширша, тому я вибрав би її як канонічну
TMS

можливий дублікат функції SQL group_concat в SQL Server
Trikaldarshi

Звідки ви знаєте, до якого порядку повинен бути побудований список, наприклад, ви показуєте A100 / B391 / X010, але, враховуючи, що в реляційній базі даних немає неявного впорядкування, це може бути так само легко X010 / A100 / B391 або будь-яка інша комбінація.
Стів Форд

Відповіді:


174

Немає РЕАЛЬНОГО простого способу зробити це. Дуже багато ідей там.

Найкращий, який я знайшов :

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;

Або версія, яка працює правильно, якщо дані можуть містити символи, такі як <

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
Цей приклад працював для мене, але я спробував зробити ще одну агрегацію, і вона не спрацювала, і принесла мені помилку: "ім'я кореляції 'pre_trimmed' вказується кілька разів у пункті FROM."
PhilChuang

7
'pre_trimmed' - це лише псевдонім для підзапиту. Псевдоніми потрібні для підзапитів і повинні бути унікальними, щоб для іншого підзапросу змінити його на щось унікальне ...
Koen

2
чи можна показати приклад без таблиці_імен як імені стовпця, що це заплутано.
S.Mason

169

Я можу трохи запізнитися на вечірку, але цей метод працює для мене і простіше, ніж метод COALESCE.

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

1
Це показує лише, як конкретизувати значення - group_concat конспектує їх по групах, що є складнішим (і те, що, здається, вимагає ОП). Дивіться прийняту відповідь на SO 15154644 про те, як це зробити - пункт WHERE є критичним доповненням
DJDave


51

Можливо, вже пізно, щоб зараз принести користь, але хіба це не найпростіший спосіб вчинити?

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

Цікаво. Я вже закінчив проект, але спробую цей метод. Дякую!
DanM

7
Хороший трюк - проблема лише в прізвищах з пробілами, вона замінить пробіл роздільником.
Марк Елліот

Я сам зіткнувся з такою проблемою, Марку. На жаль, доки MSSQL не з часом і не запровадить GROUP_CONCAT, це найменший з накладних інтенсивних методів, які мені вдалося придумати для того, що тут потрібно.
Дж. Хардіман,

Дякую за це! Ось SQL Fiddle, що показує його роботу: sqlfiddle.com/#!6/c5d56/3
втікав

42

SQL Server 2017 вводить нову сукупну функцію

STRING_AGG ( expression, separator).

З’єднує значення виразів рядків і розміщує між ними значення роздільника. Роздільник не додається в кінці рядка.

З’єднані елементи можна упорядкувати додаванням WITHIN GROUP (ORDER BY some_expression)

Для версій 2005-2016 років я зазвичай використовую метод XML у прийнятій відповіді.

Однак, за деяких обставин це може бути невдалим. наприклад, якщо дані, що підлягають об'єднанню, містять CHAR(29)ви бачите

ДЛЯ XML не вдалося серіалізувати дані ... тому що він містить символ (0x001D), який не дозволений у XML.

Більш надійним методом, який може мати справу з усіма символами, буде використання агрегату CLR. Однак застосувати замовлення до з'єднаних елементів складніше при такому підході.

Метод присвоєння змінної не гарантується та слід уникати у виробничому коді.


Це також доступне зараз у Azure SQL: azure.microsoft.com/en-us/roadmap/…
Simon_Weaver

34

Погляньте на проект GROUP_CONCAT на Github, я думаю, я роблю саме те, що ви шукаєте:

Цей проект містить набір визначених користувачем функцій сукупності (SQLCLR UDA), які колективно пропонують аналогічні функції функції MySQL GROUP_CONCAT. Існує кілька функцій для забезпечення найкращої продуктивності на основі необхідної функціональності ...


2
@MaxiWheat: багато хлопців не читають запитання і не відповідають уважно перед тим, як натиснути голос. Це впливає на повідомлення власника безпосередньо через їх помилку.
Стів Лам

Чудово працює. Єдиною особливістю, яку мені не вистачає, є можливість сортування стовпця, який може сподобатися MySQL group_concat ():GROUP_CONCAT(klascode,'(',name,')' ORDER BY klascode ASC SEPARATOR ', ')
січня

10

Щоб об'єднати всі імена менеджера проектів з проектами, у яких є кілька керівників проектів, запишіть:

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

За допомогою наведеного нижче коду вам слід встановити PermissionLevel = Зовнішній для властивостей вашого проекту перед тим, як розгорнути, і змінити базу даних на довірчий зовнішній код (не забудьте прочитати в іншому місці про ризики безпеки та альтернативи [як сертифікати]), запустивши "ALTER DATABASE database_name SET ДОСТУПНІСТЬ ВКЛ. "

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

Я перевірив це за допомогою запиту, який виглядає так:

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 

І врожайність: A, B, C, D


9

Спробував це, але для моїх цілей у MS SQL Server 2005 найкориснішим було наступне, що я знайшов у xaprb

declare @result varchar(8000);

set @result = '';

select @result = @result + name + ' '

from master.dbo.systypes;

select rtrim(@result);

@ Марк, як ви згадали, саме космічний персонаж викликав у мене проблеми.


Я думаю, що двигун насправді не гарантує жодного замовлення з цим методом, тому що змінні обчислюються як потоки даних залежно від плану виконання. Здається, це працює більшу частину часу поки.
phil_w

6

Про відповідь Дж. Хардімана, як щодо:

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

До речі, чи є "Фамілія" друкарською помилкою чи я тут не розумію поняття?

У будь-якому випадку, дякую, хлопці, адже це врятувало мене досить багато часу :)


1
Швидше недобросовісна відповідь, якщо ви запитаєте мене і зовсім не корисна як відповідь.
Тім Меерс

1
бачачи це зараз ... Я не мав на увазі це середньою мірою, в той час я був дуже розчарований сервером sql (все ще є). відповіді з цієї публікації насправді були корисними; EDIT: чому це не було корисним btw? це зробило трюк для мене
користувач422190

1

Для моїх колег Googlers там, ось дуже просте рішення підключення та роботи, яке працювало для мене після того, як деякий час боролися зі складнішими рішеннями:

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

Зауважте, що мені довелося перетворити ідентифікатор у VARCHAR, щоб об'єднати його як рядок. Якщо вам цього не потрібно, ось ще простіша версія:

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

Вся заслуга в цьому - тут: https://social.msdn.microsoft.com/Forums/sqlserver/en-US/9508abc2-46e7-4186-b57f-7f368374e084/replicating-groupconcat-function-of-mysql-in- sql-сервер? forum = transactsql

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.