Собираем строки через разделитель - STRING_AGG

Часто стоит задача по набору записей в таблице собрать строчку, перечислив значения через разделитель. Ярким примером может служить список адресов электронной почты для рассылки: 'ivanov_ii@gmail.com; petrov_pp@mail.ru; sidorov_ss@yandex.ru'.

Решить подобную задачу с помощью SQL легко. Достаточно воспользоваться функцией STRING_AGG. Она доступна и как агрегатная функция, и как оконная.

Синтаксис:

string_agg (выражение, разделитель [ORDER BY порядок сортировки значений])

Агрегатная функция

Сформируем список товаров в каждом каталоге:

SELECT c.name AS category_name,
       string_agg(p.name, ', ' ORDER BY p.name) AS products
  FROM category c,
       product p
 WHERE p.category_id = c.category_id
 GROUP BY c.category_id,
          c.name
# category_name products
1 Бытовая техника Пылесос S6, Холодильник A2
2 Фотоаппараты Lord Nikon 95, Nikon D750
3 Игровые консоли Nintendo, PlayStation, Xbox
4 Аудиотехника Наушники S3
5 Сотовые телефоны Моноблок C4, Слайдер B3
6 Ноутбуки Ультрабук X5
7 Рюкзаки Deepbox

Как получается такой результат?

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

SELECT c.category_id,
       c.name AS category_name,
       p.name AS product_name
  FROM category c,
       product p
 WHERE p.category_id = c.category_id
 ORDER BY c.category_id,
          c.name,
          p.name
# category_id category_name product_name
1 3 Бытовая техника Пылесос S6
2 3 Бытовая техника Холодильник A2
3 5 Фотоаппараты Lord Nikon 95
4 5 Фотоаппараты Nikon D750
5 6 Игровые консоли Nintendo
6 6 Игровые консоли PlayStation
7 6 Игровые консоли Xbox
8 7 Аудиотехника Наушники S3
9 8 Сотовые телефоны Моноблок C4
10 8 Сотовые телефоны Слайдер B3
11 9 Ноутбуки Ультрабук X5
12 10 Рюкзаки Deepbox

В запросе после WHERE написан GROUP BY. Значит в результате запроса будет столько строк, сколько встретилось уникальных значений перечисленных столбцов:

# category_id category_name
1 3 Бытовая техника
2 5 Фотоаппараты
3 6 Игровые консоли
4 7 Аудиотехника
5 8 Сотовые телефоны
6 9 Ноутбуки
7 10 Рюкзаки

Осталось разобраться с

SELECT c.name AS category_name,
       string_agg(p.name, ', ' ORDER BY p.name) AS products

С "c.name AS category_name" разбираться нечего. Это поле у нас есть в GROUP BY.

Смотрим на "string_agg(p.name, ', ' ORDER BY p.name) AS products". Так как over отсутсвует, значит это не оконная функция. Значит значение будет вычисляться в процессе группировки строк.

Внутри string_agg написано: "p.name, ', ' ORDER BY p.name". В переводе на русский это звучит так: для каждой группы из GROUP BY возьми все строки, отсортируй их по p.name и соедини названия продуктов p.name через запятую с пробелом ', '.

Так из

| # | category_id | category_name | product_name |
| -: | ---------: | :------------ | :---------- |
| 1 | 3 | Бытовая техника | Пылесос S6 |
| 2 | 3 | Бытовая техника | Холодильник A2 |

получается

# category_name products
1 Бытовая техника Пылесос S6, Холодильник A2

Оконная агрегатная функция

В виде оконной функции STRING_AGG используют довольно редко (кому она такая вообще нужна?). Но мы попробуем)

SELECT c.category_id,
       c.name AS category_name,
       p.name AS product_name,
       string_agg(p.name, ', ') over (PARTITION BY c.category_id) AS product_list
  FROM category c,
       product p
 WHERE p.category_id = c.category_id
 ORDER BY c.category_id,
          c.name,
          p.name
# category_id category_name product_name product_list
1 3 Бытовая техника Пылесос S6 Пылесос S6, Холодильник A2
2 3 Бытовая техника Холодильник A2 Пылесос S6, Холодильник A2
3 5 Фотоаппараты Lord Nikon 95 Nikon D750, Lord Nikon 95
4 5 Фотоаппараты Nikon D750 Nikon D750, Lord Nikon 95
5 6 Игровые консоли Nintendo Xbox, Nintendo, PlayStation
6 6 Игровые консоли PlayStation Xbox, Nintendo, PlayStation
7 6 Игровые консоли Xbox Xbox, Nintendo, PlayStation
8 7 Аудиотехника Наушники S3 Наушники S3
9 8 Сотовые телефоны Моноблок C4 Слайдер B3, Моноблок C4
10 8 Сотовые телефоны Слайдер B3 Слайдер B3, Моноблок C4
11 9 Ноутбуки Ультрабук X5 Ультрабук X5
12 10 Рюкзаки Deepbox Deepbox

В целом, результат получился весьма ожидаемый. Но есть одно но... мы не указали сортировку при формировании product_list.

Сделаем глупость, добавим ее в over:

SELECT c.category_id,
       c.name AS category_name,
       p.name AS product_name,
       string_agg(p.name, ', ') over (PARTITION BY c.category_id ORDER BY p.name) AS product_list
  FROM category c,
       product p
 WHERE p.category_id = c.category_id
 ORDER BY c.category_id,
          c.name,
          p.name
# category_id category_name product_name product_list
1 3 Бытовая техника Пылесос S6 Пылесос S6
2 3 Бытовая техника Холодильник A2 Пылесос S6, Холодильник A2
3 5 Фотоаппараты Lord Nikon 95 Lord Nikon 95
4 5 Фотоаппараты Nikon D750 Lord Nikon 95, Nikon D750
5 6 Игровые консоли Nintendo Nintendo
6 6 Игровые консоли PlayStation Nintendo, PlayStation
7 6 Игровые консоли Xbox Nintendo, PlayStation, Xbox
8 7 Аудиотехника Наушники S3 Наушники S3
9 8 Сотовые телефоны Моноблок C4 Моноблок C4
10 8 Сотовые телефоны Слайдер B3 Моноблок C4, Слайдер B3
11 9 Ноутбуки Ультрабук X5 Ультрабук X5
12 10 Рюкзаки Deepbox Deepbox

Как и ожидалось, результат получился не таким, какой хотели получить. В предыдущих заданиях рассказано, почему так.

Правильно указывать сортировка внутри самой string_agg:

SELECT c.category_id,
       c.name AS category_name,
       p.name AS product_name,
       string_agg(p.name, ', ' ORDER BY p.name) over (PARTITION BY c.category_id) AS product_list
  FROM category c,
       product p
 WHERE p.category_id = c.category_id
 ORDER BY c.category_id,
          c.name,
          p.name
error: aggregate ORDER BY is not implemented for window functions

Ну вот... Фича не реализована в PostgreSQL :(

11.9 Неуникальные значения в нарастающем итоге SUM + ORDER BY

11.11 WITHIN GROUP