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 часто нужна для формирования отчётов и анализа путей пользователя. Задачи на агрегатные и оконные функции — в тренажёре Карьерник. Больше вопросов — в разделе с примерами.