Имитация функции MySQL group_concat в Microsoft SQL Server 2005?

Я пытаюсь перенести приложение на основе MySQL на Microsoft SQL Server 2005 (не по собственному выбору, но такова жизнь).

В исходном приложении мы использовали операторы почти, полностью совместимые с ANSI-SQL, с одним существенным исключением - мы довольно часто использовали функцию MySQL group_concat.

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, поэтому, пожалуйста, ошибитесь в сторону слишком подробных объяснений :)

возможный дубликат Как создать список, разделенный запятыми, с помощью SQL-запроса? - этот пост шире, поэтому я бы выбрал его как канонический

Innate Imunity is The Way 03.08.2013 10:43

возможный дубликат Функция SQL group_concat в SQL Server

Trikaldarshiii 14.08.2013 23:23

Как узнать, в каком порядке должен быть построен список, например вы показываете A100 / B391 / X010, но, учитывая, что в реляционной базе данных нет неявного упорядочивания, это может быть X010 / A100 / B391 или любая другая комбинация.

Steve Ford 20.03.2014 20:09

Это старый вопрос, но мне нравится решение CLR, приведенное здесь.

Diego 20.03.2012 05:54

дубликат stackoverflow.com/questions/273238/…

Salman A 04.02.2018 21:20

Возможный дубликат Как использовать GROUP BY для объединения строк в SQL Server?

Salman A 04.02.2018 21:21
ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
350
7
178 948
11
Перейти к ответу Данный вопрос помечен как решенный

Ответы 11

Ответ принят как подходящий

Нет НАСТОЯЩЕГО простого способа сделать это. Тем не менее, есть много идей.

Лучшее, что я нашел:

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) 

Этот пример сработал для меня, но я попытался выполнить еще одну агрегацию, и это не сработало, я получил ошибку: «имя корреляции 'pre_trimmed' указано несколько раз в предложении FROM».

PhilChuang 22.01.2010 22:24

pre_trimmed - это просто псевдоним для подзапроса. Псевдонимы необходимы для подзапросов и должны быть уникальными, поэтому для другого подзапроса измените его на что-то уникальное ...

Koen 12.03.2012 20:57

Можете ли вы показать пример без table_name в качестве имени столбца, это сбивает с толку.

S.Mason 18.11.2017 00:47

С помощью приведенного ниже кода вы должны установить PermissionLevel = External в свойствах вашего проекта перед развертыванием и изменить базу данных, чтобы доверять внешнему коду (обязательно прочитайте в другом месте о рисках безопасности и альтернативах [например, сертификатах]), запустив команду 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

Возможно, уже слишком поздно, чтобы приносить пользу, но разве это не самый простой способ сделать что-то?

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 25.02.2010 19:59

Хороший трюк - проблема только в том, что для фамилий с пробелами пробел заменяется разделителем.

Mark Elliot 14.07.2010 20:16

Я сам столкнулся с такой проблемой, Марк. К сожалению, пока MSSQL не успеет за временем и не представит GROUP_CONCAT, это наименее затратный метод, который я смог придумать для того, что здесь необходимо.

J Hardiman 26.07.2010 05:40

Спасибо за это! Вот скрипт SQL, показывающий, что он работает: sqlfiddle.com/#!6/c5d56/3

fleed 25.09.2015 13:30

Об ответе Дж. Хардимана, как насчет:

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

Кстати, использование «Фамилии» - это опечатка, или я не понимаю концепцию здесь?

В любом случае, большое спасибо, ребята, потому что это сэкономило мне довольно много времени :)

Скорее недружелюбный ответ, если вы спросите меня, и совершенно бесполезный ответ.

Tim Meers 20.03.2012 06:04

только сейчас вижу это ... Я не имел в виду это подло, в то время я был очень разочарован sql-сервером (до сих пор). ответы из этого поста действительно были полезны; Обновлено: почему это не помогло, кстати? это помогло мне

user422190 04.06.2013 19:43

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

declare @result varchar(8000);

set @result = '';

select @result = @result + name + ' '

from master.dbo.systypes;

select rtrim(@result);

@Mark, как вы упомянули, у меня проблемы с пробелом.

Я думаю, что движок на самом деле не гарантирует какой-либо порядок с этим методом, потому что переменные вычисляются как потоки данных в зависимости от плана выполнения. Хотя, похоже, пока что работает большую часть времени.

phil_w 26.03.2019 22:58

Я могу немного опоздать на вечеринку, но этот метод работает для меня и проще, чем метод COALESCE.

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

Это только показывает, как объединять значения - group_concat объединяет их по группам, что более сложно (и то, что, по-видимому, требует OP). См. Принятый ответ на SO 15154644, чтобы узнать, как это сделать - предложение WHERE является важным дополнением.

DJDave 11.01.2018 16:26

@DJDave имел в виду этот ответ. См. Также принятый ответ на аналогичный вопрос.

John Cummings 27.02.2019 23:19

Чтобы объединить все имена менеджеров проектов из проектов, в которых есть несколько менеджеров, напишите:

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

Взгляните на проект GROUP_CONCAT на Github, думаю, я делаю именно то, что вы ищете:

This project contains a set of SQLCLR User-defined Aggregate functions (SQLCLR UDAs) that collectively offer similar functionality to the MySQL GROUP_CONCAT function. There are multiple functions to ensure the best performance based on the functionality required...

@MaxiWheat: многие ребята не читают внимательно вопросы или ответы, прежде чем нажать голосование против. Это влияет на сообщение владельца напрямую из-за его ошибки.

Steve Lam 30.10.2015 11:17

Работает отлично. Единственная функция, которую мне не хватает, - это возможность сортировки по столбцу, который может понравиться MySQL group_concat (): GROUP_CONCAT(klascode,'(',name,')' ORDER BY klascode ASC SEPARATOR ', ')

Jan 31.10.2018 00:33

SQL Server 2017 г. вводит новую агрегатную функцию

STRING_AGG ( expression, separator).

Concatenates the values of string expressions and places separator values between them. The separator is not added at the end of string.

Объединенные элементы можно упорядочить, добавив WITHIN GROUP (ORDER BY some_expression)

Для версий 2005-2016 Я обычно использую метод XML в принятом ответе.

Однако в некоторых случаях это может не сработать. например если объединяемые данные содержат CHAR(29), вы увидите

FOR XML could not serialize the data ... because it contains a character (0x001D) which is not allowed in XML.

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

Метод присвоения переменной - не гарантировано, и его следует избегать в производственном коде.

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

Simon_Weaver 14.09.2017 07:01

Для моих коллег из Google, вот очень простое решение plug-and-play, которое сработало для меня после того, как некоторое время боролся с более сложными решениями:

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-server?forum=transactsql

ОБНОВЛЕНИЕ 2020: SQL Server 2016+JSON Примеры сериализации и десериализации

Данные, предоставленные OP, вставлены во временную таблицу с именем #project_members

drop table if exists #project_members;
create table #project_members(
  empName        varchar(20) not null,
  projID         varchar(20) not null);
go
insert #project_members(empName, projID) values
('ANDY', 'A100'),
('ANDY', 'B391'),
('ANDY', 'X010'),
('TOM', 'A100'),
('TOM', 'A510');

Как сериализовать эти данные в одну строку JSON с вложенным массивом, содержащим идентификаторы projID

select empName, (select pm_json.projID 
                 from #project_members pm_json 
                 where pm.empName=pm_json.empName 
                 for json path, root('projList')) projJSON
from #project_members pm
group by empName
for json path;

Результат

'[
  {
    "empName": "ANDY",
    "projJSON": {
      "projList": [
        { "projID": "A100" },
        { "projID": "B391" },
        { "projID": "X010" }
      ]
    }
  },
  {
    "empName": "TOM",
    "projJSON": {
      "projList": [
        { "projID": "A100" },
        { "projID": "A510" }
      ]
    }
  }
]'

Как десериализовать эти данные из одной строки JSON обратно в исходные строки и столбцы

declare @json           nvarchar(max)=N'[{"empName":"ANDY","projJSON":{"projList":[{"projID":"A100"},
                                         {"projID":"B391"},{"projID":"X010"}]}},{"empName":"TOM","projJSON":
                                         {"projList":[{"projID":"A100"},{"projID":"A510"}]}}]';

select oj.empName, noj.projID 
from openjson(@json) with (empName        varchar(20),
                           projJSON       nvarchar(max) as json) oj
     cross apply openjson(oj.projJSON, '$.projList') with (projID    varchar(20)) noj;

Полученные результаты

empName projID
ANDY    A100
ANDY    B391
ANDY    X010
TOM     A100
TOM     A510

Как сохранить уникальное имя empName в таблице и сохранить projID во вложенном массиве JSON

drop table if exists #project_members_with_json;
create table #project_members_with_json(
  empName        varchar(20) unique not null,
  projJSON       nvarchar(max) not null);
go
insert #project_members_with_json(empName, projJSON) 
select empName, (select pm_json.projID 
                 from #project_members pm_json 
                 where pm.empName=pm_json.empName 
                 for json path, root('projList')) 
from #project_members pm
group by empName;

Полученные результаты

empName projJSON
ANDY    {"projList":[{"projID":"A100"},{"projID":"B391"},{"projID":"X010"}]}
TOM     {"projList":[{"projID":"A100"},{"projID":"A510"}]}

Как десериализовать из таблицы с уникальным empName и вложенным столбцом массива JSON, содержащим projID

select wj.empName, oj.projID
from
  #project_members_with_json wj
 cross apply
  openjson(wj.projJSON, '$.projList') with (projID    varchar(20)) oj;

Полученные результаты

empName projID
ANDY    A100
ANDY    B391
ANDY    X010
TOM     A100
TOM     A510

Честно говоря, не понимаю, какое отношение это имеет к заданному вопросу. OP очень конкретно спросил о моделировании поведения MySQL GROUP_CONCAT. Строка, которую производит GROUP_CONCAT, представляет собой просто список значений, разделенных разделителем. Строка в формате JSON - это гораздо больше.

Andriy M 04.09.2020 17:27

Другие вопросы по теме