sqlpostgresqlarraysaggregation

ARRAY_AGG in PostgreSQL: Collect Grouped Values into an Array with ORDER BY and FILTER

Fold a group's rows into an ordered array with ARRAY_AGG, filter and dedupe it, unnest it back to rows, and learn the MySQL workaround.

10 perc olvasásReferencesql · postgresql · arrays · aggregation · mysql
Ez a cikk jelenleg oroszul van — az angol fordítás folyamatban van.

В SQL мы обычно получаем результат в виде таблицы: одна строка за другой.

Например, если у пользователя несколько заказов, обычный запрос вернёт несколько строк:

user_id | order_id | amount
--------+----------+--------
1       | 101      | 1500
1       | 102      | 2300
1       | 103      | 900

Это нормальный табличный формат. Он удобен для фильтрации, соединений и расчётов.

Но иногда наружу хочется отдать данные иначе: не три строки на одного пользователя, а одну строку, внутри которой будет список его заказов.

Например:

user_id | order_ids
--------+---------------
1       | {101,102,103}

Вот для этого в PostgreSQL есть агрегатная функция ARRAY_AGG.

Она собирает значения из нескольких строк в один массив.

ARRAY_AGG удобно использовать, когда у одной сущности есть много связанных значений:

  • у пользователя много заказов;
  • у статьи много тегов;
  • у менеджера много подчинённых;
  • у товара много категорий;
  • у отдела много сотрудников;
  • у клиента много статусов или событий.

То есть ARRAY_AGG помогает превратить плоский результат в более компактный и вложенный.

Что делает ARRAY_AGG простыми словами

ARRAY_AGG берёт значения из строк внутри группы и складывает их в массив.

Допустим, есть таблица orders:

id  | user_id | amount
----+---------+--------
101 | 1       | 1500
102 | 1       | 2300
103 | 1       | 900
201 | 2       | 700
202 | 2       | 1200

Если мы хотим собрать суммы заказов по каждому пользователю в массив, можно написать:

SELECT
  user_id,
  ARRAY_AGG(amount) AS amounts
FROM orders
GROUP BY user_id;

Результат:

user_id | amounts
--------+----------------
1       | {1500,2300,900}
2       | {700,1200}

Что произошло:

  1. GROUP BY user_id собрал строки по пользователям.
  2. ARRAY_AGG(amount) взял все amount внутри каждой группы.
  3. PostgreSQL сложил эти значения в массив.

То есть вместо нескольких строк на одного пользователя мы получили одну строку и массив значений внутри неё.

Базовый синтаксис ARRAY_AGG

Синтаксис простой:

ARRAY_AGG(expression)

Где expression — это значение, которое нужно собрать в массив.

Например:

ARRAY_AGG(amount)
ARRAY_AGG(id)
ARRAY_AGG(email)
ARRAY_AGG(status)
ARRAY_AGG(created_at)

Как и другие агрегаты, ARRAY_AGG обычно используется вместе с GROUP BY.

Пример:

SELECT
  user_id,
  ARRAY_AGG(id) AS order_ids
FROM orders
GROUP BY user_id;

Результат:

user_id | order_ids
--------+---------------
1       | {101,102,103}
2       | {201,202}

Здесь для каждого пользователя мы собрали список его заказов.

Тип массива зависит от типа значения

ARRAY_AGG возвращает массив того типа, который вы собираете.

Если собираете числа:

ARRAY_AGG(amount)

результат будет массивом чисел, например:

{1500,2300,900}

Если собираете текст:

ARRAY_AGG(status)

результат будет массивом строк:

{paid,failed,refunded}

Если собираете даты:

ARRAY_AGG(created_at)

результат будет массивом дат или timestamp-значений.

Это важно понимать: ARRAY_AGG не превращает всё автоматически в текст. Он сохраняет тип агрегируемого выражения.

Почему ORDER BY внутри ARRAY_AGG очень важен

Самая частая ошибка с ARRAY_AGG — забыть про порядок.

Например:

SELECT
  user_id,
  ARRAY_AGG(amount) AS amounts
FROM orders
GROUP BY user_id;

Такой запрос соберёт суммы заказов в массив, но порядок элементов внутри массива не гарантирован.

Сегодня вы можете увидеть:

{1500,2300,900}

А после изменения плана запроса, индекса или объёма данных порядок может стать другим:

{900,1500,2300}

Для отчёта или API это может быть проблемой.

Поэтому в реальном коде лучше почти всегда задавать порядок явно:

SELECT
  user_id,
  ARRAY_AGG(amount ORDER BY created_at) AS amounts
FROM orders
GROUP BY user_id;

Теперь суммы будут собраны в порядке создания заказов.

Если нужны последние заказы первыми:

SELECT
  user_id,
  ARRAY_AGG(amount ORDER BY created_at DESC) AS amounts
FROM orders
GROUP BY user_id;

Главное правило:

Если порядок элементов в массиве важен, всегда пишите ORDER BY внутри ARRAY_AGG.

Не снаружи запроса, а именно внутри агрегата.

ORDER BY снаружи и внутри — это разные вещи

Важно не путать два разных ORDER BY.

Вот такой ORDER BY сортирует строки результата:

SELECT
  user_id,
  ARRAY_AGG(amount) AS amounts
FROM orders
GROUP BY user_id
ORDER BY user_id;

Он влияет на порядок пользователей в итоговой таблице.

Например:

user_id | amounts
--------+----------------
1       | {1500,2300,900}
2       | {700,1200}

Но он не гарантирует порядок элементов внутри массива.

А вот такой ORDER BY влияет именно на порядок элементов в массиве:

SELECT
  user_id,
  ARRAY_AGG(amount ORDER BY created_at) AS amounts
FROM orders
GROUP BY user_id
ORDER BY user_id;

Здесь:

ORDER BY created_at

внутри ARRAY_AGG сортирует заказы внутри массива.

А:

ORDER BY user_id

в конце запроса сортирует строки результата по пользователям.

Можно запомнить так:

ORDER BY внутри ARRAY_AGG сортирует элементы массива. ORDER BY в конце запроса сортирует строки результата.

Пример: история заказов пользователя

Допустим, есть таблица orders:

id  | user_id | amount | status | created_at
----+---------+--------+--------+---------------------
101 | 1       | 1500   | paid   | 2026-06-01 10:00:00
102 | 1       | 2300   | paid   | 2026-06-03 12:00:00
103 | 1       | 900    | failed | 2026-06-02 09:00:00
201 | 2       | 700    | paid   | 2026-06-01 11:00:00

Хотим получить по каждому пользователю список заказов от новых к старым.

SELECT
  user_id,
  ARRAY_AGG(id ORDER BY created_at DESC) AS order_ids
FROM orders
GROUP BY user_id;

Результат:

user_id | order_ids
--------+---------------
1       | {102,103,101}
2       | {201}

У пользователя 1 заказ 102 идёт первым, потому что он самый новый.

Такой результат уже удобно отдавать в API или использовать в отчёте.

FILTER внутри ARRAY_AGG

ARRAY_AGG можно сочетать с FILTER (WHERE ...).

Это позволяет собирать в массив не все значения, а только те, которые подходят под условие.

Например, собрать только оплаченные заказы:

SELECT
  user_id,
  ARRAY_AGG(id ORDER BY created_at)
    FILTER (WHERE status = 'paid') AS paid_order_ids
FROM orders
GROUP BY user_id;

Результат может быть таким:

user_id | paid_order_ids
--------+----------------
1       | {101,102}
2       | {201}

Заказ 103 не попал в массив, потому что у него статус failed.

Это читается очень понятно:

собери id заказов по времени,
но только там, где status = 'paid'

Такой подход удобнее, чем сначала делать подзапрос, а потом агрегировать уже отфильтрованные строки.

NULL внутри ARRAY_AGG

ARRAY_AGG собирает NULL как обычный элемент массива.

Например, есть данные:

user_id | status
--------+---------
1       | paid
1       | NULL
1       | failed

Запрос:

SELECT
  user_id,
  ARRAY_AGG(status) AS statuses
FROM orders
GROUP BY user_id;

может вернуть:

user_id | statuses
--------+--------------------
1       | {paid,NULL,failed}

Если NULL в массиве не нужен, добавьте FILTER:

SELECT
  user_id,
  ARRAY_AGG(status)
    FILTER (WHERE status IS NOT NULL) AS statuses
FROM orders
GROUP BY user_id;

Теперь результат будет:

user_id | statuses
--------+---------------
1       | {paid,failed}

Правило простое:

Если не хотите видеть NULL внутри массива, явно отфильтруйте его через FILTER.

ARRAY_AGG и LEFT JOIN: почему иногда получается {NULL}

Есть важная ловушка, которая часто встречается в реальных запросах.

Допустим, мы хотим получить всех пользователей и массив их заказов.

SELECT
  u.id,
  u.email,
  ARRAY_AGG(o.id ORDER BY o.created_at) AS order_ids
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.email;

Если у пользователя есть заказы, всё хорошо.

Но если заказов нет, LEFT JOIN всё равно создаёт одну строку для пользователя, просто колонки заказа будут NULL.

В результате ARRAY_AGG(o.id) может вернуть не пустой массив, а массив с одним NULL:

{NULL}

Для API это обычно неудобно. Клиент ждёт пустой список заказов, а получает список из одного пустого значения.

Лучше писать так:

SELECT
  u.id,
  u.email,
  COALESCE(
    ARRAY_AGG(o.id ORDER BY o.created_at)
      FILTER (WHERE o.id IS NOT NULL),
    ARRAY[]::integer[]
  ) AS order_ids
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.email;

Что здесь происходит:

FILTER (WHERE o.id IS NOT NULL)

убирает пустую строку, которая появилась из-за LEFT JOIN.

А:

COALESCE(..., ARRAY[]::integer[])

заменяет NULL на пустой массив.

Результат для пользователя без заказов будет:

{}

Это обычно именно то, что нужно для отчёта или API.

Почему нужен ARRAY[]::integer[]

В примере выше мы написали:

ARRAY[]::integer[]

Почему нельзя просто написать пустой массив?

Потому что PostgreSQL должен понимать тип массива.

Пустой массив сам по себе не содержит элементов, поэтому база не всегда может догадаться, какой это массив:

  • массив чисел;
  • массив текста;
  • массив дат;
  • массив UUID.

Поэтому мы явно говорим:

ARRAY[]::integer[]

Это пустой массив целых чисел.

Если собираете текстовые значения, будет так:

ARRAY[]::text[]

Если собираете UUID:

ARRAY[]::uuid[]

Если собираете numeric:

ARRAY[]::numeric[]

Для новичка можно запомнить:

Если через COALESCE подставляете пустой массив, укажите его тип явно.

ARRAY_AGG(DISTINCT ...): убрать повторы

Иногда в массиве нужны только уникальные значения.

Например, хотим получить список статусов заказов по каждому пользователю.

Таблица:

user_id | status
--------+---------
1       | paid
1       | paid
1       | failed
1       | paid
2       | refunded
2       | paid

Обычный ARRAY_AGG(status) вернёт повторы:

{paid,paid,failed,paid}

Чтобы убрать дубликаты, используем DISTINCT:

SELECT
  user_id,
  ARRAY_AGG(DISTINCT status) AS statuses
FROM orders
GROUP BY user_id;

Результат:

user_id | statuses
--------+-----------------
1       | {failed,paid}
2       | {paid,refunded}

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

SELECT
  user_id,
  ARRAY_AGG(DISTINCT status ORDER BY status) AS statuses
FROM orders
GROUP BY user_id;

Теперь статусы будут отсортированы.

Ловушка DISTINCT + ORDER BY

С ARRAY_AGG(DISTINCT ...) есть важное ограничение.

Такой запрос может не сработать:

SELECT
  user_id,
  ARRAY_AGG(DISTINCT amount ORDER BY created_at) AS amounts
FROM orders
GROUP BY user_id;

Почему?

Потому что мы говорим:

собери уникальные amount

Но сортировать пытаемся по created_at.

После DISTINCT amount у PostgreSQL остаются уникальные суммы, а не отдельные строки заказов с датами. Поэтому непонятно, какую дату использовать для сортировки одинаковой суммы.

Правильнее сортировать по тому же выражению, которое агрегируем:

SELECT
  user_id,
  ARRAY_AGG(DISTINCT amount ORDER BY amount) AS amounts
FROM orders
GROUP BY user_id;

Если нужна более сложная логика, например уникальные суммы в порядке первого появления, лучше сначала подготовить данные в подзапросе, а потом уже агрегировать.

Пример: статусы заказов по странам

Допустим, есть таблицы users и orders.

Хотим узнать, какие статусы заказов встречались в каждой стране.

SELECT
  u.country,
  ARRAY_AGG(DISTINCT o.status ORDER BY o.status) AS seen_statuses
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.country;

Результат может быть таким:

country | seen_statuses
--------+-------------------------
Germany | {paid,refunded}
Vietnam | {failed,paid,refunded}

Такой отчёт удобно использовать для быстрой проверки данных: видно, какие состояния вообще встречались в каждой группе.

Несколько массивов в одной строке

Иногда хочется собрать несколько массивов одновременно.

Например, по каждому пользователю:

  • массив order_ids;
  • массив amounts;
  • массив statuses.
SELECT
  u.id,
  u.email,
  ARRAY_AGG(o.id ORDER BY o.created_at DESC) AS order_ids,
  ARRAY_AGG(o.amount ORDER BY o.created_at DESC) AS amounts,
  ARRAY_AGG(o.status ORDER BY o.created_at DESC) AS statuses
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.email;

Это может вернуть:

id | email         | order_ids     | amounts        | statuses
---+---------------+---------------+----------------+---------------------
1  | anna@mail.com | {103,102,101} | {900,2300,1500}| {failed,paid,paid}

Здесь очень важно использовать одинаковый ORDER BY во всех массивах.

Почему?

Потому что связь между массивами держится только на позиции элемента.

Например:

order_ids[1] соответствует amounts[1] и statuses[1]

Если в одном массиве сортировка будет по created_at DESC, а в другом по amount, элементы перестанут соответствовать друг другу.

Это опасная ошибка.

Правило:

Если собираете несколько связанных массивов, используйте одинаковый ORDER BY в каждом ARRAY_AGG.

Но ещё лучше для таких случаев часто использовать JSON или массив составных значений, чтобы не держать связь только на позиции.

Лучше не злоупотреблять параллельными массивами

Параллельные массивы выглядят удобно:

order_ids = {103,102,101}
amounts   = {900,2300,1500}
statuses  = {failed,paid,paid}

Но они хрупкие.

Если порядок где-то изменился, связь между заказом, суммой и статусом ломается.

Более надёжный вариант — собрать каждый заказ как отдельный объект. Например, через JSON:

SELECT
  u.id,
  u.email,
  JSONB_AGG(
    JSONB_BUILD_OBJECT(
      'id', o.id,
      'amount', o.amount,
      'status', o.status,
      'created_at', o.created_at
    )
    ORDER BY o.created_at DESC
  ) AS orders
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.email;

Результат будет похож на список объектов:

[
  {"id": 103, "amount": 900, "status": "failed", "created_at": "2026-06-03"},
  {"id": 102, "amount": 2300, "status": "paid", "created_at": "2026-06-02"},
  {"id": 101, "amount": 1500, "status": "paid", "created_at": "2026-06-01"}
]

Для API такой формат часто удобнее, чем несколько отдельных массивов.

Но если вам нужен именно простой список значений, ARRAY_AGG остаётся отличным решением.

Пример: подчинённые менеджера

ARRAY_AGG хорошо подходит для иерархических данных.

Допустим, есть таблица employees:

id | name  | manager_id | salary
---+-------+------------+--------
1  | Anna  | NULL       | 250000
2  | Bob   | 1          | 180000
3  | Kate  | 1          | 170000
4  | Tom   | 2          | 120000

Хотим собрать список подчинённых для каждого менеджера:

SELECT
  manager_id,
  ARRAY_AGG(name ORDER BY salary DESC) AS reports
FROM employees
WHERE manager_id IS NOT NULL
GROUP BY manager_id;

Результат:

manager_id | reports
-----------+-------------
1          | {Bob,Kate}
2          | {Tom}

Здесь сотрудники внутри массива отсортированы по зарплате от большей к меньшей.

Это хороший пример, где массив делает результат компактнее и удобнее для чтения.

Пример: теги статьи

Допустим, есть статьи и теги.

Таблица article_tags:

article_id | tag
-----------+------------
1          | sql
1          | postgresql
1          | arrays
2          | mysql
2          | json

Хотим получить одну строку на статью и массив тегов:

SELECT
  article_id,
  ARRAY_AGG(tag ORDER BY tag) AS tags
FROM article_tags
GROUP BY article_id;

Результат:

article_id | tags
-----------+-------------------------
1          | {arrays,postgresql,sql}
2          | {json,mysql}

Это удобно для блога, каталога, карточек товаров и любых сущностей, где есть набор меток.

UNNEST: развернуть массив обратно в строки

Обратная операция к ARRAY_AGGUNNEST.

Если ARRAY_AGG собирает строки в массив, то UNNEST разворачивает массив обратно в строки.

Пример:

SELECT UNNEST(ARRAY['sql', 'postgresql', 'arrays']) AS tag;

Результат:

tag
----------
sql
postgresql
arrays

Если в таблице articles есть колонка tags с массивом тегов:

id | title             | tags
---+-------------------+-------------------------
1  | PostgreSQL arrays | {sql,postgresql,arrays}

можно развернуть теги так:

SELECT
  id,
  UNNEST(tags) AS tag
FROM articles;

Результат:

id | tag
---+------------
1  | sql
1  | postgresql
1  | arrays

Теперь с тегами снова можно работать как со строками: фильтровать, группировать, соединять с другими таблицами.

WITH ORDINALITY: сохранить позицию элемента

Иногда важно не только развернуть массив, но и знать позицию элемента.

Для этого используется WITH ORDINALITY.

SELECT
  a.id,
  t.tag,
  t.pos
FROM articles a,
UNNEST(a.tags) WITH ORDINALITY AS t(tag, pos);

Результат:

id | tag        | pos
---+------------+-----
1  | sql        | 1
1  | postgresql | 2
1  | arrays     | 3

pos показывает номер элемента в массиве.

Это полезно, если порядок тегов, шагов, статусов или событий имеет смысл.

ARRAY_AGG как финальный слой выдачи

Важно понимать, где ARRAY_AGG особенно полезен.

Внутри базы данные часто лучше хранить нормально, в отдельных строках.

Например, теги статьи лучше хранить так:

article_id | tag
-----------+----------
1          | sql
1          | postgres
1          | arrays

Так их проще:

  • индексировать;
  • фильтровать;
  • обновлять;
  • удалять;
  • соединять с другими таблицами;
  • проверять на дубли.

А вот на выходе, например для API или отчёта, можно собрать их в массив:

SELECT
  article_id,
  ARRAY_AGG(tag ORDER BY tag) AS tags
FROM article_tags
GROUP BY article_id;

То есть хороший паттерн такой:

Хранить данные строками, а собирать массив на чтении.

ARRAY_AGG отлично подходит как финальный слой представления данных.

Когда массив в колонке — плохая идея

PostgreSQL умеет хранить массивы прямо в колонках.

Например:

id | tags
---+-------------------------
1  | {sql,postgresql,arrays}

Иногда это удобно.

Но не стоит использовать массивы как способ спрятать плохо продуманную связь.

Если по элементам массива нужно часто:

  • искать;
  • фильтровать;
  • джойнить;
  • обновлять отдельные элементы;
  • считать статистику;
  • проверять уникальность;
  • строить связи с другими таблицами,

то отдельная дочерняя таблица почти всегда лучше.

Например, вместо массива тегов в колонке:

articles(id, title, tags)

часто лучше сделать таблицу:

article_tags(article_id, tag)

А массив собирать уже в запросе через ARRAY_AGG.

Можно запомнить так:

Массив хорош для выдачи. Отдельные строки хороши для хранения и анализа.

ARRAY_AGG и пустые группы

Если агрегату нечего собрать, результатом будет NULL, а не пустой массив.

Это часто всплывает в запросах с FILTER или подзапросами.

Например:

SELECT
  user_id,
  ARRAY_AGG(id) FILTER (WHERE status = 'paid') AS paid_order_ids
FROM orders
GROUP BY user_id;

Если у пользователя есть заказы, но ни один не оплачен, paid_order_ids может быть NULL.

Если для API нужен пустой массив, используйте COALESCE:

SELECT
  user_id,
  COALESCE(
    ARRAY_AGG(id ORDER BY created_at)
      FILTER (WHERE status = 'paid'),
    ARRAY[]::integer[]
  ) AS paid_order_ids
FROM orders
GROUP BY user_id;

Теперь вместо NULL будет пустой массив:

{}

Для клиентского кода это часто удобнее: можно всегда ожидать список, а не проверять отдельно null.

ARRAY_AGG в подзапросе для каждого пользователя

Иногда удобно собрать массив через подзапрос.

Например, получить всех пользователей, а рядом — массив их заказов.

SELECT
  u.id,
  u.email,
  COALESCE(o.order_ids, ARRAY[]::integer[]) AS order_ids
FROM users u
LEFT JOIN (
  SELECT
    user_id,
    ARRAY_AGG(id ORDER BY created_at DESC) AS order_ids
  FROM orders
  GROUP BY user_id
) o ON o.user_id = u.id;

Такой подход иногда читается проще, чем большой запрос с LEFT JOIN и GROUP BY по всем колонкам пользователя.

Логика такая:

  1. во внутреннем запросе собираем заказы по user_id;
  2. потом присоединяем готовый массив к пользователю;
  3. если заказов нет, через COALESCE возвращаем пустой массив.

ARRAY_AGG в PostgreSQL и JSON в MySQL

В PostgreSQL есть настоящий тип массива, поэтому ARRAY_AGG возвращает именно массив.

В MySQL настоящих массивов как отдельного типа нет. Поэтому похожую задачу часто решают через JSON.

Например:

SELECT
  user_id,
  JSON_ARRAYAGG(amount) AS amounts
FROM orders
GROUP BY user_id;

Результатом будет JSON-массив.

Например:

[1500, 2300, 900]

Это похоже по смыслу, но это уже не SQL-массив PostgreSQL, а JSON-значение.

Также в MySQL иногда используют GROUP_CONCAT.

SELECT
  user_id,
  GROUP_CONCAT(amount ORDER BY created_at SEPARATOR ',') AS amounts
FROM orders
GROUP BY user_id;

Но GROUP_CONCAT возвращает строку:

1500,2300,900

Это не массив. С такой строкой сложнее безопасно работать дальше, особенно если значения сами могут содержать разделители.

Поэтому если нужен именно структурированный список, в MySQL чаще лучше смотреть в сторону JSON.

Аналог в ClickHouse

В ClickHouse похожую задачу решает функция groupArray.

Например:

SELECT
  user_id,
  groupArray(amount) AS amounts
FROM orders
GROUP BY user_id;

Она собирает значения группы в массив.

Для сортировки можно использовать функции вроде arraySort или специальные варианты в зависимости от задачи и версии ClickHouse.

Пример:

SELECT
  user_id,
  arraySort(groupArray(amount)) AS amounts
FROM orders
GROUP BY user_id;

Общая идея такая же:

собрать несколько строк одной группы в один массив.

Но синтаксис отличается от PostgreSQL.

Практические шаблоны

Собрать id заказов по пользователю

SELECT
  user_id,
  ARRAY_AGG(id ORDER BY created_at) AS order_ids
FROM orders
GROUP BY user_id;

Собрать суммы заказов по пользователю

SELECT
  user_id,
  ARRAY_AGG(amount ORDER BY created_at) AS amounts
FROM orders
GROUP BY user_id;

Собрать только оплаченные заказы

SELECT
  user_id,
  ARRAY_AGG(id ORDER BY created_at)
    FILTER (WHERE status = 'paid') AS paid_order_ids
FROM orders
GROUP BY user_id;

Убрать NULL из массива

SELECT
  user_id,
  ARRAY_AGG(status)
    FILTER (WHERE status IS NOT NULL) AS statuses
FROM orders
GROUP BY user_id;

Вернуть пустой массив вместо NULL

SELECT
  user_id,
  COALESCE(
    ARRAY_AGG(id ORDER BY created_at)
      FILTER (WHERE status = 'paid'),
    ARRAY[]::integer[]
  ) AS paid_order_ids
FROM orders
GROUP BY user_id;

Собрать уникальные статусы

SELECT
  user_id,
  ARRAY_AGG(DISTINCT status ORDER BY status) AS statuses
FROM orders
GROUP BY user_id;

Собрать теги статьи

SELECT
  article_id,
  ARRAY_AGG(tag ORDER BY tag) AS tags
FROM article_tags
GROUP BY article_id;

Собрать подчинённых менеджера

SELECT
  manager_id,
  ARRAY_AGG(name ORDER BY salary DESC) AS reports
FROM employees
WHERE manager_id IS NOT NULL
GROUP BY manager_id;

Развернуть массив обратно в строки

SELECT
  id,
  UNNEST(tags) AS tag
FROM articles;

Развернуть массив с позицией элемента

SELECT
  a.id,
  t.tag,
  t.pos
FROM articles a,
UNNEST(a.tags) WITH ORDINALITY AS t(tag, pos);

Что важно запомнить

ARRAY_AGG собирает значения из нескольких строк в массив.

Пример:

SELECT
  user_id,
  ARRAY_AGG(id ORDER BY created_at) AS order_ids
FROM orders
GROUP BY user_id;

Так можно получить одну строку на пользователя и список его заказов внутри массива.

Главные правила:

  • ARRAY_AGG используется как агрегатная функция;
  • чаще всего он работает вместе с GROUP BY;
  • без ORDER BY порядок элементов массива не гарантирован;
  • ORDER BY лучше писать внутри ARRAY_AGG;
  • NULL попадает в массив, если его не отфильтровать;
  • если агрегату нечего собрать, результат может быть NULL;
  • для пустого массива используйте COALESCE(..., ARRAY[]::type[]);
  • ARRAY_AGG(DISTINCT ...) убирает повторы;
  • при нескольких связанных массивах используйте одинаковый порядок;
  • если нужна вложенная структура для API, иногда лучше использовать JSONB_AGG;
  • для хранения связей отдельная таблица часто лучше массива в колонке.

Короткий вывод

ARRAY_AGG — это удобный способ собрать несколько строк в один массив.

Он особенно полезен, когда в базе данные хранятся нормально, строками, но на выходе хочется получить компактный список.

Например:

SELECT
  user_id,
  ARRAY_AGG(id ORDER BY created_at DESC) AS order_ids
FROM orders
GROUP BY user_id;

Такой запрос превращает много заказов пользователя в один массив заказов.

Главная мысль:

храните данные строками, а массив собирайте на чтении.

И почти всегда задавайте порядок:

ARRAY_AGG(value ORDER BY created_at)

Так результат будет стабильным, понятным и безопасным для отчётов, API и дальнейшей обработки.

Gyakorolj valós feladatokon

Oldj meg feladatokat az SQL-trénerben azonnali értékeléssel és tippekkel.

Tréner megnyitása