Comment concaténer des chaînes d'un champ de chaîne dans une requête 'group by' de PostgreSQL?

351

Je cherche un moyen de concaténer les chaînes d'un champ au sein d'un groupe par requête. Ainsi, par exemple, j'ai une table:

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

et je voulais regrouper par company_id pour obtenir quelque chose comme:

COMPANY_ID   EMPLOYEE
1            Anna, Bill
2            Carol, Dave

Il y a une fonction intégrée dans mySQL pour faire ceci group_concat

Guy C
la source
1
La réponse de Markus Döring est techniquement meilleure.
pstanton
@pstanton, la réponse de Döring n'est meilleure que pour la version 8.4 et inférieure.
Jared Beck
Cette question semble mieux adaptée à dba.stackexchange.com .
Dave Jarvis du
Cela devrait être la réponse valide maintenant stackoverflow.com/a/47638417/243233
Jus12

Réponses:

542

PostgreSQL 9.0 ou version ultérieure:

Les versions récentes de Postgres (depuis fin 2010) ont la string_agg(expression, delimiter)fonction qui fera exactement ce que la question demandait, vous permettant même de spécifier la chaîne de délimitation:

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

Postgres 9.0 a également ajouté la possibilité de spécifier une ORDER BYclause dans n'importe quelle expression agrégée ; sinon, la commande n'est pas définie. Vous pouvez donc maintenant écrire:

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

Ou bien:

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

PostgreSQL 8.4 ou version ultérieure:

PostgreSQL 8.4 (en 2009) a introduit la fonction d'agrégationarray_agg(expression) qui concatène les valeurs dans un tableau. Ensuite, array_to_string()peut être utilisé pour donner le résultat souhaité:

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

string_agg pour les versions antérieures à 8.4:

Dans le cas où quelqu'un rencontre ce problème à la recherche d'un module de compatibilité pour les bases de données antérieures à la version 9.0, il est possible de tout mettre en œuvre à l' string_aggexception de la ORDER BYclause.

Ainsi, avec la définition ci-dessous, cela devrait fonctionner de la même manière que dans une base de données Postgres 9.x:

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

Mais ce sera une erreur de syntaxe:

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

Testé sur 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
);

Variations personnalisées (toutes les versions Postgres)

Avant la version 9.0, il n'y avait pas de fonction d'agrégation intégrée pour concaténer les chaînes. L'implémentation personnalisée la plus simple ( suggérée par Vajda Gabo dans cette publication de liste de diffusion , entre autres) consiste à utiliser la textcatfonction intégrée (qui se trouve derrière l' ||opérateur):

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

Voici la CREATE AGGREGATEdocumentation.

Cela colle simplement toutes les cordes ensemble, sans séparateur. Pour obtenir un "," inséré entre eux sans l'avoir à la fin, vous pouvez créer votre propre fonction de concaténation et la remplacer par le "textcat" ci-dessus. En voici un que j'ai assemblé et testé le 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;

Cette version affichera une virgule même si la valeur de la ligne est nulle ou vide, vous obtenez donc une sortie comme celle-ci:

a, b, c, , e, , g

Si vous préférez supprimer les virgules supplémentaires pour afficher ceci:

a, b, c, e, g

Ajoutez ensuite une ELSIFvérification à la fonction comme ceci:

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
la source
1
J'ai dû S&R varchar au texte (dernière version stable de pgsql) mais c'est génial!
Kev
1
Vous pouvez écrire la fonction en SQL uniquement, ce qui est plus facile pour l'installation (plpgsql doit être installé par le superutilisateur). Voir mon article pour un exemple.
bortzmeyer
11
"Il n'y a pas de fonction d'agrégation intégrée pour concaténer les chaînes" - pourquoi ne pas utiliser array_to_string(array_agg(employee), ',')?
pstanton
2
+1 pour la fonction PostgreSQL 9.0. Si vous devez vous préoccuper de la version antérieure à 9.0, la réponse de Markus est meilleure.
Brad Koch
7
Notez que les versions récentes de Postgres autorisent également une Order Byclause à l'intérieur de la fonction d'agrégation, par exemplestring_agg(employee, ',' Order By employee)
IMSoP
99

Que diriez-vous d'utiliser les fonctions de tableau intégrées de Postgres? Au moins sur 8.4 cela fonctionne hors de la boîte:

SELECT company_id, array_to_string(array_agg(employee), ',')
FROM mytable
GROUP BY company_id;
Markus Döring
la source
malheureusement, cela ne fonctionne pas pour nous sur Greenplum (v8.2). +1 tout de même
ekkis
Fonctionne très bien pour moi sur Greenplum 4.3.4.1 (construit sur PostgreSQL 8.2.15).
PhilHibbs
19

Depuis PostgreSQL 9.0, vous pouvez utiliser la fonction d'agrégation appelée string_agg . Votre nouveau SQL devrait ressembler à ceci:

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

dirbacke
la source
13

Je ne demande aucun crédit pour la réponse car je l'ai trouvée après quelques recherches:

Ce que je ne savais pas, c'est que PostgreSQL vous permet de définir vos propres fonctions d'agrégation avec CREATE AGGREGATE

Ce message sur la liste PostgreSQL montre à quel point il est trivial de créer une fonction pour faire ce qui est nécessaire:

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
la source
7

Comme déjà mentionné, la création de votre propre fonction d'agrégation est la bonne chose à faire. Voici ma fonction d'agrégation de concaténation (vous pouvez trouver des détails en français ):

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 = ''

);

Et puis utilisez-le comme:

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

Ce dernier extrait de liste d'annonces pourrait être intéressant si vous passez à la version 8.4:

Jusqu'à ce que la version 8.4 sorte avec une native très efficace, vous pouvez ajouter la fonction array_accum () dans la documentation PostgreSQL pour enrouler n'importe quelle colonne dans un tableau, qui peut ensuite être utilisée par le code d'application, ou combinée avec array_to_string () pour formater comme une liste:

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

Je créerais un lien vers les documents de développement 8.4, mais ils ne semblent pas encore répertorier cette fonctionnalité.

Kev
la source
5

Suivi de la réponse de Kev, en utilisant les documents Postgres:

Créez d'abord un tableau des éléments, puis utilisez la array_to_stringfonction intégrée.

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
la source
5

Suite encore une fois à l'utilisation d'une fonction d'agrégation personnalisée de concaténation de chaînes: vous devez vous rappeler que l'instruction select placera les lignes dans n'importe quel ordre, vous devrez donc effectuer une sous- sélection dans l' instruction from avec une clause order by , et puis une sélection externe avec une clause group by pour agréger les chaînes, ainsi:

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
la source
2

Utilisez la STRING_AGGfonction pour PostgreSQL et Google BigQuery SQL :

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

Selon la version PostgreSQL 9.0 et supérieure, vous pouvez utiliser la fonction d'agrégation appelée string_agg. Votre nouveau SQL devrait ressembler à ceci:

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

Vous pouvez également utiliser la fonction de formatage. Ce qui peut également implicitement prendre en charge la conversion de type de texte, int, etc. par lui-même.

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
la source
1
Comment est-ce lié à l'utilisation d'un agrégat pour concaténer des valeurs de chaîne?
a_horse_with_no_name
0

J'utilise Jetbrains Rider et c'était une corvée de copier les résultats des exemples ci-dessus pour les réexécuter, car il semblait tout en JSON. Cela les joint en une seule instruction plus facile à exécuter

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

Si vous êtes sur Amazon Redshift, où string_agg n'est pas pris en charge, essayez d'utiliser listagg.

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