como excluir valores nulos em array_agg como em string_agg usando postgres?

96

Se eu uso array_aggpara coletar nomes, eu os separo por vírgulas, mas caso haja um nullvalor, nulo também é considerado um nome no agregado. Por exemplo :

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;

ele retorna em ,Larry,Philvez de apenas Larry,Phil(no meu 9.1.2, ele mostra NULL,Larry,Phil). como neste violino

Em vez disso, se eu usar string_agg(), ele mostra apenas os nomes (sem vírgulas vazias ou nulos) como aqui

O problema é que Postgres 8.4instalei no servidor e string_agg()não funciona lá. Existe alguma maneira de fazer array_agg funcionar de forma semelhante a string_agg ()?

Daud
fonte
Veja este tópico da lista de discussão do PostgreSQL sobre este tópico: postgresql.1045698.n5.nabble.com/…
Craig Ringer
Sinto muito, não acho que haja uma solução nesse tópico ..
Daud
Existem duas soluções nesse segmento. Um é criar uma função e o outro (apenas sugerido não mostrado) é o que respondi.
Clodoaldo Neto
@Clodoaldo - todas as linhas terão entrada canônica ('y', 'n') ... então a cláusula where parece redundante. O problema é que dentro de um agrupamento, se o valor do campo canônico for 'Y' e estivermos coletando 'N's, então um nulo também será coletado ..
Daud
Está bem. Agora entendi. Verifique a resposta de atualização.
Clodoaldo Neto

Respostas:

28

SQL Fiddle

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, mais simples e pode ser mais barato, usando o array_to_stringque elimina nulos:

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

SQL Fiddle

Clodoaldo Neto
fonte
Obrigado. Mas se a consulta principal retornar 1000 linhas, as 2 subconsultas (usando unnest) serão executadas uma vez para cada linha. Será melhor tolerar NULLs do que executar 2.000 consultas extras de seleção?
Daud
@Daud Nova versão que pode ser mais barata. Pegue a saída de explicação de ambos para ter certeza.
Clodoaldo Neto
3
@Clodoaldo Se você estiver usando, array_to_string(array_agg(...))você também pode usar string_agg.
Craig Ringer
1
@Craig O problema da pergunta é 8,4
Clodoaldo Neto
@Clodoaldo Gah, versões antigas. Obrigado.
Craig Ringer
245

Com o postgresql-9.3 pode-se fazer isso;

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;

Atualização : com 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
fonte
5
Isso funciona e é rápido e elegante, resolveu um problema parecido com os OP's. Um motivo para atualizar para o 9.3 para aqueles que ainda não o fizeram. +1
Pavel V.
12
O 9.4 é ainda mais elegante. Funciona como um encanto
jmgarnier
2
A variante 9.4 é ainda melhor, porque o que preciso filtrar no meu caso são os nulos.
coladito de
Usei a versão atualizada primeiro, mas depois percebi que precisava remover nulos e duplicatas, então voltei para a primeira sugestão. É uma consulta grande, mas é para criar uma visão materializada, portanto, não é um grande problema.
Relequestual
12

Ao resolver a questão geral de remover nulos de agregados de array, há duas maneiras principais de atacar o problema: fazendo array_agg (unnest (array_agg (x)) ou criando um agregado personalizado.

O primeiro tem o formato mostrado acima :

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

O segundo:

/*
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 = '{}'
);

Chamar o segundo é (naturalmente) um pouco mais bonito do que o primeiro:

selecione array_agg_notnull (v) de x;

rorycl
fonte
9

Estou adicionando isso, embora este tópico seja bastante antigo, mas encontrei um truque bacana que funciona muito bem em matrizes pequenas. Ele roda em Postgres 8.4+ sem bibliotecas ou funções adicionais.

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

O array_to_string()método realmente elimina os nulos.

ced-b
fonte
8

Se você está procurando uma resposta moderna para a questão geral de como remover um NULL de uma matriz , é:

array_remove(your_array, NULL)

Eu estava especificamente curioso sobre o desempenho e queria comparar isso com a melhor alternativa possível:

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
;

Fazer um teste pgbench provou (com alta confiança) que array_remove () é um pouco mais do que duas vezes mais rápido . Eu fiz meu teste em números de precisão dupla com uma variedade de tamanhos de array (10, 100 e 1000 elementos) e NULLs aleatórios entre eles.

Alexi Theodore
fonte
@VivekSinha qual versão do postgres você está usando? Acabei de testar sua consulta e resultou em "{1,2,3}" para mim. Estou usando 12.1.
Alexi Theodore
Ah, eu vejo @ alexi-theodore o que está acontecendo no meu fim. Eu estava usando um driver postgres customizado + modificado. Quando faço uma consulta diretamente no console, posso ver a saída correta! Desculpe pela confusão. Comentário anterior eliminado e resposta votada!
Vivek Sinha
3

Como foi sugerido nos comentários, você pode escrever uma função para substituir os nulos em uma matriz, no entanto, como também apontado no tópico vinculado aos comentários, isso prejudica a eficiência da função de agregação se você tiver que criar um agregado , divida-o e, em seguida, agregue-o novamente.

Acho que manter nulos na matriz é apenas um recurso (talvez indesejado) de Array_Agg. Você pode usar subconsultas para evitar isso:

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
fonte
Obrigado. Mas eu precisava de 'caso' para lidar com linhas em um determinado agrupamento, e as subconsultas seriam ineficientes lá
Daud
0

É muito simples, basta primeiro criar um novo operador - (menos) para o texto [] :

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[]
);

E simplesmente subtraia a matriz [null]:

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

Isso é tudo:

{S, N}

Miklos
fonte
array_agg(x) FILTER (WHERE x is not null)parece muito mais fácil: dbfiddle.uk/… e você realmente não precisa de sua própria função, você pode simplesmente usar array_remove() dbfiddle.uk/…
a_horse_with_no_name
-6

Uma questão maior é por que puxar todos os combos de usuário / grupo de uma vez. Garantido que sua interface do usuário não pode lidar com todos esses dados. Adicionar paginação a dados superdimensionados também é uma má ideia. Faça com que seus usuários filtrem o conjunto antes de ver os dados. Certifique-se de que o conjunto de opções JOIN esteja na lista para que eles possam filtrar por desempenho, se quiserem. Às vezes, 2 consultas deixam os usuários mais felizes se ambos forem rápidos.

Michael
fonte