Часто стоит задача по набору записей в таблице собрать строчку, перечислив значения через разделитель. Ярким примером может служить список адресов электронной почты для рассылки: '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 :(