¿Cómo concatenar cadenas de un campo de cadena en una consulta PostgreSQL 'group by'?

351

Estoy buscando una forma de concatenar las cadenas de un campo dentro de un grupo por consulta. Entonces, por ejemplo, tengo una tabla:

ID   COMPANY_ID   EMPLOYEE
1    1            Anna
2    1            Bill
3    2            Carol
4    2            Dave

y quería agrupar por company_id para obtener algo como:

COMPANY_ID   EMPLOYEE
1            Anna, Bill
2            Carol, Dave

Hay una función incorporada en mySQL para hacer esto group_concat

Guy C
fuente
1
La respuesta de Markus Döring es técnicamente mejor.
pstanton
@pstanton, la respuesta de Döring solo es mejor para 8.4 y menos.
Jared Beck
Esta pregunta parece ser más adecuada para dba.stackexchange.com .
Dave Jarvis el
Esta debería ser la respuesta válida ahora stackoverflow.com/a/47638417/243233
Jus12

Respuestas:

542

PostgreSQL 9.0 o posterior:

Las versiones recientes de Postgres (desde finales de 2010) tienen la string_agg(expression, delimiter)función que hará exactamente lo que pedía la pregunta, incluso permitiéndole especificar la cadena delimitador:

SELECT company_id, string_agg(employee, ', ')
FROM mytable
GROUP BY company_id;

Postgres 9.0 también agregó la capacidad de especificar una ORDER BYcláusula en cualquier expresión agregada ; de lo contrario, el orden no está definido. Entonces ahora puedes escribir:

SELECT company_id, string_agg(employee, ', ' ORDER BY employee)
FROM mytable
GROUP BY company_id;

O de hecho:

SELECT string_agg(actor_name, ', ' ORDER BY first_appearance)

PostgreSQL 8.4 o posterior:

PostgreSQL 8.4 (en 2009) introdujo la función agregadaarray_agg(expression) que concatena los valores en una matriz. Luego array_to_string()se puede usar para dar el resultado deseado:

SELECT company_id, array_to_string(array_agg(employee), ', ')
FROM mytable
GROUP BY company_id;

string_agg para versiones anteriores a la 8.4:

En caso de que alguien se encuentre con esto buscando una compatibilidad de compatibilidad para bases de datos anteriores a 9.0, es posible implementar todo string_aggexcepto la ORDER BYcláusula.

Entonces, con la definición a continuación, esto debería funcionar igual que en una base de datos Postgres 9.x:

SELECT string_agg(name, '; ') AS semi_colon_separated_names FROM things;

Pero esto será un error de sintaxis:

SELECT string_agg(name, '; ' ORDER BY name) AS semi_colon_separated_names FROM things;
--> ERROR: syntax error at or near "ORDER"

Probado en PostgreSQL 8.3.

CREATE FUNCTION string_agg_transfn(text, text, text)
    RETURNS text AS 
    $$
        BEGIN
            IF $1 IS NULL THEN
                RETURN $2;
            ELSE
                RETURN $1 || $3 || $2;
            END IF;
        END;
    $$
    LANGUAGE plpgsql IMMUTABLE
COST 1;

CREATE AGGREGATE string_agg(text, text) (
    SFUNC=string_agg_transfn,
    STYPE=text
);

Variaciones personalizadas (todas las versiones de Postgres)

Antes de 9.0, no había una función agregada incorporada para concatenar cadenas. La implementación personalizada más simple ( sugerida por Vajda Gabo en esta publicación de la lista de correo , entre muchas otras) es usar la textcatfunción incorporada (que se encuentra detrás del ||operador):

CREATE AGGREGATE textcat_all(
  basetype    = text,
  sfunc       = textcat,
  stype       = text,
  initcond    = ''
);

Aquí está la CREATE AGGREGATEdocumentación.

Esto simplemente pega todas las cadenas juntas, sin separador. Para obtener un "," insertado entre ellos sin tenerlo al final, es posible que desee hacer su propia función de concatenación y sustituirla por el "textcat" anterior. Aquí hay uno que armé y probé en 8.3.12:

CREATE FUNCTION commacat(acc text, instr text) RETURNS text AS $$
  BEGIN
    IF acc IS NULL OR acc = '' THEN
      RETURN instr;
    ELSE
      RETURN acc || ', ' || instr;
    END IF;
  END;
$$ LANGUAGE plpgsql;

Esta versión generará una coma incluso si el valor en la fila es nulo o vacío, por lo que obtendrá una salida como esta:

a, b, c, , e, , g

Si prefiere eliminar comas adicionales para generar esto:

a, b, c, e, g

Luego agregue un ELSIFcheque a la función de esta manera:

CREATE FUNCTION commacat_ignore_nulls(acc text, instr text) RETURNS text AS $$
  BEGIN
    IF acc IS NULL OR acc = '' THEN
      RETURN instr;
    ELSIF instr IS NULL OR instr = '' THEN
      RETURN acc;
    ELSE
      RETURN acc || ', ' || instr;
    END IF;
  END;
$$ LANGUAGE plpgsql;
Neall
fuente
1
Tuve que S&R varchar a texto (último pgsql estable) pero esto es genial!
Kev
1
Puede escribir la función solo en SQL, que es más fácil de instalar (el superusuario debe instalar plpgsql). Vea mi publicación para un ejemplo.
bortzmeyer
11
"No hay una función agregada incorporada para concatenar cadenas" - ¿por qué no usarías array_to_string(array_agg(employee), ',')?
pstanton
2
+1 para la función PostgreSQL 9.0. Si necesita preocuparse por la versión anterior a la 9.0, la respuesta de Markus es mejor.
Brad Koch el
77
Tenga en cuenta que las versiones recientes de Postgres también permiten una Order Bycláusula dentro de la función agregada, por ejemplostring_agg(employee, ',' Order By employee)
IMSoP
99

¿Qué tal el uso de las funciones de matriz integradas de Postgres? Al menos en 8.4 esto funciona de la caja:

SELECT company_id, array_to_string(array_agg(employee), ',')
FROM mytable
GROUP BY company_id;
Markus Döring
fuente
lamentablemente esto no funciona para nosotros en Greenplum (v8.2). +1 de todos
modos
Funciona bien para mí en Greenplum 4.3.4.1 (construido en PostgreSQL 8.2.15).
PhilHibbs
19

A partir de PostgreSQL 9.0 puede usar la función agregada llamada string_agg . Su nuevo SQL debería verse así:

SELECT company_id, string_agg(employee, ', ')
FROM mytable
GROUP BY company_id;

dirbacke
fuente
13

No reclamo crédito por la respuesta porque la encontré después de buscar:

Lo que no sabía es que PostgreSQL le permite definir sus propias funciones agregadas con CREATE AGGREGATE

Esta publicación en la lista PostgreSQL muestra cuán trivial es crear una función para hacer lo que se requiere:

CREATE AGGREGATE textcat_all(
  basetype    = text,
  sfunc       = textcat,
  stype       = text,
  initcond    = ''
);

SELECT company_id, textcat_all(employee || ', ')
FROM mytable
GROUP BY company_id;
Guy C
fuente
7

Como ya se mencionó, crear su propia función agregada es lo correcto. Aquí está mi función agregada de concatenación (puede encontrar detalles en francés ):

CREATE OR REPLACE FUNCTION concat2(text, text) RETURNS text AS '
    SELECT CASE WHEN $1 IS NULL OR $1 = \'\' THEN $2
            WHEN $2 IS NULL OR $2 = \'\' THEN $1
            ELSE $1 || \' / \' || $2
            END; 
'
 LANGUAGE SQL;

CREATE AGGREGATE concatenate (
  sfunc = concat2,
  basetype = text,
  stype = text,
  initcond = ''

);

Y luego úsalo como:

SELECT company_id, concatenate(employee) AS employees FROM ...
bortzmeyer
fuente
5

Este último fragmento de la lista de anuncios podría ser de interés si va a actualizar a 8.4:

Hasta que 8.4 salga con una versión nativa súper eficiente, puede agregar la función array_accum () en la documentación de PostgreSQL para enrollar cualquier columna en una matriz, que luego puede ser utilizada por el código de la aplicación, o combinada con array_to_string () para formatear como una lista:

http://www.postgresql.org/docs/current/static/xaggr.html

Me vincularía a los documentos de desarrollo 8.4 pero todavía no parecen enumerar esta característica.

Kev
fuente
5

Continuando con la respuesta de Kev, usando los documentos de Postgres:

Primero, cree una matriz de los elementos, luego use la array_to_stringfunción incorporada.

CREATE AGGREGATE array_accum (anyelement)
(
 sfunc = array_append,
 stype = anyarray,
 initcond = '{}'
);

select array_to_string(array_accum(name),'|') from table group by id;
Brad Koch
fuente
5

Siguiendo una vez más en el uso de una función agregada costumbre de la concatenación de cadenas: Es necesario recordar que la instrucción de selección colocará filas en cualquier orden, por lo que tendrá que hacer un sub seleccionar en el de la sentencia con una orden por cláusula, y luego una selección externa con una cláusula group by para agregar las cadenas, por lo tanto:

SELECT custom_aggregate(MY.special_strings)
FROM (SELECT special_strings, grouping_column 
        FROM a_table 
        ORDER BY ordering_column) MY
GROUP BY MY.grouping_column
Brad Koch
fuente
2

Utilice la STRING_AGGfunción para PostgreSQL y Google BigQuery SQL :

SELECT company_id, STRING_AGG(employee, ', ')
FROM employees
GROUP BY company_id;
Valentin Podkamennyi
fuente
0

De acuerdo con la versión PostgreSQL 9.0 y superior, puede usar la función agregada llamada string_agg. Su nuevo SQL debería verse así:

SELECT company_id, string_agg(employee, ', ')
    FROM mytable GROUP BY company_id;
Gobinath
fuente
0

También puede usar la función de formato. Que también puede encargarse implícitamente de la conversión de tipo de texto, int, etc.

create or replace function concat_return_row_count(tbl_name text, column_name text, value int)
returns integer as $row_count$
declare
total integer;
begin
    EXECUTE format('select count(*) from %s WHERE %s = %s', tbl_name, column_name, value) INTO total;
    return total;
end;
$row_count$ language plpgsql;


postgres=# select concat_return_row_count('tbl_name','column_name',2); --2 is the value
Sandip Debnath
fuente
1
¿Cómo se relaciona esto con el uso de un agregado para concatenar valores de cadena?
a_horse_with_no_name
0

Estoy usando Jetbrains Rider y fue una molestia copiar los resultados de los ejemplos anteriores para volver a ejecutarlos porque parecía envolverlo todo en JSON. Esto los une en una sola declaración que fue más fácil de ejecutar

select string_agg('drop table if exists "' || tablename || '" cascade', ';') 
from pg_tables where schemaname != $$pg_catalog$$ and tableName like $$rm_%$$
Damien Sawyer
fuente
0

Si está en Amazon Redshift, donde string_agg no es compatible, intente usar listagg.

SELECT company_id, listagg(EMPLOYEE, ', ') as employees
FROM EMPLOYEE_table
GROUP BY company_id;
Gapp
fuente