comment exclure les valeurs nulles dans array_agg comme dans string_agg en utilisant postgres?

96

Si j'utilise array_aggpour collecter des noms, j'obtiens mes noms séparés par des virgules, mais au cas où il y aurait une nullvaleur, cette valeur nulle est également considérée comme un nom dans l'agrégat. Par exemple :

SELECT g.id,
       array_agg(CASE WHEN g.canonical = 'Y' THEN g.users ELSE NULL END) canonical_users,
       array_agg(CASE WHEN g.canonical = 'N' THEN g.users ELSE NULL END) non_canonical_users
FROM groups g
GROUP BY g.id;

il retourne ,Larry,Philau lieu de juste Larry,Phil(dans mon 9.1.2, cela montre NULL,Larry,Phil). comme dans ce violon

Au lieu de cela, si j'utilise string_agg(), il ne me montre que les noms (sans virgules vides ni nulles) comme ici

Le problème est que je l' ai Postgres 8.4installé sur le serveur, et string_agg()ne fonctionne pas là. Existe-t-il un moyen de faire fonctionner array_agg de la même manière que string_agg ()?

Daud
la source
Voir ce fil de discussion de la liste de diffusion PostgreSQL sur ce sujet: postgresql.1045698.n5.nabble.com/...
Craig Ringer
Je suis désolé, je ne pense pas qu'il y ait une solution dans ce fil ..
Daud
Il existe deux solutions dans ce fil. L'une consiste à créer une fonction et l'autre (juste suggérée non illustrée) est celle à laquelle j'ai répondu.
Clodoaldo Neto
@Clodoaldo - toutes les lignes auront canonical in ('y', 'n') ... donc la clause where semble être redondante. Le problème est qu'à l'intérieur d'un groupement, si la valeur du champ canonique est «Y», et que nous collectons des «N», alors un nul doit être collecté aussi ..
Daud
D'accord. Maintenant j'ai compris. Vérifiez la réponse de mise à jour.
Clodoaldo Neto

Réponses:

28

Violon SQL

select
    id,
    (select array_agg(a) from unnest(canonical_users) a where a is not null) canonical_users,
    (select array_agg(a) from unnest(non_canonical_users) a where a is not null) non_canonical_users
from (
    SELECT g.id,
           array_agg(CASE WHEN g.canonical = 'Y' THEN g.users ELSE NULL END) canonical_users,
           array_agg(CASE WHEN g.canonical = 'N' THEN g.users ELSE NULL END) non_canonical_users
    FROM groups g
    GROUP BY g.id
) s

Ou, plus simple et peut être moins cher, en utilisant array_to_stringce qui élimine les valeurs nulles:

SELECT
    g.id,
    array_to_string(
        array_agg(CASE WHEN g.canonical = 'Y' THEN g.users ELSE NULL END)
        , ','
    ) canonical_users,
    array_to_string(
        array_agg(CASE WHEN g.canonical = 'N' THEN g.users ELSE NULL END)
        , ','
    ) non_canonical_users
FROM groups g
GROUP BY g.id

Violon SQL

Clodoaldo Neto
la source
Merci. Mais si la (les) requête (s) principale (s) renvoie (s) 1000 lignes, alors les 2 sous-requêtes (utilisant unnest) s'exécuteront une fois pour chaque ligne .. Est-ce qu'il vaudra mieux tolérer les NULL que d'exécuter 2000 requêtes de sélection supplémentaires?
Daud
@Daud Nouvelle version qui pourrait être moins chère. Prenez la sortie explicative des deux pour être sûr.
Clodoaldo Neto
3
@Clodoaldo Si vous utilisez, array_to_string(array_agg(...))vous pouvez aussi bien utiliser string_agg.
Craig Ringer
1
@Craig Le problème dans la question est 8.4
Clodoaldo Neto
@Clodoaldo Gah, anciennes versions. Merci.
Craig Ringer
248

Avec postgresql-9.3, on peut faire cela;

SELECT g.id,
   array_remove(array_agg(CASE WHEN g.canonical = 'Y' THEN g.users ELSE NULL END), NULL) canonical_users,
   array_remove(array_agg(CASE WHEN g.canonical = 'N' THEN g.users ELSE NULL END), NULL) non_canonical_users
FROM groups g 
GROUP BY g.id;

Mise à jour : avec postgresql-9.4;

SELECT g.id,
   array_agg(g.users) FILTER (WHERE g.canonical = 'Y') canonical_users,
   array_agg(g.users) FILTER (WHERE g.canonical = 'N') non_canonical_users
FROM groups g 
GROUP BY g.id;
Dale O'Brien
la source
5
Cela fonctionne et est rapide et élégant, cela m'a résolu un problème similaire à celui des OP. Une raison de passer à la version 9.3 pour ceux qui ne l'ont pas encore fait. +1
Pavel V.
12
Le 9.4 est encore plus élégant. Fonctionne comme un charme
jmgarnier
2
La variante 9.4 est encore meilleure, car ce que je dois filtrer dans mon cas, ce sont les valeurs nulles.
coladict
J'ai d'abord utilisé la version mise à jour, mais j'ai ensuite réalisé que je devais supprimer les Nulls et les doublons, alors je suis retourné à la première suggestion. C'est une grande requête, mais c'est pour créer une vue matérialisée, donc pas un gros problème.
Relequestual
12

Pour résoudre la question générale de la suppression des valeurs nulles des agrégats de tableaux, il existe deux façons principales d'attaquer le problème: soit en faisant array_agg (unnest (array_agg (x)) soit en créant un agrégat personnalisé.

Le premier est de la forme ci - dessus :

SELECT 
    array_agg(u) 
FROM (
    SELECT 
        unnest(
            array_agg(v)
        ) as u 
    FROM 
        x
    ) un
WHERE 
    u IS NOT NULL;

La deuxième:

/*
With reference to
http://ejrh.wordpress.com/2011/09/27/denormalisation-aggregate-function-for-postgresql/
*/
CREATE OR REPLACE FUNCTION fn_array_agg_notnull (
    a anyarray
    , b anyelement
) RETURNS ANYARRAY
AS $$
BEGIN

    IF b IS NOT NULL THEN
        a := array_append(a, b);
    END IF;

    RETURN a;

END;
$$ IMMUTABLE LANGUAGE 'plpgsql';

CREATE AGGREGATE array_agg_notnull(ANYELEMENT) (
    SFUNC = fn_array_agg_notnull,
    STYPE = ANYARRAY,
    INITCOND = '{}'
);

Appeler le second est (naturellement) un peu plus joli que le premier:

sélectionnez array_agg_notnull (v) à partir de x;

rorycl
la source
9

J'ajoute cela même si ce fil est assez ancien, mais je suis tombé sur cette astuce qui fonctionne assez bien sur de petits tableaux. Il fonctionne sur Postgres 8.4+ sans bibliothèques ou fonctions supplémentaires.

string_to_array(array_to_string(array_agg(my_column)))::int[]

La array_to_string()méthode supprime en fait les valeurs nulles.

ced-b
la source
9

Si vous recherchez une réponse moderne à la question générale de savoir comment supprimer un NULL d'un tableau , c'est:

array_remove(your_array, NULL)

J'étais particulièrement curieux de connaître les performances et je voulais comparer cela à la meilleure alternative possible:

CREATE OR REPLACE FUNCTION strip_nulls(
    IN array_in ANYARRAY
)
RETURNS anyarray AS
'
SELECT
    array_agg(a)
FROM unnest(array_in) a
WHERE
    a IS NOT NULL
;
'
LANGUAGE sql
;

Faire un test pgbench a prouvé (avec une grande confiance) que array_remove () est un peu plus de deux fois plus rapide . J'ai fait mon test sur des nombres à double précision avec une variété de tailles de tableaux (10, 100 et 1000 éléments) et des NULL aléatoires entre les deux.

Alexi Théodore
la source
@VivekSinha quelle version de postgres utilisez-vous? Je viens de tester votre requête et cela a abouti à "{1,2,3}" pour moi. J'utilise 12.1.
Alexi Theodore
Ah, je vois @ alexi-theodore ce qui se passe à ma fin. J'utilisais un pilote postgres personnalisé + modifié. Lorsque j'interroge directement dans la console, je peux voir la bonne sortie! Désolé pour la confusion. Commentaire précédent supprimé et réponse positive!
Vivek Sinha
3

Comme cela a été suggéré dans les commentaires, vous pouvez écrire une fonction pour remplacer les valeurs nulles dans un tableau, mais comme également souligné dans le fil lié à dans les commentaires, ce type de fonction va à l'encontre de l'efficacité de la fonction d'agrégation si vous devez créer un agrégat , divisez-le puis agrégez-le à nouveau.

Je pense que garder les valeurs nulles dans le tableau n'est qu'une fonctionnalité (peut-être indésirable) de Array_Agg. Vous pouvez utiliser des sous-requêtes pour éviter cela:

SELECT  COALESCE(y.ID, n.ID) ID,
        y.Users,
        n.Users
FROM    (   SELECT  g.ID, ARRAY_AGG(g.Users) AS Users
            FROM    Groups g
            WHERE   g.Canonical = 'Y'
            GROUP BY g.ID
        ) y
        FULL JOIN 
        (   SELECT  g.ID, ARRAY_AGG(g.Users) AS Users
            FROM    Groups g
            WHERE   g.Canonical = 'N'
            GROUP BY g.ID
        ) n
            ON n.ID = y.ID

SQL FIDDLE

GarethD
la source
Merci. Mais j'avais besoin de `` cas '' pour gérer les lignes dans un groupe donné, et les sous-requêtes y seraient inefficaces
Daud
0

C'est très simple, tout d'abord créez un nouvel opérateur - (moins) pour le texte [] :

CREATE OR REPLACE FUNCTION diff_elements_text
    (
        text[], text[] 
    )
RETURNS text[] as 
$$
    SELECT array_agg(DISTINCT new_arr.elem)
    FROM
        unnest($1) as new_arr(elem)
        LEFT OUTER JOIN
        unnest($2) as old_arr(elem)
        ON new_arr.elem = old_arr.elem
    WHERE old_arr.elem IS NULL
$$ LANGUAGE SQL IMMUTABLE;

CREATE OPERATOR - (
    PROCEDURE = diff_elements_text,
    leftarg = text[],
    rightarg = text[]
);

Et soustrayez simplement le tableau [null]:

select 
    array_agg(x)-array['']
from
    (   select 'Y' x union all
        select null union all
        select 'N' union all
        select '' 
    ) x;

C'est tout:

{O, N}

Miklos
la source
1
array_agg(x) FILTER (WHERE x is not null)semble beaucoup plus facile: dbfiddle.uk/ ... et vous n'avez pas vraiment besoin de votre propre fonction, vous pouvez simplement utiliser array_remove() dbfiddle.uk
...
-6

Une question plus importante est de savoir pourquoi tirer tous les combos utilisateur / groupe à la fois. Garanti que votre interface utilisateur ne peut pas gérer toutes ces données. L'ajout de pagination à des données surdimensionnées est également une mauvaise idée. Demandez à vos utilisateurs de filtrer l'ensemble avant de voir les données. Assurez-vous que votre jeu d'options JOIN est dans la liste afin qu'ils puissent filtrer les performances s'ils le souhaitent. Parfois, 2 requêtes rendent les utilisateurs plus heureux s'ils sont tous les deux rapides.

Michael
la source