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

Предположим, продажи нужно разделить на сегменты по сумме заказа и получить число заказов в каждом сегменте. Как SQL определит, к какой группе отнести строку в этом запросе?

WITH orders(amount) AS (
    VALUES (50), (120), (80), (200)
)
SELECT
    CASE
        WHEN amount >= 100 THEN 'крупный'
        ELSE 'обычный'
    END AS segment,
    COUNT(*) AS orders_count
FROM orders
GROUP BY CASE
    WHEN amount >= 100 THEN 'крупный'
    ELSE 'обычный'
END;
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

SQL вычисляет выражение CASE для каждой исходной строки до формирования групп. Строки, для которых выражение возвращает одинаковое значение, попадают в одну группу; поэтому результат содержит отдельную строку для сегмента крупный и отдельную строку для обычный.

Группировка выполняется не по исходному amount, а по результату вычисляемого выражения, указанного в GROUP BY.

Исторический контекст

GROUP BY появился как механизм получения сводных данных из набора строк: вместо вывода каждой записи запрос формирует группы по ключу и вычисляет агрегаты для каждой группы. Вычисляемый ключ расширяет этот подход: группировать можно не только по физическому столбцу, но и по результату выражения.

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

Постановка проблемы

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

Неверно выбранный ключ группировки приводит к лишним строкам, неправильному количеству групп и агрегатам на слишком детальном уровне. При этом выражение сегмента должно быть логически одинаковым для всех строк, иначе одна и та же бизнес-категория может разделиться на несколько групп.

Подробное решение

Для каждой строки SQL сначала вычисляет выражение:

CASE WHEN amount >= 100 THEN 'крупный' ELSE 'обычный' END

Значения 120 и 200 дают ключ крупный, а 50 и 80обычный. Затем GROUP BY объединяет строки с одинаковыми ключами, а COUNT(*) считает строки внутри каждой группы.

Порядок логической обработки упрощённо выглядит так: FROM формирует исходные строки, затем вычисляется условие группировки, создаются группы, вычисляются агрегаты, и только после этого формируется список SELECT.

В стандартном SQL выражение группировки обычно указывают в GROUP BY явно. Некоторые СУБД разрешают использовать алиас segment, но переносимость такого варианта зависит от диалекта, поэтому повторение выражения является наиболее универсальным решением.

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

Следует отдельно продумать NULL. В данном запросе заказ с amount = NULL попадёт в обычный, потому что сравнение NULL >= 100 не является истинным, а срабатывает ветка ELSE. Если NULL означает отсутствие данных, лучше выделить его в отдельный сегмент через WHEN amount IS NULL.

Ситуация из практики

В интернет-магазине нужно сравнить число крупных и обычных заказов за месяц. Вариант с группировкой по amount даёт множество строк — по одной для каждого значения суммы — и непригоден для бизнес-отчёта.

Можно сначала добавить физический столбец segment в таблицу. Это ускоряет чтение отчётов и упрощает запросы, но требует синхронизации значения при изменении суммы заказа; устаревший сегмент создаст неверную статистику.

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

Для разового или регулярно пересчитываемого отчёта выбранный запрос с выражением в GROUP BY предпочтительнее: правило сегмента находится рядом с агрегацией, не дублируется в данных и гарантированно применяется к актуальной сумме. Результат содержит две группы с корректным количеством заказов.

Что кандидаты часто упускают

  1. Можно ли заменить выражение в GROUP BY алиасом segment из SELECT?

    Это зависит от СУБД и её правил разрешения имён. В логической модели SELECT обрабатывается после группировки, поэтому стандартный переносимый вариант — повторить выражение в GROUP BY или вычислить его во внешнем запросе/CTE. Рассчитывать на алиас без проверки диалекта не следует.

  2. Что произойдёт, если две разные ветки CASE возвращают одинаковый текст?

    Группировка использует итоговое значение ключа, а не путь, которым оно было получено. Поэтому одинаковые результаты объединятся в одну группу, даже если они появились из разных условий.

  3. Как сохранить отдельную группу для заказов с NULL?

    Нужно явно обработать NULL до общего условия:

    CASE WHEN amount IS NULL THEN 'нет суммы' WHEN amount >= 100 THEN 'крупный' ELSE 'обычный' END

    Без первой ветки NULL попадёт в ELSE. Это не ошибка SQL, но может скрыть проблему качества данных, смешав неизвестную сумму с обычным заказом.