STRING_AGG в SQL — агрегация строк
Коротко
STRING_AGG — агрегатная функция, которая объединяет значения из нескольких строк в одну строку с разделителем. Полезна для создания списков: «все города пользователя», «все категории заказа», «теги через запятую». В MySQL аналог — GROUP_CONCAT, в Oracle — LISTAGG. На собеседованиях встречается в задачах на агрегацию и формирование отчётов.
Синтаксис
-- PostgreSQL
STRING_AGG(expression, delimiter ORDER BY ...)
-- MySQL
GROUP_CONCAT(expression ORDER BY ... SEPARATOR delimiter)
-- ClickHouse
groupArray(expression) -- массив
arrayStringConcat(groupArray(expression), delimiter) -- строкаБазовый пример
-- Таблица orders
-- user_id | product
-- 1 | Ноутбук
-- 1 | Мышь
-- 1 | Клавиатура
-- 2 | Телефон
-- 2 | Чехол
-- PostgreSQL
SELECT
user_id,
STRING_AGG(product, ', ') AS products
FROM orders
GROUP BY user_id;
-- Результат:
-- user_id | products
-- 1 | Ноутбук, Мышь, Клавиатура
-- 2 | Телефон, ЧехолSTRING_AGG работает как COUNT или SUM — внутри GROUP BY, но вместо числа возвращает строку.
Сортировка внутри группы
По умолчанию порядок значений в STRING_AGG не определён. Для контроля порядка — ORDER BY внутри функции:
-- PostgreSQL: ORDER BY внутри STRING_AGG
SELECT
user_id,
STRING_AGG(product, ', ' ORDER BY product) AS products_sorted
FROM orders
GROUP BY user_id;
-- 1 | Клавиатура, Мышь, Ноутбук
-- MySQL: ORDER BY внутри GROUP_CONCAT
SELECT
user_id,
GROUP_CONCAT(product ORDER BY product SEPARATOR ', ') AS products_sorted
FROM orders
GROUP BY user_id;Можно сортировать по любому столбцу — по дате, по цене, по алфавиту:
SELECT
user_id,
STRING_AGG(product, ' → ' ORDER BY order_date) AS purchase_path
FROM orders
GROUP BY user_id;
-- 1 | Ноутбук → Мышь → КлавиатураDISTINCT — только уникальные
-- Без DISTINCT: дубликаты сохраняются
SELECT
user_id,
STRING_AGG(category, ', ') AS categories
FROM orders
GROUP BY user_id;
-- 1 | electronics, electronics, accessories
-- С DISTINCT: только уникальные
SELECT
user_id,
STRING_AGG(DISTINCT category, ', ') AS categories
FROM orders
GROUP BY user_id;
-- 1 | accessories, electronicsВ MySQL: GROUP_CONCAT(DISTINCT category ORDER BY category SEPARATOR ', ').
Практические примеры
Список тегов пользователя
SELECT
u.user_id,
u.name,
STRING_AGG(t.tag_name, ', ' ORDER BY t.tag_name) AS tags
FROM users u
JOIN user_tags t ON u.user_id = t.user_id
GROUP BY u.user_id, u.nameПуть пользователя по страницам
SELECT
session_id,
STRING_AGG(page_url, ' → ' ORDER BY event_time) AS user_path
FROM page_views
GROUP BY session_idПоказывает последовательность переходов: главная → каталог → товар → корзина → оплата. Полезно для анализа воронок.
Все заказы пользователя одной строкой
SELECT
u.name,
COUNT(o.order_id) AS order_count,
STRING_AGG(
o.order_id::VARCHAR || ' (' || o.amount::VARCHAR || '₽)',
'; '
ORDER BY o.order_date DESC
) AS orders_list
FROM users u
JOIN orders o ON u.user_id = o.user_id
GROUP BY u.nameРезультат: Иван | 3 | 105 (8000₽); 98 (3500₽); 72 (12000₽).
Email-рассылка: список получателей
SELECT
department,
STRING_AGG(email, '; ' ORDER BY last_name) AS email_list
FROM employees
WHERE is_active = TRUE
GROUP BY departmentГенерация SQL-списка (IN)
SELECT STRING_AGG(QUOTE_LITERAL(user_id::VARCHAR), ', ')
FROM active_users;
-- Результат: '101', '202', '303', '404'
-- Можно вставить в WHERE user_id IN (...)STRING_AGG как оконная функция
В PostgreSQL STRING_AGG можно использовать с OVER:
SELECT
order_date,
product,
STRING_AGG(product, ', ') OVER (
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_products
FROM ordersНакопительный список — на каждой строке видны все предыдущие продукты.
Аналоги в разных СУБД
| СУБД | Функция | Пример |
|---|---|---|
| PostgreSQL | STRING_AGG | STRING_AGG(col, ', ' ORDER BY col) |
| MySQL | GROUP_CONCAT | GROUP_CONCAT(col ORDER BY col SEPARATOR ', ') |
| SQL Server | STRING_AGG | STRING_AGG(col, ', ') WITHIN GROUP (ORDER BY col) |
| Oracle | LISTAGG | LISTAGG(col, ', ') WITHIN GROUP (ORDER BY col) |
| ClickHouse | groupArray + arrayStringConcat | arrayStringConcat(groupArray(col), ', ') |
| BigQuery | STRING_AGG | STRING_AGG(col, ', ' ORDER BY col) |
Обратная операция: разбить строку
-- PostgreSQL: строка → массив → строки
SELECT UNNEST(STRING_TO_ARRAY('A,B,C', ','));
-- A
-- B
-- C
-- MySQL
-- В MySQL 8+ можно через JSON_TABLE или рекурсивный CTE
-- PostgreSQL: REGEXP_SPLIT_TO_TABLE
SELECT REGEXP_SPLIT_TO_TABLE('Ноутбук, Мышь, Клавиатура', ',\s*');Типичные ошибки
NULL в данных. STRING_AGG игнорирует NULL. Если нужно включить: STRING_AGG(COALESCE(col, 'N/A'), ', ').
Слишком длинная строка. GROUP_CONCAT в MySQL имеет лимит 1024 байта по умолчанию. Увеличить: SET group_concat_max_len = 100000. В PostgreSQL лимита нет.
Забыли GROUP BY. STRING_AGG без GROUP BY объединит все строки таблицы в одну строку. Обычно нужна группировка.
Порядок не детерминирован. Без ORDER BY внутри STRING_AGG порядок значений непредсказуем. Всегда указывайте ORDER BY, если порядок важен.
Вопросы с собеседований
-- Как объединить значения группы в одну строку?
-- PostgreSQL: STRING_AGG(col, ', '). MySQL: GROUP_CONCAT(col SEPARATOR ', '). Обе — агрегатные функции, работают с GROUP BY.
-- Как отсортировать значения внутри STRING_AGG?
-- STRING_AGG(col, ', ' ORDER BY col) — PostgreSQL. GROUP_CONCAT(col ORDER BY col SEPARATOR ', ') — MySQL. ORDER BY внутри функции.
-- Как исключить дубликаты?
-- STRING_AGG(DISTINCT col, ', '). В MySQL: GROUP_CONCAT(DISTINCT col).
-- Аналог STRING_AGG в MySQL? -- GROUP_CONCAT. Синтаксис отличается: разделитель через SEPARATOR, ORDER BY перед SEPARATOR.
Потренируйтесь решать задачи — откройте тренажёр с 1500+ вопросами для подготовки к собеседованиям аналитиков.
FAQ
STRING_AGG vs ARRAY_AGG?
STRING_AGG возвращает строку: 'A, B, C'. ARRAY_AGG возвращает массив: {A, B, C}. Массив удобнее для дальнейшей обработки в SQL, строка — для отображения и экспорта.
Можно ли использовать STRING_AGG в WHERE?
Нельзя напрямую — это агрегатная функция, работает на этапе SELECT. Для фильтрации по результату используйте HAVING или подзапрос/CTE.
Как разбить строку обратно в строки?
PostgreSQL: UNNEST(STRING_TO_ARRAY(col, ',')) или REGEXP_SPLIT_TO_TABLE(col, ',\s*'). MySQL 8+: через JSON_TABLE или рекурсивный CTE.
Как тренироваться
STRING_AGG часто нужна для формирования отчётов и анализа путей пользователя. Задачи на агрегатные и оконные функции — в тренажёре Карьерник. Больше вопросов — в разделе с примерами.