Предположим, продажи нужно разделить на сегменты по сумме заказа и получить число заказов в каждом сегменте. Как 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;
SQL вычисляет выражение CASE для каждой исходной строки до формирования групп. Строки, для которых выражение возвращает одинаковое значение, попадают в одну группу; поэтому результат содержит отдельную строку для сегмента крупный и отдельную строку для обычный.
Группировка выполняется не по исходному amount, а по результату вычисляемого выражения, указанного в GROUP BY.
GROUP BY появился как механизм получения сводных данных из набора строк: вместо вывода каждой записи запрос формирует группы по ключу и вычисляет агрегаты для каждой группы. Вычисляемый ключ расширяет этот подход: группировать можно не только по физическому столбцу, но и по результату выражения.
Это позволяет строить отчёты по бизнес-категориям, которых нет в таблице напрямую: возрастным диапазонам, ценовым сегментам, месяцам или статусам, рассчитанным по нескольким полям.
Сегмент заказа является производным значением. Если сгруппировать данные по amount, каждый размер заказа станет отдельной группой, и сводка по категориям не получится.
Неверно выбранный ключ группировки приводит к лишним строкам, неправильному количеству групп и агрегатам на слишком детальном уровне. При этом выражение сегмента должно быть логически одинаковым для всех строк, иначе одна и та же бизнес-категория может разделиться на несколько групп.
Для каждой строки SQL сначала вычисляет выражение:
Значения 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 предпочтительнее: правило сегмента находится рядом с агрегацией, не дублируется в данных и гарантированно применяется к актуальной сумме. Результат содержит две группы с корректным количеством заказов.
Можно ли заменить выражение в GROUP BY алиасом segment из SELECT?
Это зависит от СУБД и её правил разрешения имён. В логической модели SELECT обрабатывается после группировки, поэтому стандартный переносимый вариант — повторить выражение в GROUP BY или вычислить его во внешнем запросе/CTE. Рассчитывать на алиас без проверки диалекта не следует.
Что произойдёт, если две разные ветки CASE возвращают одинаковый текст?
Группировка использует итоговое значение ключа, а не путь, которым оно было получено. Поэтому одинаковые результаты объединятся в одну группу, даже если они появились из разных условий.
Как сохранить отдельную группу для заказов с NULL?
Нужно явно обработать NULL до общего условия:
Без первой ветки NULL попадёт в ELSE. Это не ошибка SQL, но может скрыть проблему качества данных, смешав неизвестную сумму с обычным заказом.