¿Simulando la función group_concat MySQL en Microsoft SQL Server 2005?

347

Estoy tratando de migrar una aplicación basada en MySQL a Microsoft SQL Server 2005 (no por elección, pero así es la vida).

En la aplicación original, utilizamos casi completamente declaraciones compatibles con ANSI-SQL, con una excepción significativa: utilizamos la group_concatfunción de MySQL con bastante frecuencia.

group_concat, por cierto, hace esto: dada una tabla de, por ejemplo, nombres de empleados y proyectos ...

SELECT empName, projID FROM project_members;

devoluciones:

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

... y esto es lo que obtienes con group_concat:

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

devoluciones:

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

Entonces, lo que me gustaría saber es: ¿es posible escribir, por ejemplo, una función definida por el usuario en SQL Server que emule la funcionalidad de group_concat?

Casi no tengo experiencia en el uso de UDF, procedimientos almacenados o algo por el estilo, solo SQL directo, así que por favor cometa demasiadas explicaciones :)

DanM
fuente
Esta es una vieja pregunta, pero me gusta la solución CLR que se ofrece aquí .
Diego
posible duplicado de ¿Cómo creo una lista separada por comas usando una consulta SQL? - esa publicación es más amplia, así que elegiría esa como canónica
TMS
posible duplicado de la función SQL group_concat en SQL Server
Trikaldarshi
¿Cómo saber en qué orden se debe construir la lista? Por ejemplo, muestra A100 / B391 / X010 pero dado que no hay un pedido implícito en una base de datos relacional, podría ser X010 / A100 / B391 o cualquier otra combinación.
Steve Ford

Respuestas:

174

No hay una manera REAL de hacer esto. Sin embargo, hay muchas ideas por ahí.

El mejor que he encontrado :

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;

O una versión que funciona correctamente si los datos pueden contener caracteres como <

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
fuente
1
Este ejemplo funcionó para mí, pero intenté hacer otra agregación y no funcionó, me dio un error: "el nombre de correlación 'pre_trimmed' se especifica varias veces en una cláusula FROM".
PhilChuang
77
'pre_trimmed' es solo un alias para la subconsulta. Los alias son necesarios para las subconsultas y deben ser únicos, por lo que para otra subconsulta cámbielo a algo único ...
Koen
2
¿puede mostrar un ejemplo sin nombre_tabla como nombre de columna? Es confuso.
S.Mason
169

Puede que llegue un poco tarde a la fiesta, pero este método funciona para mí y es más fácil que el método COALESCE.

SELECT STUFF(
             (SELECT ',' + Column_Name 
              FROM Table_Name
              FOR XML PATH (''))
             , 1, 1, '')
Scott
fuente
1
Esto solo muestra cómo concatenar valores: group_concat los concatena por grupo, lo que es más desafiante (y lo que parece requerir el OP). Vea la respuesta aceptada a SO 15154644 para saber cómo hacer esto: la cláusula WHERE es la adición crítica
DJDave el
@DJDave se refería a esta respuesta . Vea también la respuesta aceptada a una pregunta similar .
John Cummings
51

Posiblemente demasiado tarde para ser beneficioso ahora, pero ¿no es esta la forma más fácil de hacer las cosas?

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
fuente
Interesante. Ya he terminado el proyecto en cuestión, pero probaré este método. ¡Gracias!
DanM
77
Buen truco: el único problema es que los apellidos con espacios reemplazarán el espacio con el separador.
Mark Elliot
Me he encontrado con ese problema, Mark. Desafortunadamente, hasta que MSSQL llegue con los tiempos y presente GROUP_CONCAT, este es el menor de los métodos intensivos en sobrecarga que he podido encontrar para lo que se necesita aquí.
J Hardiman
¡Gracias por esto! Aquí hay un Fiddle de SQL que lo muestra funcionando: sqlfiddle.com/#!6/c5d56/3
huyó el
42

SQL Server 2017 introduce una nueva función agregada

STRING_AGG ( expression, separator).

Concatena los valores de las expresiones de cadena y coloca valores separadores entre ellas. El separador no se agrega al final de la cadena.

Los elementos concatenados se pueden ordenar agregando WITHIN GROUP (ORDER BY some_expression)

Para las versiones 2005-2016 , generalmente uso el método XML en la respuesta aceptada.

Sin embargo, esto puede fallar en algunas circunstancias. por ejemplo, si los datos que se concatenarán contienen CHAR(29), verá

FOR XML no pudo serializar los datos ... porque contiene un carácter (0x001D) que no está permitido en XML.

Un método más robusto que pueda manejar todos los caracteres sería usar un agregado CLR. Sin embargo, aplicar un orden a los elementos concatenados es más difícil con este enfoque.

El método de asignación a una variable no está garantizado y debe evitarse en el código de producción.

Martin Smith
fuente
Esto también está disponible ahora en Azure SQL: azure.microsoft.com/en-us/roadmap/…
Simon_Weaver
34

Echa un vistazo al proyecto GROUP_CONCAT en Github, creo que hago exactamente lo que estás buscando:

Este proyecto contiene un conjunto de funciones agregadas definidas por el usuario SQLCLR (UDA SQLCLR) que colectivamente ofrecen una funcionalidad similar a la función GROUP_CONCAT de MySQL. Existen múltiples funciones para garantizar el mejor rendimiento en función de la funcionalidad requerida ...

MaxiWheat
fuente
2
@MaxiWheat: muchos chicos no leen las preguntas o respuestas cuidadosamente antes de hacer clic en votar. Afecta a la publicación del propietario directamente debido a su error.
Steve Lam
Funciona genial. La única característica que me falta es la capacidad de ordenar en una columna que le gusta a MySQL group_concat ():GROUP_CONCAT(klascode,'(',name,')' ORDER BY klascode ASC SEPARATOR ', ')
Ene
10

Para concatenar todos los nombres de gerentes de proyecto de proyectos que tienen múltiples gerentes de proyecto, escriba:

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
fuente
9

Con el siguiente código, debe establecer PermissionLevel = External en las propiedades de su proyecto antes de implementarlo, y cambiar la base de datos para confiar en el código externo (asegúrese de leer en otro lugar sobre riesgos de seguridad y alternativas [como certificados]) ejecutando "ALTER DATABASE database_name SET CONFIABLE EN ".

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

He probado esto usando una consulta que se parece a:

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 

Y rendimientos: A, B, C, D

GregTSmith
fuente
9

Intenté esto, pero para mis propósitos en MS SQL Server 2005, lo siguiente fue más útil, lo que encontré en xaprb

declare @result varchar(8000);

set @result = '';

select @result = @result + name + ' '

from master.dbo.systypes;

select rtrim(@result);

@Mark como mencionaste, fue el personaje espacial el que me causó problemas.

isoughtajam
fuente
Creo que el motor realmente no garantiza ningún orden con este método, porque las variables se calculan como flujos de datos según el plan ejecutivo. Sin embargo, parece funcionar la mayor parte del tiempo hasta ahora.
phil_w
6

Sobre la respuesta de J Hardiman, ¿qué tal:

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

Por cierto, ¿el uso de "Apellido" es un error tipográfico o no estoy entendiendo un concepto aquí?

De todos modos, muchas gracias chicos porque me ahorró bastante tiempo :)

usuario422190
fuente
1
Respuesta bastante hostil si me preguntas y no es de ninguna ayuda como respuesta.
Tim Meers
1
solo viendo eso ahora ... No lo dije en serio, en ese momento estaba muy frustrado con el servidor sql (todavía lo estoy). las respuestas de esta publicación fueron realmente útiles; EDITAR: ¿por qué no fue útil por cierto? fue el truco para mí
user422190
1

Para mis compañeros de Google, hay una solución plug-and-play muy simple que funcionó para mí después de luchar por un tiempo con las soluciones más complejas:

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

Tenga en cuenta que tuve que convertir el ID en un VARCHAR para concatenarlo como una cadena. Si no tiene que hacer eso, aquí hay una versión aún más simple:

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

Todo el crédito para esto va aquí: 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
fuente