¿Cómo excluir valores nulos en array_agg como en string_agg usando postgres?

96

Si utilizo array_aggpara recopilar nombres, obtengo mis nombres separados por comas, pero en caso de que haya un nullvalor, ese nulo también se toma como un nombre en el agregado. Por ejemplo :

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;

devuelve en ,Larry,Phillugar de solo Larry,Phil(en mi 9.1.2, se muestra NULL,Larry,Phil). como en este violín

En cambio, si lo uso string_agg(), me muestra solo los nombres (sin comas vacías o nulos) como aquí

El problema es que lo he Postgres 8.4instalado en el servidor y string_agg()no funciona allí. ¿Hay alguna forma de hacer que array_agg funcione de manera similar a string_agg ()?

Daud
fuente
Vea este hilo de la lista de correo de PostgreSQL sobre gran parte de este tema: postgresql.1045698.n5.nabble.com/…
Craig Ringer
Lo siento, no creo que haya una solución en ese hilo ..
Daud
Hay dos soluciones en ese hilo. Uno es crear una función y el otro (solo sugerido, no mostrado) es el que respondí.
Clodoaldo Neto
@Clodoaldo: todas las filas tendrán canonical in ('y', 'n') ... por lo que la cláusula where parece ser redundante. El problema es que dentro de una agrupación, si el valor del campo canónico es 'Y', y estamos recolectando 'N's, entonces también se recolectará un valor nulo ..
Daud
Okay. Ahora lo tengo. Verifique la respuesta de actualización.
Clodoaldo Neto

Respuestas:

28

Violín 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

O, más simple y puede ser más económico, usando array_to_stringque elimina los 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

Violín SQL

Clodoaldo Neto
fuente
Gracias. Pero si la (s) consulta (s) principal (es) devuelve 1000 filas, entonces las 2 subconsultas (usando unnest) se ejecutarán una vez para cada fila. ¿Será mejor tolerar NULL que ejecutar 2000 consultas de selección adicionales?
Daud
@Daud Nueva versión que podría ser más económica. Tome la salida explicada de ambos para estar seguro.
Clodoaldo Neto
3
@Clodoaldo Si está usando, array_to_string(array_agg(...))también podría usar string_agg.
Craig Ringer
1
@Craig El problema en la pregunta es 8.4
Clodoaldo Neto
@Clodoaldo Gah, versiones antiguas. Gracias.
Craig Ringer
248

Con postgresql-9.3 uno puede hacer esto;

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;

Actualizar : con 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
fuente
5
Esto funciona y es rápido y elegante, me solucionó un problema similar al de los OP. Una razón para actualizar a 9.3 para aquellos que aún no lo hicieron. +1
Pavel V.
12
El 9.4 es aún más elegante. Funciona como un encanto
jmgarnier
2
La variante 9.4 es aún mejor, porque lo que necesito filtrar en mi caso son los nulos.
coladict
Primero usé la versión actualizada, pero luego me di cuenta de que necesitaba eliminar Nulls y duplicados, así que volví a la primera sugerencia. Es una consulta grande, pero es para crear una vista materializada, por lo que no es un gran problema.
Relequestual el
12

Para resolver la cuestión general de eliminar nulos de los agregados de matrices, hay dos formas principales de atacar el problema: haciendo array_agg (unnest (array_agg (x)) o creando un agregado personalizado.

El primero es de la forma que se muestra arriba :

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

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

Llamar al segundo es (naturalmente) un poco más agradable que el primero:

seleccione array_agg_notnull (v) de x;

rorycl
fuente
9

Estoy agregando esto a pesar de que este hilo es bastante antiguo, pero encontré este ingenioso truco que funciona bastante bien en arreglos pequeños. Se ejecuta en Postgres 8.4+ sin bibliotecas o funciones adicionales.

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

En array_to_string()realidad, el método elimina los nulos.

ced-b
fuente
9

Si está buscando una respuesta moderna a la pregunta general de cómo eliminar un NULL de una matriz , es:

array_remove(your_array, NULL)

Tenía especial curiosidad por el rendimiento y quería compararlo con la mejor alternativa posible:

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
;

Al hacer una prueba pgbench se demostró (con alta confianza) que array_remove () es un poco más del doble de rápido . Hice mi prueba en números de doble precisión con una variedad de tamaños de matriz (10, 100 y 1000 elementos) y NULL aleatorios en el medio.

Alexi Theodore
fuente
@VivekSinha ¿qué versión de postgres estás usando? Acabo de probar su consulta y me dio "{1,2,3}". Estoy usando 12.1.
Alexi Theodore
Ah, veo @ alexi-theodore lo que está pasando al final. Estaba usando un controlador de postgres personalizado + modificado. Cuando consulto directamente en la consola, puedo ver el resultado correcto. Perdón por la confusión. ¡Comentario anterior eliminado y respuesta votada a favor!
Vivek Sinha
3

Como se sugirió en los comentarios, puede escribir una función para reemplazar nulos en una matriz, sin embargo, como también se señaló en el hilo vinculado en los comentarios, este tipo de derrota la eficiencia de la función agregada si tiene que crear una agregación , divídalo y luego vuelva a agregarlo.

Creo que mantener nulos en la matriz es solo una característica (quizás no deseada) de Array_Agg. Puede usar subconsultas para evitar esto:

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

Violín SQL

GarethD
fuente
Gracias. Pero necesitaba 'caso' para manejar filas dentro de una agrupación determinada, y las subconsultas serían ineficientes allí
Daud
0

Es muy simple, primero que nada cree un nuevo operador - (menos) para el 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[]
);

Y simplemente reste la matriz [nulo]:

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

Eso es todo:

{S, N}

Miklos
fuente
1
array_agg(x) FILTER (WHERE x is not null)parece mucho más fácil: dbfiddle.uk/… y realmente no necesita su propia función, simplemente puede usar array_remove() dbfiddle.uk/…
a_horse_with_no_name
-6

Una pregunta más importante es por qué extraer todos los combos de usuario / grupo a la vez. Garantizado que su interfaz de usuario no puede manejar todos esos datos. Agregar paginación a datos de gran tamaño también es una mala idea. Haga que sus usuarios filtren el conjunto antes de que vean los datos. Asegúrese de que su conjunto de opciones JOIN esté en la lista para que puedan filtrar el rendimiento si así lo desean. A veces, 2 consultas hacen que los usuarios estén más contentos si ambas son rápidas.

Miguel
fuente