Spark SQL 执行流程详解

从一条 SQL 语句到分布式任务执行,经历 6 个核心阶段。点击下方流程节点或展开卡片查看各阶段的内部机制。

1
SQL Parsing
语法解析
2
Analysis
语义分析
3
Logical Optimize
逻辑优化
4
Physical Plan
物理计划
5
Code Generation
代码生成
6
Execution
分布式执行
入口
SQL / DataFrame / Dataset API
Catalyst
Catalyst Optimizer(解析 → 分析 → 逻辑优化 → 物理计划)
Tungsten
Tungsten Engine(内存管理 + Whole-Stage Code Generation)
Runtime
Spark Core — RDD DAG → Stage → Task → Executor 分布式执行
1

SQL Parsing — 语法解析

SQL 文本 → Unresolved Logical Plan(未解析逻辑计划)

这是整个流程的第一步。Spark SQL 使用 ANTLR4(Another Tool for Language Recognition) 作为其语法解析器。当你提交一条 SQL 语句时,ANTLR4 会根据 Spark 定义的 SQL 语法规则 SqlBase.g4 将文本解析为一棵抽象语法树(AST)

SQL 文本
ANTLR4 Lexer
Token 流
ANTLR4 Parser
AST
Unresolved LogicalPlan

随后,Spark 的 AstBuilder(继承自 ANTLR4 生成的 Visitor) 遍历 AST,将其转换为 Spark 内部的 Unresolved Logical Plan。 这个阶段的计划是"未解析"的——表名、列名等标识符仅是字符串,没有与实际数据源绑定。

关键组件

组件职责
SparkSqlParser入口类,持有 ANTLR4 Lexer/Parser 实例,协调整个解析流程
SqlBase.g4ANTLR4 语法文件,定义了 Spark SQL 的完整语法规则(SELECT / INSERT / CREATE / ALTER 等)
AstBuilder遍历 ANTLR4 生成的 ParseTree,构建 Spark 内部的 LogicalPlan 节点树
UnresolvedRelation表示一个尚未绑定的表引用(此时只有表名字符串)
UnresolvedAttribute表示一个尚未绑定的列引用(此时只有列名字符串)
DataFrame / Dataset API 也走这条路。当你写 df.filter(...).groupBy(...) 时, Spark 也会在内部构建等价的 Unresolved Logical Plan,然后进入后续优化流程。SQL 和 API 最终殊途同归。
2

Analysis — 语义分析

Unresolved Logical Plan → Resolved Logical Plan

Analysis 阶段由 Analyzer 负责,它通过查询 Catalog(元数据目录) 来解析上一步留下的未绑定引用。这一步回答的核心问题是: "这些名字到底指的是什么?类型是否匹配?"

Unresolved Plan
Analyzer Rules
Catalog 查找
Resolved Plan

Analyzer 执行的关键操作

表解析 (Relation Resolution)
将 UnresolvedRelation 绑定到 Catalog 中的实际表/视图
列解析 (Attribute Resolution)
将 UnresolvedAttribute 绑定到具体列,确定数据类型
类型转换 (Type Coercion)
自动插入隐式类型转换(如 INT 与 LONG 比较时向上转型)
函数解析 (Function Resolution)
将函数名(如 count、sum)绑定到内置或注册的 UDF 实现
权限检查 (Authorization)
验证当前用户是否有权访问目标表和列
星号展开 (Star Expansion)
将 SELECT * 展开为表的所有具体列

Analyzer 采用规则批处理(Rule Batches)的方式工作。每个 Batch 包含一组规则, 会被反复应用(固定点迭代),直到计划不再变化或达到最大迭代次数。这保证了所有依赖关系都被正确解析。

常见错误源。大部分 SQL 报错(表不存在、列歧义、类型不匹配)都在这个阶段抛出。 如果你看到 AnalysisException,说明语法正确但语义有问题。
3

Logical Optimization — 逻辑优化

Resolved Logical Plan → Optimized Logical Plan(Catalyst 核心)

这是 Catalyst Optimizer 的核心阶段。优化器通过一系列 基于规则的优化(RBO)基于代价的优化(CBO), 对逻辑计划进行等价变换,在不改变语义的前提下大幅提升执行效率。

基于规则的优化(RBO)— 常见规则

谓词下推 Predicate Pushdown
将 WHERE 条件尽早下推到数据源层,减少扫描量
列裁剪 Column Pruning
只读取查询真正需要的列,避免全列扫描
常量折叠 Constant Folding
编译期计算常量表达式(如 1+2 直接变成 3)
分区裁剪 Partition Pruning
根据分区键条件直接跳过不相关的分区目录
子查询消除 Subquery Elimination
将 IN/EXISTS 子查询重写为 Join 或 Semi-Join
布尔简化 Boolean Simplification
简化冗余条件(如 true AND x → x, x OR true → true)

基于代价的优化(CBO)

Spark 2.2+ 引入了 CBO,利用表和列的统计信息(行数、去重基数、NULL 比例、直方图等) 来做更智能的决策。典型场景:

优化机制
Join 排序将小表放到 Join 的构建侧(Build Side),大表放到探测侧(Probe Side)
Join 策略选择根据表大小选择 BroadcastHashJoin / SortMergeJoin / ShuffledHashJoin
聚合策略根据 distinct 值数量决定 Hash Agg vs Sort Agg
CBO 需要先收集统计信息才能生效:ANALYZE TABLE t COMPUTE STATISTICSANALYZE TABLE t COMPUTE STATISTICS FOR COLUMNS c1, c2。 没有统计信息时会回退到 RBO。
4

Physical Planning — 物理计划生成

Optimized Logical Plan → SparkPlan(可执行的物理计划)

物理计划阶段由 SparkPlanner 负责,它将逻辑算子映射为具体的物理执行算子(SparkPlan)。 一个逻辑算子可能有多种物理实现方式——这个阶段就是选出最优的那个。

物理算子选择策略(SparkStrategy)

逻辑算子物理算子候选选择依据
Join BroadcastHashJoinExec
SortMergeJoinExec
ShuffledHashJoinExec
BroadcastNestedLoopJoinExec
表大小、Join 类型、是否有等值条件。小表 < spark.sql.autoBroadcastJoinThreshold(默认 10MB)时选 BroadcastHashJoin
Aggregate HashAggregateExec
SortAggregateExec
ObjectHashAggregateExec
聚合函数是否支持 partial merge、数据类型是否可 mutable
Sort SortExec 使用 Tungsten UnsafeRow 的内存排序,支持溢写到磁盘
Scan FileSourceScanExec
InMemoryTableScanExec
数据源类型(Parquet/ORC/CSV vs 缓存表)

EnsureRequirements — 插入 Shuffle 和 Sort

物理计划生成后,EnsureRequirements 规则会检查每个算子对输入数据的 分区要求(Distribution)排序要求(Ordering)。 如果子节点的输出不满足要求,就自动插入 ShuffleExchangeExec(重分区) 或 SortExec(排序)。

Adaptive Query Execution (AQE) — Spark 3.x

Spark 3.0+ 引入了 AQE,允许在运行时根据 Shuffle 阶段的实际统计数据 动态调整物理计划。这是一个重大突破——计划不再是一成不变的。

动态合并小分区
将 Shuffle 后过小的分区自动合并,减少 Task 数量
动态切换 Join 策略
运行时发现某侧数据量小于阈值,从 SortMerge 切换为 Broadcast
倾斜 Join 优化
检测数据倾斜的分区并自动拆分,避免长尾 Task
5

Code Generation — 代码生成

SparkPlan → 可执行的 JVM 字节码(Whole-Stage CodeGen + Tungsten)

这是 Spark 2.x 引入的一项颠覆性优化。传统的 Volcano 执行模型(每个算子拉取一行、逐行处理) 会在算子之间产生大量虚函数调用开销。Whole-Stage Code Generation 将 整个 Stage 内的多个算子融合成一个 Java 方法,直接编译为 JVM 字节码执行。

Volcano 模型 vs Whole-Stage CodeGen

对比维度Volcano 迭代器模型Whole-Stage CodeGen
执行方式逐行拉取:每个算子的 next() 调用子算子的 next()将多算子融合为一个紧凑循环(tight loop)
函数调用每行 × 每算子 = 大量虚函数调用单次方法调用处理整批数据
CPU 缓存频繁跳转,缓存命中率低连续内存访问,缓存友好
性能提升基线通常提升 2-10x

Tungsten 内存管理

与 CodeGen 配合的是 Project Tungsten,它绕过 JVM 的对象模型, 使用 UnsafeRow(紧凑二进制行格式)存储数据:

Off-Heap 内存
避免 GC 压力,直接操作原始内存(sun.misc.Unsafe)
UnsafeRow 格式
固定长度头部 + 变长数据区,无对象头开销,序列化零拷贝
Cache-Aware 计算
排序/哈希操作利用 CPU 缓存行特性设计数据布局
在 Spark UI 的物理计划中,看到算子名前带 *(如 *(1) Filter) 表示该算子已被 Whole-Stage CodeGen 融合。没有星号的算子(如 Exchange)是融合边界。
6

Execution — 分布式执行

SparkPlan → RDD DAG → Stages → Tasks → 集群并行执行

最终的物理计划被转换为 RDD(Resilient Distributed Dataset)的 DAG, 然后交给 Spark Core 的调度引擎执行。这是真正"动起来"的阶段。

SparkPlan
RDD DAG
DAGScheduler
Stages
TaskScheduler
Tasks on Executors

DAGScheduler — Stage 划分

DAGScheduler 将 RDD DAG 按宽依赖(Shuffle 依赖)切分为多个 Stage。 同一 Stage 内的算子通过窄依赖(Pipeline)连接,不需要数据交换,可以在单个 Task 中流水线执行。

依赖类型特征示例
窄依赖父 RDD 的每个分区最多被一个子 RDD 分区使用map, filter, union(对齐分区时)
宽依赖父 RDD 的分区被多个子 RDD 分区使用,需要 ShufflegroupByKey, reduceByKey, join(非 broadcast)

TaskScheduler — Task 分发

每个 Stage 被进一步拆分为与分区数相等的 Task,分发到集群的 Executor 上并行执行。 调度策略考虑数据本地性(Data Locality)

PROCESS_LOCAL
数据在同一 JVM 进程中(缓存数据)
NODE_LOCAL
数据在同一节点的其他 Executor 或本地磁盘上
RACK_LOCAL / ANY
数据在同机架的其他节点或需跨机架传输

Shuffle 过程

Shuffle 是整个执行中最昂贵的操作。数据需要在网络上重新分布—— Map 端将数据按 key 分桶写入本地磁盘(Shuffle Write),Reduce 端从所有 Map 端拉取对应分区的数据(Shuffle Read)。

性能调优的核心。减少 Shuffle 是 Spark SQL 调优的第一原则。手段包括: 使用 Broadcast Join 避免 Shuffle、合理设置 spark.sql.shuffle.partitions、 利用 Bucket 表预分区、以及 AQE 的动态分区合并。