От SQL-текста к запросу: statement, parser, AST
Вы отправляете в Trino строку: SELECT name FROM customer WHERE acctbal > 1000. Через секунды приходит результат. Между этими двумя моментами движок проделывает длинную цепочку преобразований — это жизненный цикл запроса, позвоночник всего курса. Модуль 04 проходит цикл от начала до конца. Этот, первый, урок — про самый первый шаг: превращение текста SQL в структуру, с которой движок может работать.
Текст SQL — это просто последовательность символов. Программа не может «понять» строку напрямую; ей нужна структура. Разберём, как Trino эту структуру строит и зачем нужны понятия statement, query, parser и AST.
Statement и query: текст против выполнения
Два слова, которые легко спутать, но Trino их строго различает.
Statement — это сам текст SQL, переданный в Trino. Просто строка символов. SELECT name FROM customer — это statement.
Query — это runtime-инстанс, который Trino создаёт, обработав statement. Query — живой объект: у него есть идентификатор, состояние, он объединяет всё, что породит выполнение, — stages, tasks, splits, обращения к коннекторам. Statement — это «что попросили», query — это «выполнение этого по жизни».
Различие не педантизм. Один statement, выполненный дважды, породит два разных query с разными идентификаторами и своими наборами задач. Когда вы смотрите в Web UI «список запросов» — вы смотрите список query (runtime-инстансов), а не текстов. Текст внутри них может совпадать, query — всегда разные.
Превращение statement в query — не один шаг, а конвейер. Самое первое звено — синтаксический разбор, парсинг.
Зачем нужен parser
Текст SELECT name FROM customer WHERE acctbal > 1000 для движка — это поток байтов. Чтобы что-то с ним сделать, движку нужно ответить на вопросы: где здесь список выбираемых столбцов, где имя таблицы, где условие фильтра, какие части условия — операнды, а какая — оператор. Плоская строка на эти вопросы не отвечает.
Parser (синтаксический анализатор) — компонент, который читает текст SQL и строит из него дерево. Парсинг идёт в два логических этапа:
-
Лексический анализ. Поток символов разбивается на токены — неделимые лексические единицы: ключевое слово
SELECT, идентификаторname, ключевое словоFROM, идентификаторcustomer, оператор>, числовой литерал1000. Пробелы и переводы строк отбрасываются. Из строки получается список токенов. -
Синтаксический анализ. Список токенов сопоставляется с грамматикой SQL — формальным набором правил, описывающим, какие последовательности токенов допустимы. Грамматика говорит:
SELECTсопровождается списком выражений, затем может идтиFROMс источником, затем опциональноWHEREс булевым условием. По грамматике из плоского списка токенов строится дерево.
Trino описывает грамматику SQL формально и генерирует по ней код парсера инструментом ANTLR. Это значит, что грамматика — не код, написанный руками, а декларативная спецификация языка, из которой парсер получается автоматически. Практический итог: Trino принимает чётко определённый диалект SQL, и любое отклонение от грамматики ловится здесь, на парсинге.
Парсер проверяет только синтаксис — форму. Он убедится, что SELECT построен по правилам грамматики. Но парсер НЕ знает, существует ли таблица customer и есть ли в ней столбец name. Проверка существования имён и совместимости типов — это уже семантический анализ, отдельный следующий этап. Парсер работает с формой, а не со смыслом.
AST: дерево синтаксиса
Результат работы парсера — AST, Abstract Syntax Tree (абстрактное синтаксическое дерево). Это и есть та структура, которой так не хватало в плоской строке. AST представляет SQL-запрос как дерево вложенных узлов, где каждый узел — синтаксическая конструкция, а связи родитель-потомок отражают вложенность.
Возьмём запрос и посмотрим его AST концептуально:
SELECT name
FROM customer
WHERE acctbal > 1000
Узел Where сам по себе не лист — внутри него вложено дерево булевого выражения acctbal > 1000:
Дерево, а не строка, потому что SQL рекурсивен по природе. Выражение содержит подвыражения, подзапрос содержит полный запрос, условие может вкладывать условия. Только древовидная структура естественно выражает эту вложенность. Слово «abstract» в названии означает, что дерево хранит смысловую структуру, а не точное написание: пробелы, переводы строк, необязательные скобки в AST не попадают — они нужны были только для разбора.
С AST уже можно работать программно. Движок обходит дерево, узнаёт типы узлов, добирается до любой части запроса по ветке. Все дальнейшие этапы цикла — анализ, планирование — оперируют не текстом, а этим деревом и его потомками.
Что происходит при синтаксической ошибке
Поскольку парсер сверяет токены с грамматикой, любая конструкция, не укладывающаяся в грамматику, отлавливается прямо здесь — раньше всех остальных этапов. Это синтаксическая ошибка.
-- Пропущено имя таблицы после FROM
SELECT name FROM WHERE acctbal > 1000;
Query failed: line 1:18: mismatched input 'WHERE'.
Expecting: '(', 'JSON_TABLE', 'LATERAL', 'TABLE', 'UNNEST', <identifier>
Сообщение показывает позицию (line 1:18), что парсер встретил (WHERE) и что грамматика разрешала встретить в этом месте. После FROM грамматика ждёт источник данных — идентификатор таблицы, подзапрос в скобках, UNNEST и так далее — а получила ключевое слово WHERE. Несоответствие грамматике, разбор останавливается.
Синтаксическая ошибка ловится на парсинге — это самый ранний этап. До планирования и тем более до распределённого исполнения дело не доходит вообще: запрос отклоняется ещё до того, как стать полноценным query. Поэтому синтаксические ошибки дешёвые — кластер не тратит на них ресурсы воркеров.
Важно, чего на этом этапе ещё НЕ проверяется. Запрос SELECT nonexistent_column FROM nonexistent_table синтаксически безупречен — он построен по грамматике. Парсер построит для него корректный AST. Что таблицы не существует, выяснится только на следующем этапе — семантическом анализе. Граница чёткая: парсер отвечает за форму, семантический анализ — за смысл.
Место парсинга в жизненном цикле
Зафиксируем картину целиком, чтобы видеть, куда мы движемся дальше в модуле.
Если разложить жизненный цикл в линию: statement (текст SQL) проходит парсинг и становится AST; AST проходит семантический анализ и становится Analysis; дальше идёт логическое планирование, оптимизация, распределённое планирование и исполнение. Парсинг — самое первое звено этой цепи.
Парсинг — это переход «текст -> структура». Без него никакой дальнейший анализ невозможен: нельзя проверять типы или строить план для плоской строки символов. AST — первая структура в длинной цепочке, и каждый следующий этап будет преобразовывать представление запроса дальше: AST станет аналитической моделью, та — логическим планом, план — распределённым планом. Жизненный цикл запроса это и есть последовательность таких преобразований представления.
Попробуй сам
На кластере Trino поэкспериментируйте с границей между синтаксисом и смыслом:
- Выполните заведомо синтаксически неверный запрос, например
SELECT FROM customerилиSELECT name customer FROM. Прочитайте сообщение: где позиция ошибки, что парсер ожидал. - Выполните синтаксически верный, но бессмысленный запрос:
SELECT * FROM tpch.sf1.no_such_table. Сравните сообщение об ошибке с предыдущим. Это уже не синтаксическая ошибка — таблица не найдена на этапе анализа. - Выполните
SELECT col_that_does_not_exist FROM tpch.sf1.nation. Снова: запрос построен по грамматике, AST для него существует, но столбца нет. - Сформулируйте письменно: за какую категорию ошибок отвечает парсер, а какие ошибки в принципе не могут быть пойманы на парсинге и почему.
Это разделение «форма против смысла» — ключ к пониманию следующего урока про семантический анализ.