Логический план: Анализ и разрешение ссылок
От текста к дереву: Parser
Когда Spark получает SQL-запрос, первый шаг — парсинг. Parser (на основе ANTLR) преобразует текст SQL в дерево абстрактного синтаксиса — Unresolved Logical Plan.
На этом этапе Spark ещё ничего не знает о реальных таблицах и колонках. Все ссылки остаются “нерезолвенными”:
UnresolvedRelation('employees')— Spark не знает, существует ли таблицаemployeesUnresolvedAttribute('age')— тип и принадлежность колонкиageнеизвестныUnresolvedStar(*)—SELECT *ещё не развернут в список конкретных колонок
Рассмотрим конкретный SQL-запрос, который будет нашим сквозным примером:
SELECT name, dept_name
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
WHERE e.age > 30
На стадии Parser этот запрос превращается в дерево с нерезолвенными узлами — Spark знает структуру запроса, но не знает, существуют ли таблицы и каковы их схемы.
Analyzer: разрешение ссылок через Catalog
Analyzer — вторая стадия Catalyst pipeline. Его задача — взять Unresolved Logical Plan и разрешить все ссылки, обратившись к Catalog.
Что такое Catalog?
Catalog — это метаданное хранилище Spark, содержащее информацию о:
- Базах данных и их таблицах
- Схемах таблиц (имена колонок, типы данных)
- Функциях (встроенных и пользовательских)
- Временных представлениях (temporary views)
В Spark 4.0 GA (2025) и 4.1 (текущая, декабрь 2025) Catalog доступен через spark.catalog (поведение совместимо со Spark 3.5 LTS):
# Список таблиц в текущей базе данных
spark.catalog.listTables()
# Колонки конкретной таблицы
spark.catalog.listColumns("employees")
Процесс разрешения
Analyzer применяет набор правил для разрешения ссылок:
- ResolveRelations — ищет
UnresolvedRelation('employees')в Catalog и заменяет наLogicalRelationс полной схемой - ResolveReferences — сопоставляет
UnresolvedAttribute('age')с конкретной колонкойemployees.age: Int - ResolveFunctions — разрешает вызовы функций (
count,sum, UDF) - ResolveAliases — обрабатывает алиасы (
e,d) - TypeCoercion — приводит типы (например,
'30'в SQL сравнивается сInt)
После анализа наш запрос имеет полностью определенную схему:
employees.name: String
employees.age: Int
employees.dept_id: Int
departments.dept_id: Int
departments.dept_name: String
Что произойдёт при ошибке? Если Analyzer не найдёт таблицу в Catalog, Spark выбросит AnalysisException: Table or view not found: employees. Аналогично, ссылка на несуществующую колонку вызовет AnalysisException: cannot resolve 'salary' given input columns [name, age, dept_id]. Эти ошибки возникают до выполнения запроса — на этапе анализа.
Чтение explain(true): Parsed и Analyzed планы
Используем explain(true) для нашего сквозного примера:
employees = spark.createDataFrame(
[(1, "Alice", 30, 100), (2, "Bob", 35, 200), (3, "Carol", 25, 100)],
["id", "name", "age", "dept_id"]
)
departments = spark.createDataFrame(
[(100, "Engineering"), (200, "Marketing")],
["dept_id", "dept_name"]
)
employees.createOrReplaceTempView("employees")
departments.createOrReplaceTempView("departments")
result = spark.sql("""
SELECT name, dept_name
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
WHERE e.age > 30
""")
result.explain(True)
Вывод содержит несколько секций. Разберем первые две:
== Parsed Logical Plan ==
'Project ['name, 'dept_name]
+- 'Filter ('e.age > 30)
+- 'Join Inner, ('e.dept_id = 'd.dept_id)
:- 'SubqueryAlias e
: +- 'UnresolvedRelation [employees]
+- 'SubqueryAlias d
+- 'UnresolvedRelation [departments]
Обратите внимание на апострофы ('name, 'dept_name) — это маркер нерезолвенных ссылок. Spark видит структуру запроса, но типы и таблицы ещё не разрешены.
== Analyzed Logical Plan ==
name: string, dept_name: string
Project [name#5, dept_name#12]
+- Filter (age#6 > 30)
+- Join Inner, (dept_id#7 = dept_id#11)
:- SubqueryAlias e
: +- Project [id#4, name#5, age#6, dept_id#7]
: +- LocalRelation [id#4, name#5, age#6, dept_id#7]
+- SubqueryAlias d
+- Project [dept_id#11, dept_name#12]
+- LocalRelation [dept_id#11, dept_name#12]
Теперь все ссылки разрешены:
'UnresolvedRelation [employees]сталоLocalRelation [id#4, name#5, age#6, dept_id#7]'nameсталоname#5(суффикс#5— уникальный идентификатор колонки в Spark)- Типы указаны в заголовке:
name: string, dept_name: string 'e.age > 30сталоage#6 > 30с известным типомInt
Как читать explain(true) пошагово:
- Начинайте с Parsed Logical Plan — это ваш запрос “как есть”, в форме дерева
- Сравните с Analyzed Logical Plan — все апострофы должны исчезнуть, типы стать явными
- Если видите
UnresolvedRelationв Analyzed плане — это баг (не должно быть) - Суффиксы
#N— внутренние ID колонок, они уникальны в пределах запроса
Роль Catalog в production-средах
В production Spark обычно работает с Hive Metastore или Unity Catalog (Databricks). Catalog хранит не только схемы, но и:
- Статистики таблиц — количество строк, размер в байтах
- Статистики колонок — min/max, количество distinct значений, null count
- Информацию о партициях — partitioning scheme, partition values
Эти статистики критически важны для Cost-Based Optimizer, который мы разберем в уроке об оптимизации правил.
# Собрать статистики для CBO
spark.sql("ANALYZE TABLE employees COMPUTE STATISTICS FOR ALL COLUMNS")
# Посмотреть статистики
spark.sql("DESCRIBE EXTENDED employees").show()
Граница между Analyzer и Optimizer
Analyzer гарантирует корректность плана — все ссылки разрешены, типы совместимы. После анализа план является валидным, но ещё не оптимальным.
Следующая стадия — Optimizer — берет Analyzed Logical Plan и трансформирует его для повышения производительности. Optimizer не меняет семантику запроса, он только перестраивает план так, чтобы результат был таким же, но вычислялся быстрее.
DataFusion: правила логической оптимизации