FROM-first синтаксис и trailing commas
Стандартный SQL заставляет писать запрос в порядке, который не совпадает ни с порядком исполнения, ни с порядком мышления. Вы пишете SELECT первым, хотя движок сначала читает таблицу (FROM), потом фильтрует (WHERE), потом группирует, и только в самом конце вычисляет список колонок SELECT. И когда вы пишете запрос с нуля, вы тоже сначала думаете «откуда данные», а уже потом «какие колонки мне нужны». Порядок SELECT ... FROM ... — это исторический артефакт SQL-86, который пережил сорок лет просто по инерции.
DuckDB разрывает эту инерцию. Его диалект — расширение PostgreSQL-совместимого SQL, и одно из первых расширений «friendly SQL» — возможность ставить FROM перед SELECT. Это не косметика: FROM-first меняет то, как пишутся, читаются и автодополняются запросы. В этом уроке разберём три связанные вещи: FROM-first порядок, FROM вообще без SELECT, и trailing commas — мелочь, которая убирает целый класс ошибок при правках кода.
Порядок исполнения против порядка написания
Логический порядок вычисления SQL-запроса фиксирован стандартом и не зависит от того, в каком порядке вы написали клаузы. Движок всегда идёт так:
Обратите внимание: SELECT стоит пятым из шести шагов. Именно поэтому в стандартном SQL алиас, заданный в SELECT, не виден в WHERE — WHERE уже отработал, когда движок дошёл до SELECT. FROM-first синтаксис приводит написание запроса в соответствие с этим порядком: вы пишете источник первым, потому что движок и так читает его первым.
В DuckDB обе формы валидны и абсолютно эквивалентны — это просто два способа записать один и тот же запрос:
-- Классический порядок
SELECT region, count(*) AS orders
FROM sales
WHERE amount > 100
GROUP BY region;
-- FROM-first: тот же запрос, источник вынесен вперёд
FROM sales
WHERE amount > 100
SELECT region, count(*) AS orders
GROUP BY region;
Парсер DuckDB принимает оба варианта и строит из них идентичное логическое дерево. Никакой разницы в производительности нет — оптимизатор видит один и тот же план. Разница только в эргономике написания.
FROM без SELECT
Раз SELECT логически идёт после FROM, возникает вопрос: а обязателен ли он вообще? В DuckDB — нет. Если вы написали только FROM, движок подставляет SELECT * неявно:
-- Полная форма
SELECT * FROM sales;
-- Сокращённая: SELECT * подразумевается
FROM sales;
Обе строки возвращают всю таблицу. Это мелочь, но она радикально ускоряет интерактивную работу. Когда вы исследуете незнакомый датасет в CLI или ноутбуке, главная команда — «покажи, что в таблице». FROM orders; — это весь запрос. То же работает и с функциями чтения файлов:
-- Посмотреть содержимое Parquet-файла одной командой
FROM 'data/orders.parquet';
-- С фильтром и лимитом, по-прежнему без SELECT
FROM 'data/orders.parquet'
WHERE order_date >= '2026-01-01'
LIMIT 10;
Последний запрос полностью валиден: FROM, WHERE, LIMIT без единого SELECT. Движок понимает, что выходные колонки не заданы, и берёт все. Это превращает DuckDB CLI в удобный инструмент быстрого просмотра данных — FROM 'file' вместо SELECT * FROM read_parquet('file').
FROM-first особенно полезен для автодополнения в IDE и CLI. Когда источник написан первым, редактор уже знает, какие таблицы и колонки существуют, и может подсказать имена колонок в SELECT. В классическом порядке на момент написания SELECT редактор ещё не видел FROM и подсказать ничего не может.
Trailing commas: мелочь против целого класса ошибок
Trailing comma — это запятая после последнего элемента списка. Стандартный SQL её запрещает: лишняя запятая перед FROM — синтаксическая ошибка. DuckDB её разрешает везде, где есть список через запятую: в SELECT, в GROUP BY, в ORDER BY, в списке значений VALUES, в аргументах функций.
Почему это важно — видно на типичной правке кода. Представьте список колонок, который растёт со временем:
-- Версия запроса вчера
SELECT
region,
product,
amount
FROM sales;
Завтра вам нужно добавить customer_id. В стандартном SQL правка затрагивает ДВЕ строки: вы дописываете новую строку И ставите запятую после amount. Если забыли запятую — ошибка. Если правите через git, в diff видно изменение двух строк, хотя смысловое изменение одно. С trailing comma добавление колонки — это всегда добавление ровно одной строки:
-- С trailing comma: каждая строка заканчивается запятой
SELECT
region,
product,
amount,
FROM sales;
-- Добавить колонку = дописать одну строку, ничего не трогая выше
SELECT
region,
product,
amount,
customer_id,
FROM sales;
Эффект ощутимее всего в code review. Diff, где добавление одной колонки выглядит как одна добавленная строка, читается мгновенно. Diff, где та же правка трогает две строки, заставляет ревьюера проверять, не сломалось ли что-то в «изменённой» предыдущей строке. Trailing commas — это не про удобство печати, это про чистоту истории изменений и снижение когнитивной нагрузки при ревью.
Trailing commas работают и в других списках:
-- В GROUP BY и ORDER BY
SELECT region, product, sum(amount) AS total
FROM sales
GROUP BY
region,
product,
ORDER BY
total DESC,
;
-- В VALUES
INSERT INTO sales VALUES
('EU', 'A', 100),
('US', 'B', 200),
;
DuckDB заимствовал trailing commas из языков программирования — в Python, JavaScript, Go, Rust они давно норма ровно по той же причине: чистые diff-ы. SQL долго оставался исключением. DuckDB и несколько других современных движков (BigQuery тоже разрешает trailing comma в SELECT) приводят SQL к этому стандарту.
Когда какой стиль выбирать
FROM-first и классический порядок взаимозаменяемы, и в одной кодовой базе можно встретить оба. Практическое правило такое:
| Ситуация | Рекомендуемый стиль |
|---|---|
| Интерактивный просмотр данных в CLI | FROM tbl; без SELECT |
| Быстрый запрос к файлу | FROM 'file.parquet' WHERE ... |
| Сложный аналитический запрос с CTE | Любой — выберите один и держитесь его |
| Код, который читают аналитики из мира классического SQL | Классический SELECT ... FROM |
| Списки колонок, которые будут расти | Всегда с trailing comma |
Главное — последовательность внутри проекта. Смешивать FROM-first и классический порядок в соседних моделях одного dbt-проекта не стоит: это сбивает читателя. Trailing commas же стоит включить везде без исключений — у них нет минусов, и они работают независимо от того, какой порядок клауз вы выбрали.
Попробуй сам
Запустите DuckDB CLI (duckdb) и создайте тестовую таблицу:
CREATE TABLE sales AS
SELECT * FROM (VALUES
('EU', 'A', 120),
('US', 'B', 80),
('EU', 'C', 200),
) t(region, product, amount);
Задания:
- Выведите всю таблицу запросом без единого
SELECT. - Напишите один и тот же агрегирующий запрос (сумма
amountпоregion) дважды: в классическом порядке и в FROM-first. Убедитесь, что результат идентичен. - Намеренно поставьте trailing comma после последней колонки в
SELECTи после последнего ключа вGROUP BY. Проверьте, что запрос всё равно выполняется. - Попробуйте
FROM sales WHERE amount > 100 LIMIT 2;— запрос безSELECT, но с фильтром и лимитом. Объясните себе, почему это валидно с точки зрения логического порядка вычисления.
Логический порядок выполнения SQL-запроса