как исключить нулевые значения в 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()Метод фактически избавляется от нулей.


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


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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.