как исключить нулевые значения в array_agg, как в string_agg, с помощью postgres?

101

Если я использую array_aggдля сбора имен, я получаю имена через запятую, но в случае, если есть nullзначение, это значение null также принимается как имя в совокупности. Например :

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;

он возвращается, ,Larry,Philа не просто Larry,Phil(в моем 9.1.2 это видно NULL,Larry,Phil). как в этой скрипке

Вместо этого, если я использую string_agg(), он показывает мне только имена (без пустых запятых и нулей), как здесь

Проблема в том, что я Postgres 8.4установил на сервер, а string_agg()там не работает. Есть ли способ заставить array_agg работать аналогично string_agg ()?

Дауд
источник
Смотрите эту ветку списка рассылки PostgreSQL по большей части этой темы: postgresql.1045698.n5.nabble.com/…
Крейг Рингер,
Мне очень жаль, но я не думаю, что в этой ветке есть решение ..
Дауд
В этой ветке есть два решения. Один из них - создать функцию, а другой (предложенный, но не показанный) - тот, на который я ответил.
Clodoaldo Neto
@Clodoaldo - все строки будут иметь канонические in ('y', 'n') ... поэтому предложение where кажется избыточным. Проблема в том, что внутри группировки, если значение канонического поля - «Y», а мы собираем «N», то также будет собран нуль ..
Дауд
ОК. Теперь я понял. Проверьте ответ на обновление.
Клодоальдо Нето,

Ответы:

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

Или, что проще и может быть дешевле, с использованием array_to_stringкоторого исключаются нули:

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

Клодоальдо Нето
источник
Спасибо. Но если основной запрос (ы) возвращает 1000 строк, то 2 подзапроса (с использованием unnest) будут выполняться один раз для каждой строки. Будет ли лучше допускать значения NULL, чем выполнение 2000 дополнительных запросов выбора?
Дауд
@Daud Новая версия, которая может быть дешевле. Чтобы быть уверенным, возьмите вывод объяснения обоих.
Клодоальдо Нето,
3
@Clodoaldo Если вы используете, array_to_string(array_agg(...))вы также можете использовать string_agg.
Крейг Рингер,
1
@Craig Проблема в вопросе 8.4
Клодоальдо Нето
@Clodoaldo Gah, старые версии. Спасибо.
Крейг Рингер,
256

С postgresql-9.3 это можно сделать;

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;

Обновление : с 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;
Дейл О'Брайен
источник
5
Это работает, быстро и элегантно, это решило мне проблему, аналогичную OP. Причина перехода на 9.3 для тех, кто еще этого не делал. +1
Павел В.
12
9.4 еще элегантнее. Работает как шарм
jmgarnier 08
2
Вариант 9.4 даже лучше, потому что в моем случае мне нужно отфильтровать нули.
коладикт
Сначала я использовал обновленную версию, но потом понял, что мне нужно удалить пустые значения и дубликаты, поэтому вернулся к первому предложению. Это большой запрос, но он предназначен для создания материализованного представления, так что это не большая проблема.
Relequestual
12

При решении общего вопроса об удалении нулей из агрегатов массивов есть два основных способа решения проблемы: либо выполнение array_agg (unnest (array_agg (x))), либо создание настраиваемого агрегата.

Первый имеет форму, показанную выше :

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

Второй:

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

Вызов второго (естественно) выглядит немного лучше, чем первый:

выберите array_agg_notnull (v) из x;

Рорикл
источник
12

Если вы ищете современный ответ на общий вопрос о том, как удалить NULL из массива , это:

array_remove(your_array, NULL)

Мне было особенно интересно узнать о производительности, и я хотел сравнить это с лучшей альтернативой:

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
;

Тест pgbench доказал (с большой уверенностью), что array_remove () работает чуть более чем в два раза быстрее . Я провел свой тест на числах двойной точности с различными размерами массивов (10, 100 и 1000 элементов) и случайными значениями NULL между ними.


Также стоит отметить, что это можно использовать для удаления пробелов (''! = NULL). Но второй параметр принимает anyelement, и, поскольку, скорее всего, вы укажете пробел с помощью строкового литерала, обязательно приведите его к желаемой форме, обычно не к массиву.

Например:

select array_remove(array['abc', ''], ''::text);

Если вы пытаетесь:

select array_remove(array['abc', ''], '');

он будет считать, что это TEXT [] (массив), и выдаст эту ошибку:

ОШИБКА: литерал искаженного массива: ""

Алекси Теодор
источник
@VivekSinha какую версию postgres вы используете? Я только что проверил ваш запрос и получил ответ "{1,2,3}". Я использую 12.1.
Алекси Теодор
А, я вижу @ alexi-theodore, что происходит у меня на конце. Я использовал нестандартный + модифицированный драйвер postgres. Когда я запрашиваю прямо в консоли, я вижу правильный результат! Извините за путаницу. Удален предыдущий комментарий и одобрен ответ!
Вивек Синха,
Наверное, полезно отметить, что array_remove поддерживается с 9.3
Анатолий Ругалев
9

Я добавляю это, хотя этот поток довольно старый, но я столкнулся с этим изящным трюком, который довольно хорошо работает с небольшими массивами. Он работает на Postgres 8.4+ без дополнительных библиотек или функций.

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

array_to_string()Метод фактически избавляется от нулей.

ced-b
источник
3

Как было предложено в комментариях, вы можете написать функцию для замены нулей в массиве, однако, как также указано в потоке, связанном с комментариями, этот вид побеждает эффективность агрегатной функции, если вам нужно создать агрегат , разделите его и снова объедините.

Я думаю, что сохранение нулей в массиве - это просто (возможно, нежелательная) функция Array_Agg. Чтобы этого избежать, можно использовать подзапросы:

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
источник
Спасибо. Но мне нужен case для обработки строк внутри данной группы, и подзапросы там будут неэффективными
Дауд
0

Это очень просто, просто сначала создайте новый оператор - (минус) для text [] :

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

И просто вычтите массив [null]:

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

Вот и все:

{Д, Н}

Миклош
источник
2
array_agg(x) FILTER (WHERE x is not null)кажется намного проще: dbfiddle.uk/… и вам действительно не нужна ваша собственная функция, вы можете просто использовать array_remove() dbfiddle.uk/…
a_horse_with_no_name
-6

Более важный вопрос - зачем использовать сразу все комбинации пользователей и групп. Гарантированно, что ваш пользовательский интерфейс не может обрабатывать все эти данные. Добавление подкачки к негабаритным данным также является плохой идеей. Попросите пользователей отфильтровать набор, прежде чем они увидят данные. Убедитесь, что ваш набор параметров JOIN находится в списке, чтобы они могли фильтровать производительность, если захотят. Иногда два запроса делают пользователей счастливее, если они оба работают быстро.

Майкл
источник