GROUP BY ALL и ORDER BY ALL
В любом агрегирующем запросе есть скрытое правило, которое SQL заставляет соблюдать вручную: каждая неагрегатная колонка из SELECT обязана присутствовать в GROUP BY. Если в SELECT стоит region, product, sum(amount), то GROUP BY обязан содержать ровно region, product. Это правило — следствие семантики группировки: движок не знает, какое значение region показать для группы, если region не входит в ключ группировки.
Проблема в том, что список группировки — это дубликат неагрегатной части SELECT. Два списка, которые должны совпадать всегда, но синхронизировать их обязаны вы. Добавили колонку в SELECT — не забудьте добавить в GROUP BY. На широком запросе с шестью группировочными колонками это превращается в источник тупых, но раздражающих ошибок. GROUP BY ALL устраняет дубликат: DuckDB сам выводит ключи группировки из SELECT. ORDER BY ALL решает родственную, хоть и более редкую задачу для сортировки.
Дубликат, который приходится синхронизировать
Посмотрим на обычный агрегирующий запрос и на то, какая информация в нём повторяется:
SELECT
region,
product,
extract(year FROM order_date) AS yr,
sum(amount) AS revenue,
count(*) AS orders
FROM sales
GROUP BY region, product, extract(year FROM order_date);
Список GROUP BY — это в точности неагрегатные элементы SELECT: region, product и выражение extract(year FROM order_date). Агрегаты sum(amount) и count(*) в GROUP BY не входят. Вы написали одну и ту же информацию дважды, и теперь обязаны держать копии синхронными.
GROUP BY ALL заменяет ручной список ключевым словом ALL. DuckDB смотрит на SELECT, отделяет агрегатные выражения от неагрегатных и берёт все неагрегатные как ключи группировки:
-- Тот же запрос, GROUP BY выведен автоматически
SELECT
region,
product,
extract(year FROM order_date) AS yr,
sum(amount) AS revenue,
count(*) AS orders
FROM sales
GROUP BY ALL;
Результат идентичен предыдущему запросу. GROUP BY ALL развернулся в GROUP BY region, product, extract(year FROM order_date) ещё на этапе биндинга. Оптимизатор и физический план не отличаются никак — это чисто синтаксическое удобство, разворачиваемое до планирования.
Как DuckDB отличает агрегат от неагрегата
Логика GROUP BY ALL держится на одном решении: для каждого элемента SELECT — он агрегатный или нет? DuckDB определяет это так: элемент считается агрегатным, если он содержит вызов агрегатной функции (sum, count, avg, min, max, string_agg и т. д.). Всё остальное — неагрегатное и идёт в ключи группировки.
Важно, что проверка рекурсивная и работает по всему выражению. Рассмотрим граничные случаи:
-- amount * 2 — НЕ агрегат, попадёт в ключи группировки
-- sum(amount) — агрегат, в ключи не попадёт
SELECT amount * 2 AS double_amount, sum(qty) AS total_qty
FROM sales
GROUP BY ALL;
-- эквивалент: GROUP BY amount * 2
-- sum(amount) / count(*) — целиком агрегатное выражение
-- (оба под-вызова агрегатные), в ключи группировки НЕ идёт
SELECT region, sum(amount) / count(*) AS avg_amount
FROM sales
GROUP BY ALL;
-- эквивалент: GROUP BY region
Во втором запросе выражение sum(amount) / count(*) — это деление, но оба операнда агрегатные, и DuckDB видит выражение целиком как агрегатное. Оно не попадает в GROUP BY. Ключ группировки — только region.
GROUP BY ALL работает корректно, только когда SELECT действительно делится на агрегаты и чистые ключи. Если в запросе есть оконная функция или подзапрос, поведение может оказаться не таким, как вы ожидаете — оконные функции вычисляются после группировки и агрегатами не считаются. Для запросов с оконными функциями лучше указывать GROUP BY явно.
Что произойдёт, если в SELECT вообще нет агрегатов? Тогда GROUP BY ALL группирует по всем колонкам SELECT — это эквивалентно SELECT DISTINCT:
-- Нет агрегатов: GROUP BY ALL = группировка по всем колонкам = DISTINCT
SELECT region, product FROM sales GROUP BY ALL;
-- то же, что: SELECT DISTINCT region, product FROM sales;
Полезно зафиксировать ещё одно следствие. Поскольку GROUP BY ALL разворачивается на этапе биндинга — до планирования и оптимизации — оптимизатор и физический планировщик видят точно такое же дерево, как если бы вы написали GROUP BY явно. Это значит, что GROUP BY ALL не имеет ни малейшей стоимости в рантайме: это не «умный оператор, который что-то решает во время выполнения», а чисто текстовая подстановка на ранней стадии компиляции запроса. План, который покажет EXPLAIN, для запроса с GROUP BY ALL и для эквивалентного запроса с явным GROUP BY будет идентичным до последнего узла. Выбор между ними — исключительно вопрос читаемости исходного кода, а не производительности.
Есть и граница применимости, о которой важно помнить. GROUP BY ALL корректно работает, когда SELECT чисто делится на агрегаты и неагрегатные ключи. Если в SELECT присутствует оконная функция, картина усложняется: оконные функции вычисляются уже после группировки, агрегатами они не считаются, и выражение с оконной функцией GROUP BY ALL попытается затащить в ключи группировки — что почти наверняка не то, что вам нужно. Для запросов, смешивающих агрегаты и оконные функции, надёжнее указывать GROUP BY явным списком: так вы полностью контролируете, что именно стало ключом, и не зависите от того, как GROUP BY ALL классифицирует пограничное выражение.
ORDER BY ALL
ORDER BY ALL устроен проще: он сортирует результат по всем колонкам SELECT, слева направо, в порядке их следования. Это удобно для получения детерминированного, стабильно упорядоченного вывода — например, при сравнении результатов двух запросов или при подготовке снапшота для теста.
-- Сортировка по region, затем product, затем revenue
SELECT region, product, sum(amount) AS revenue
FROM sales
GROUP BY ALL
ORDER BY ALL;
-- эквивалент: ORDER BY region, product, revenue
ORDER BY ALL поддерживает модификаторы DESC и NULLS LAST, которые применяются ко всем колонкам сразу:
-- Все колонки по убыванию
SELECT region, sum(amount) AS revenue
FROM sales
GROUP BY ALL
ORDER BY ALL DESC;
В отличие от GROUP BY ALL, у ORDER BY ALL нет умной логики отделения агрегатов — он буквально берёт все выходные колонки по порядку. Поэтому он полезен в основном для двух сценариев: детерминированный вывод небольших результатов и быстрая сортировка при интерактивном исследовании. Для боевых запросов, где важен конкретный порядок сортировки, по-прежнему пишут явный ORDER BY с нужными колонками — ORDER BY ALL не даёт указать разное направление для разных колонок.
ORDER BY ALL на большом результате — это полная сортировка по всем колонкам, и она может быть дорогой. Сортировка не бесплатна: она материализует и упорядочивает весь результат. Не добавляйте ORDER BY ALL к запросу, который возвращает миллионы строк, если порядок вам на самом деле не нужен.
Сравнение двух конструкций
| Свойство | GROUP BY ALL | ORDER BY ALL |
|---|---|---|
| Что выводит | Неагрегатные колонки SELECT | Все колонки SELECT |
| Отделяет агрегаты | Да, исключает их из ключей | Нет, берёт всё подряд |
| Когда разворачивается | На этапе биндинга | На этапе биндинга |
| Главная польза | Убирает дубликат SELECT/GROUP BY | Детерминированный вывод |
| Когда лучше писать явно | Запросы с оконными функциями | Когда нужны разные направления сортировки |
Главная ценность — у GROUP BY ALL. Он устраняет настоящий дубликат: список группировки перестаёт быть копией SELECT, которую надо синхронизировать руками. Это убирает целый класс ошибок «забыл добавить колонку в GROUP BY» и делает diff при правке запроса чище — меняется только SELECT. ORDER BY ALL — более узкий инструмент, удобный для интерактивной работы и тестов, но в боевом коде его применяют реже.
Стоит обратить внимание, почему именно GROUP BY страдает от дублирования сильнее других клауз. Связь SELECT и GROUP BY — не стилистическая, а семантически обязательная: SQL физически не может вывести неагрегатную колонку, не входящую в ключ группировки, потому что для группы с несколькими разными значениями этой колонки непонятно, какое из них показать. То есть неагрегатная часть SELECT и список GROUP BY обязаны совпадать всегда, по правилам языка, — это не два независимых списка, а один список, записанный дважды. Любая ситуация «одна и та же информация записана в двух местах, и они обязаны быть синхронны» — это приглашение к ошибке: рано или поздно правят одно место и забывают другое. GROUP BY ALL устраняет именно эту структурную проблему — он делает производную информацию (ключи группировки) действительно производной, вычисляемой из единственного источника истины — списка SELECT. В этом смысле GROUP BY ALL ближе к принципу «не повторяйся» из инженерии кода, чем к синтаксическому сахару.
Попробуй сам
Создайте таблицу:
CREATE TABLE sales AS
SELECT * FROM (VALUES
('EU', 'A', DATE '2026-01-15', 120),
('EU', 'A', DATE '2026-02-10', 80),
('US', 'B', DATE '2026-01-20', 200),
('US', 'B', DATE '2026-03-05', 150),
) t(region, product, order_date, amount);
Задания:
- Напишите запрос с
region,product,sum(amount)иcount(*), используяGROUP BY ALL. Затем перепишите его с явнымGROUP BYи убедитесь в идентичности результата. - Добавьте в
SELECTколонкуextract(month FROM order_date) AS mon. Запрос сGROUP BY ALLдолжен заработать без правкиGROUP BY. Подумайте, сколько строк пришлось бы поменять в варианте с явнымGROUP BY. - Напишите
SELECT region, product FROM sales GROUP BY ALLбез агрегатов. Сравните результат сSELECT DISTINCT region, product. - Добавьте к запросу из задания 1
ORDER BY ALL DESCи посмотрите на порядок строк. - Объясните себе, почему выражение
sum(amount) / count(*)не попадает в ключи группировки приGROUP BY ALL.