考察点
Catalyst 是 Spark SQL 的大脑,这题考察的是你对「声明式 SQL 怎么落地成分布式执行」的理解。面试官想听:从 SQL 到物理计划的四个阶段各自干什么;规则优化里哪几条收益最大(谓词下推、列裁剪);CBO 用什么统计信息做什么决策;以及 Catalyst 的「规则可扩展」设计为什么重要。追问常往「AQE 和 Catalyst 的分工」「explain 怎么读」「为什么 DataFrame 比 RDD 快」方向走。
参考答案
四个阶段的流水线
一条 SQL 从字符串到物理执行,Catalyst 走四步,每步的产物都是树(TreeNode),变换都是模式匹配 + 规则应用:
1. 解析(Parser)
SQL 字符串经过 ANTLR 生成的解析器变成 Unresolved LogicalPlan——一棵「未绑定」的逻辑计划树。此时表名、列名还是字符串符号,Catalyst 不知道 t.id 是哪张表的什么类型。DataFrame API 的链式调用也直接构造这种未绑定计划。
2. 分析(Analyzer)
拿 Catalog(元数据仓库)把未绑定计划里的符号解析成真实对象:表名 → 表的元数据和文件位置,列名 → 列的类型和序号。产物是 Resolved LogicalPlan。报「table or view not found」「cannot resolve column」这类错就在这个阶段。
3. 逻辑优化(Optimizer)
对 Resolved LogicalPlan 应用一批规则(Rule),每条规则是模式匹配 + 等价变换,迭代执行直到不动点(plan 不再变化)。产出 Optimized LogicalPlan。核心规则后面单独说。
4. 物理规划(Physical Planning)
把优化后的逻辑计划映射成物理计划(PhysicalPlan)。这一步开始引入代价:同一个逻辑 join 可以映射成 BroadcastHashJoin 或 SortMergeJoin,Catalyst 生成多个候选物理计划,用代价模型(CBO 开启时基于统计信息)选代价最低的一个。选定的物理计划最后交给 Whole-stage Codegen 编译成 Java 字节码执行。
逻辑优化的核心规则
几十条规则里,面试必须能讲透这三条:
谓词下推(PushDownPredicate):把 filter 尽量推到离数据源最近的位置。SELECT * FROM fact JOIN dim ON ... WHERE fact.dt = '2026-08-01' 里,dt 过滤会被推到扫描 fact 表之前,甚至推到文件格式层——Parquet 支持谓词下推到 row group 级别,不满足条件的 row group 整个跳过不读。数据扫描量可能从 TB 级降到 GB 级,这是收益最大的一条规则。
列裁剪(ColumnPruning):只读查询真正用到的列。SELECT name FROM t 不会读 t 的其余 99 列。对 Parquet 这种列式存储,裁剪直接变成「不读那些列的文件块」,IO 和网络省一大截。
常量折叠(ConstantFolding):WHERE dt > date_sub('2026-08-03', 7) 里的表达式在优化阶段直接算成 dt > '2026-07-27',避免每条记录都执行一遍函数。
还有 join 重排(ReorderJoin,CBO 下按代价排 join 顺序)、子查询改写、合并投影/过滤等,但上面三条是收益最大、最常见的。
CBO:代价模型做什么
基于代价的优化(spark.sql.cbo.enabled,3.x 默认开)在物理规划阶段用统计信息做决策。统计信息来自 ANALYZE TABLE ... COMPUTE STATISTICS(表级行数、字节数)和 ANALYZE TABLE ... COMPUTE STATISTICS FOR COLUMNS(列级直方图、NDV)。主要决策:
- join 策略选择:一侧估算小于广播阈值就选 BHJ,否则 SMJ。
- join 顺序:多表 join 时把选择性高、输出小的 join 往前排,减少中间结果。
- build side 选择:SMJ/SHJ 里选小的一侧做 build。
统计不准是 CBO 的死穴——表没 ANALYZE 或数据剧烈变化后,估算可能差几个数量级,选错策略。这就是为什么 AQE 重要:AQE 用运行时真实统计修正 Catalyst 的计划时错误,两者互补。
Whole-stage Codegen
物理计划选好后不是直接解释执行,而是把相邻的算子(Project、Filter、HashAggregate 等)融合编译成一个 Java 类,数据在算子间以局部变量/寄存器传递,消除虚函数调用和迭代器开销。这是 Tungsten 的一部分,单机吞吐比解释执行高几倍。UI 里看到物理计划节点标 * 号(如 *(2) HashAggregate)就是全阶段代码生成生效的标志。
Catalyst 的设计亮点
为什么 Catalyst 比手写优化器好维护:规则就是模式匹配 + 变换函数,新增一条优化规则是几十行 Scala,不用动优化器框架。Analyzer、Optimizer、物理规划全是同一套规则引擎,Spark 社区每年往里加几十条新规则(AQE 的重规划规则也是同一套机制)。这个可扩展性是 Catalyst 能持续进化的根本原因。
怎么验证 Catalyst 干了什么
df.explain(true) // 四个阶段的计划全打印
输出里 Parsed Logical Plan → Analyzed Logical Plan → Optimized Logical Plan → Physical Plan 四段,对照看谓词下推和列裁剪有没有生效、join 策略选了什么。生产调优的第一课就是读 explain——看不懂计划,调参就是碰运气。
可能的追问
- DataFrame 为什么比 RDD 快? DataFrame 有 schema,Catalyst 能做谓词下推/列裁剪/自动 join 选择,Tungsten 能按二进制布局操作内存;RDD 是不透明的 Java 对象,Spark 不知道里面是什么,只能按用户代码原样执行,无优化空间。
- AQE 和 Catalyst 是什么关系? Catalyst 是计划时优化(编译期),AQE 是运行时优化。Catalyst 生成初始物理计划后,AQE 在 shuffle 边界拿到真实统计,对剩余计划重新跑 Catalyst 的规则引擎做修正。AQE 复用的还是 Catalyst 那套规则框架。
- explain 里 Physical Plan 的星号是什么?
*(n)表示这个算子被 whole-stage codegen 融合进第 n 个代码生成单元。没有星号的算子(Exchange、BroadcastExchange 等)是 shuffle/广播边界,不能融合。