Семантический анализ: имена и типы
Парсер построил AST — дерево синтаксиса. Дерево корректно по форме, но движок про него почти ничего не знает по смыслу. В узле Identifier написано name — но что это? Столбец какой таблицы? Какого типа? В узле Comparison > сравниваются два операнда — а можно ли их вообще сравнивать? На эти вопросы парсер ответить не мог. Отвечает следующий этап — семантический анализ.
Семантический анализ — это проверка смысла запроса и насыщение AST информацией, которой в нём не было. Этот урок разбирает две его главные задачи: разрешение имён и проверку типов, а также их результат — модель Analysis.
Граница: форма против смысла
Зафиксируем разделение, начатое в прошлом уроке. Парсер проверил форму — что запрос построен по грамматике. Семантический анализ проверяет смысл — что запрос осмыслен относительно реальных таблиц, столбцов и системы типов.
Запрос SELECT no_column FROM no_table пройдёт парсер — он грамматически правилен. Но он бессмыслен: ни таблицы, ни столбца не существует. Поймать это — задача семантического анализа. Анализ берёт AST и проверяет каждый его узел на соответствие реальности.
Разрешение имён
Первая задача — разрешение имён (name resolution). В AST имена лежат как простые строки. Анализ должен связать каждое имя с конкретной сущностью.
Разрешение имён таблиц. Каждую ссылку на таблицу анализ должен сопоставить с реальной таблицей. Здесь работает трёхуровневая модель catalog.schema.table из третьего модуля. Если в запросе таблица написана полным именем — анализ проверяет её существование напрямую. Если коротким (customer вместо tpch.sf1.customer) — анализ достраивает имя из catalog и schema по умолчанию текущей сессии, и затем проверяет. Чтобы узнать, существует ли таблица, анализ обращается к коннектору — к сервису Metadata, который мы разбирали в модуле про коннекторы. Именно Metadata отвечает: есть такая таблица или нет.
Разрешение имён столбцов. Каждую ссылку на столбец анализ привязывает к конкретному столбцу конкретной таблицы. Здесь возникают типичные ошибки:
- Столбца нет ни в одной из таблиц запроса — ошибка «column not found».
- Столбец с таким именем есть в нескольких таблицах join-а, а из какой именно брать — не указано. Имя неоднозначно (ambiguous), и анализ это отвергает.
-- Столбец name есть и в customer, и в supplier — имя неоднозначно
SELECT name
FROM tpch.sf1.customer AS c
JOIN tpch.sf1.supplier AS s ON c.nationkey = s.nationkey;
Query failed: line 1:8: Column 'name' is ambiguous
Лечится квалификацией — указанием, из какой таблицы: c.name или s.name. Тогда имя разрешается однозначно.
Разрешение алиасов и областей видимости. Анализ учитывает алиасы (customer AS c), области видимости подзапросов, видимость столбцов из CTE. Какие имена доступны в данной точке запроса — это вопрос области видимости (scope), и анализ её аккуратно отслеживает, спускаясь по дереву.
Разрешение имён — это место, где этап анализа обращается наружу, к коннекторам. Парсер был полностью автономен: ему хватало текста и грамматики. Анализу нужно знать реальную структуру источников — а её знает только коннектор через сервис Metadata. Поэтому скорость анализа частично зависит от того, как быстро коннектор отдаёт метаданные.
Проверка типов
Вторая задача — проверка типов (type checking). У каждого столбца есть тип, у каждого литерала есть тип, и каждая операция предъявляет требования к типам своих операндов. Анализ проверяет, что эти требования выполнены, и заодно вычисляет тип каждого выражения.
Тип выражения вычисляется снизу вверх по дереву. У листьев тип известен: столбец acctbal имеет тип из метаданных таблицы (скажем, double), литерал 1000 — целочисленный тип. Поднимаясь к родителю, анализ вычисляет тип составного выражения из типов потомков и проверяет, что операция к этим типам применима.
Когда типы операндов не совпадают, но операция в принципе осмысленна, анализ вставляет неявное приведение типов (implicit coercion). В сравнении acctbal > 1000 слева double, справа integer. Trino не отвергает запрос — он по правилам приведения расширяет integer до double (целое всегда представимо как вещественное без потерь) и сравнивает два double. Приведение работает, когда оно безопасно и однозначно: integer к bigint, integer к double, более узкий decimal к более широкому.
Когда же типы несовместимы и осмысленного приведения нет — анализ отвергает запрос с ошибкой типов:
-- Нельзя сложить число и произвольную строку
SELECT acctbal + name FROM tpch.sf1.customer;
Query failed: line 1:8: Cannot apply operator: double + varchar(25)
Сообщение точно называет проблему: оператор + не определён для пары double и varchar. Безопасного приведения строки к числу нет, поэтому запрос отклоняется здесь, на анализе.
Неявное приведение удобно, но у него есть цена для производительности. Если в фильтре по большому столбцу приведение применяется к самому столбцу (а не к литералу), это может помешать pushdown и заставить движок вычислять приведение для каждой строки. Хорошая практика — писать литералы в типе столбца, чтобы приводился литерал (один раз), а не столбец (миллионы раз).
Результат: модель Analysis
Парсинг выдавал AST. Семантический анализ выдаёт Analysis — модель, в которой к структуре запроса добавлены все установленные смысловые факты.
AST после парсинга «знал» только форму: вот узел Identifier со строкой name. Analysis знает гораздо больше: этот name — столбец name таблицы tpch.sf1.customer, его тип varchar(25); это сравнение имеет тип boolean; сюда вставлено приведение integer к double; в запросе участвуют такие-то таблицы и столбцы.
Зачем это нужно следующему этапу. Планировщик, который строит логический план, не может работать с «голым» AST: чтобы построить корректный план, ему нужно точно знать, к какой таблице обращается каждая ссылка, какого типа каждое выражение, где приведения. Всю эту информацию поставляет Analysis. Семантический анализ — это мост между «синтаксически разобранным запросом» и «запросом, готовым к планированию».
И ещё одно свойство этапа: к началу планирования запрос либо признан полностью осмысленным, либо уже отклонён. Планировщик и распределённое исполнение никогда не сталкиваются с несуществующими столбцами или несовместимыми типами — анализ гарантирует, что дальше идёт только корректный запрос. Это разделение ответственности упрощает все последующие этапы.
Место анализа в жизненном цикле
Жизненный цикл — это последовательность преобразований представления запроса. Текст стал AST на парсинге. AST стал Analysis на семантическом анализе — это второй этап. Дальше Analysis станет логическим планом — деревом PlanNode, и это тема следующего урока. Каждый этап обогащает или перестраивает представление, приближая запрос к исполнимой форме.
Попробуй сам
На кластере Trino отработайте обе задачи семантического анализа:
- Разрешение имён. Сделайте join
tpch.sf1.customerиtpch.sf1.supplierи выберитеnameбез квалификации таблицей. Получите ошибку неоднозначности. Затем квалифицируйте имя (c.name) и убедитесь, что запрос проходит. - Несуществующий столбец. Выполните
SELECT no_such_col FROM tpch.sf1.nation. Это ошибка разрешения имени, а не синтаксиса. - Несовместимые типы. Выполните
SELECT name + 1 FROM tpch.sf1.nation. Прочитайте сообщение: оператор не определён для этой пары типов. - Неявное приведение. Выполните
SELECT * FROM tpch.sf1.customer WHERE acctbal > 1000(гдеacctbal—double, а1000— целое). Запрос работает — анализ молча вставил безопасное приведениеintegerкdouble. Подумайте, почему это приведение безопасно, а строка-в-число — нет.