摘要:SparkSQL 作为 Apache Spark 生态中最核心的模块之一,不仅提供了统一的 SQL 查询入口,更承载了 DataFrame/Dataset API 的全部执行能力。本文从数据源接入机制入手,深入 Catalyst 优化器四阶段流水线、WholeStageCodegen 代码生成、Tungsten 执行引擎等核心组件,结合源码级追踪,全面剖析 SparkSQL 底层架构设计。无论你是日常写 SQL 的数据工程师,还是需要调优 Spark 作业的架构师,这篇文章都会让你对 SparkSQL 有一个全新的认识。
关键词:SparkSQL、Catalyst、Tungsten、DataSource V2、谓词下推、WholeStageCodegen、DataFrame、Dataset、RDD、大数据架构
一、引言:为什么 SparkSQL 值得被深入理解?
如果你使用过 Spark,那你一定写过这些代码:
scala
val df = spark.read.format("parquet").load("/data/events")
df.filter($"date" === "2024-01-01").groupBy("uid").count().show()
或者是等价的 SQL:
sql
SELECT uid, COUNT(*) FROM events WHERE date = '2024-01-01' GROUP BY uid;
你可能已经知道 SparkSQL "很快"。但 它为什么快? 当我们写下 spark.read.parquet() 时,Spark 做了什么?当我们写 filter().groupBy() 时,这些操作是如何被转换成一个高效的 RDD DAG 的?
答案藏在四个核心组件之中:DataSource API (数据源接入层)、Catalyst Optimizer (优化器)、Tungsten Engine (执行引擎)以及 WholeStageCodegen(全阶段代码生成)。本文将以"数据源 → 优化器 → 执行引擎"为主线,带你一览 SparkSQL 底层架构全貌。

二、数据源全景架构:SparkSQL 如何统一接入多源异构数据?
2.1 四大数据源类别
SparkSQL 的 DataSource API 设计目标非常明确:让所有数据源都像操作关系型数据库一样简单。从 Source 类型来看,主要分为四大类:
| 类别 | 典型数据源 | Format 参数 | 特点 |
|---|---|---|---|
| 结构化文件 | Parquet、ORC、Avro、JSON、CSV | parquet / orc / json / csv |
Schema 自描述,支持列裁剪 |
| 数据库与数仓 | Hive、HBase、MySQL、PostgreSQL | jdbc / hive |
支持谓词下推,减少数据传输 |
| 流式数据 | Kafka、Kinesis、Socket | kafka / kinesis / socket |
Structured Streaming 统一 API |
| 云对象存储 | S3、ADLS Gen2、GCS、OSS | 依赖文件 format | Hadoop FileSystem 抽象层复用 |
无论底层是什么,对用户而言,接口始终是统一的 DataFrameReader / DataFrameWriter:
scala
// 统一读取接口
spark.read
.format("parquet") // 或 "json", "orc", "jdbc", "kafka" ...
.option("key", "val") // 数据源特定参数
.load("/path")
// 统一写入接口
df.write
.mode(SaveMode.Append)
.format("parquet")
.partitionBy("dt")
.save("/output")
2.2 DataSource API 演进:V1 → V2
Spark 最初使用 DataSource V1 API,基于 RelationProvider、SchemaRelationProvider 等 trait 实现,支持基本的读写和谓词下推。但 V1 存在明显局限:
- 写操作不支持事务:无法原子性地提交多个文件
- 缺乏列式读取支持:无法精细控制需要读取哪些列
- 流式支持不足:与 Structured Streaming 集成困难
DataSource V2 API (Spark 2.3+) 通过 ReadSupport、WriteSupport 接口解决了以上问题:
java
// DataSource V2 ReadSupport 接口(简化)
public interface ReadSupport extends DataSourceV2 {
DataSourceReader createReader(DataSourceOptions options);
}
public interface DataSourceReader {
List<DataPartition> planInputPartitions(); // 分区规划
StructType readSchema(); // Schema 获取
// 支持列式读取、过滤下推
List<InputPartition<InternalRow>> planInputPartitions(
List<Filter> filters);
}
V2 的核心增强包括:列式读取(Columnar Batch Read) 、分区裁剪(Partition Pruning) 、动态过滤(Dynamic Filtering) 、事务性写入(如 Iceberg/Delta Lake)。
三、Catalyst 优化器:SparkSQL 的"大脑"
如果说 DataSource API 是 SparkSQL 的"感官系统",那么 Catalyst Optimizer 就是 SparkSQL 的大脑。它负责将用户书写的 SQL 或 DataFrame 操作,经过四阶段转换,最终生成高效的 RDD 执行计划。

3.1 Phase 1:Analysis------语义分析
SQL 经 ANTLR4 解析后生成 Unresolved Logical Plan。此时表名、列名尚未与 Catalog 中的元数据进行绑定,类型也未检查。
Analysis 阶段通过 SessionCatalog 查询 Hive Metastore,将 UnresolvedRelation 替换为具体的表元数据,进行类型推断和隐式转换。核心规则包括:
ResolveRelations------ 解析 FROM 子句中的表引用ResolveReferences------ 解析 SELECT/WHERE 中的列引用TypeCoercion------ 类型自动转换(如 String → Int)ResolveFunctions------ 解析内置函数和 UDF
3.2 Phase 2:Logical Optimization------规则优化(RBO)
分析后的 Logical Plan 进入 70+ 条优化规则 的优化器,核心规则包括:
| 优化规则 | 作用 | 示例 |
|---|---|---|
| Predicate Pushdown | 将 Filter 下推到数据源 | WHERE date='2024-01-01' 下推到 Parquet Reader |
| Column Pruning | 裁剪不需要的列 | SELECT name FROM t 只读 name 列 |
| Constant Folding | 编译期计算常量表达式 | SELECT 1+2+3 → SELECT 6 |
| Limit Pushdown | 将 LIMIT 下推 | SELECT * FROM t LIMIT 10 → Reader 只取前 10 行 |
| Boolean Simplification | 逻辑表达式简化 | WHERE a=1 AND a=1 → WHERE a=1 |
| Outer Join Elimination | 消除不必要的 Outer Join | 当 Right 表不需要其他列时转为 Inner Join |
一条看似简单的 SQL 经过 RBO 后,执行计划可能已被大幅优化。我们可以通过 explain() 验证:
scala
df.filter($"uid" > 1000).select("name").explain(true)
// == Parsed Logical Plan ==
// 'Project ['name]
// +- Filter (uid#0 > 1000)
// +- Relation[id#0L,uid#1L,name#2] parquet
// == Analyzed Logical Plan ==
// Project [name#2]
// +- Filter (uid#1 > 1000)
// +- Relation[id#0L,uid#1L,name#2] parquet
// == Optimized Logical Plan ==
// Project [name#2] ← Column Pruning: 只读 name
// +- Filter (uid#1 > 1000) ← Predicate Pushdown: 已在 Parquet 层过滤
// +- Relation[id#0L,uid#1L,name#2] parquet
3.3 Phase 3:Physical Planning------代价优化(CBO)
Optimized Logical Plan 尚不能直接运行,因为 JOIN 算法、Shuffle 策略、聚合方式 都尚未确定。Physical Planning 阶段通过 Strategy + Cost Model 来选择最优的物理执行计划。
关键决策包括:
scala
// 示例:JoinSelection 策略
if (leftSize < spark.sql.autoBroadcastJoinThreshold) {
BroadcastHashJoinExec(left, right) // 小表广播,避免 Shuffle
} else {
SortMergeJoinExec(left, right) // 大表排序归并 Join
}
启用 CBO 需要执行 ANALYZE 收集统计信息:
sql
ANALYZE TABLE events COMPUTE STATISTICS;
ANALYZE TABLE events COMPUTE STATISTICS FOR COLUMNS uid, event_type;
SET spark.sql.cbo.enabled=true;
SET spark.sql.cbo.joinReorder.enabled=true;
3.4 Phase 4:Code Generation------WholeStageCodegen
传统关系型数据库大多采用 Volcano Iterator Model ------每个算子都有 open() / next() / close() 接口,层层嵌套调用 next():
Project.next() → Filter.next() → Scan.next() → (virtual call overhead × millions)
SparkSQL 抛弃了这种模式,而是将 多个算子融合为一个 Java 方法,通过 Janino 编译器在运行时动态编译为字节码,从而:
- 消除虚函数调用 ------ 所有逻辑在一个 for-loop 中完成
- 利于 JIT 内联 ------ JVM 可以更激进地优化编译
- CPU Cache 友好 ------ 数据在寄存器/缓存间流转,减少内存访问
在 Spark Web UI 的 SQL 页面中,WholeStageCodegen (1) 标记的 Stage 代表已启用全阶段代码生成:
== Physical Plan ==
*(1) Project [name#2]
+- *(1) Filter (uid#1 > 1000)
+- *(1) ColumnarToRow
+- FileScan parquet [name#2,uid#1] Batched: true, DataFilters: [(uid#1 > 1000)]
这里的 *(1) 就是 WholeStageCodegen 标识。
四、Tungsten:大数据时代的"特制 CPU 缓存格式"
Catalyst 决定了 执行什么 ,Tungsten 决定了 如何高效执行。它的核心设计包括:
4.1 堆外内存 + 二进制格式
传统 RDD 中的对象存在 JVM Heap 上,每个 Java 对象都有 Object Header + GC 开销。Tungsten 改用 UnsafeRow 直接操作堆外内存:
传统 Java Object (堆内):
┌──────────────┬────┬────┬──────────────┐
│ Object Header│ id │uid │ name │
│ (12-16B) │ 8B │ 8B │ var-length │
└──────────────┴────┴────┴──────────────┘
Tungsten UnsafeRow (堆外):
┌─────┬────┬──────────┐
│ 0-8B│8-16│ 16-N │ ← 紧致打包,无 Object Header
│ null│ uid│ name bytes│
│ bits│ │ │
└─────┴────┴──────────┘
4.2 缓存友好的数据结构
Tungsten 的 Sort 和 Hash Aggregation 使用 指针数组 + 二进制行 结构,数据以列式方式组织,让 CPU L1/L2 缓存更高效地预取数据:
java
// Tungsten 内部排序的核心结构
// 排序只操作 8 字节指针,实际数据留在原位
long[] ptrArray = new long[numRows]; // 每个元素 = 8 字节指针
// ... 排序的 comparator 解引用指针来比较实际值
五、DataFrame / Dataset / RDD:三者的关系与选择
Spark 提供了三个层级的编程抽象,理解它们的关系对于写出高性能代码至关重要:

5.1 三者关系
| 特性 | DataFrame | DatasetT | RDDT |
|---|---|---|---|
| 类型安全 | ❌ 运行时 | ✅ 编译时 | ✅ 编译时 |
| Catalyst 优化 | ✅ 完全 | ✅ 完全 | ❌ 无 |
| Tungsten 二进制 | ✅ UnsafeRow | ✅ Encoder | ❌ Java Heap |
| 代码简洁度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
| 性能 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
| 适用场景 | ETL、SQL 分析 | 复杂类型变换 | 自定义 Partition |
5.2 互转路径
scala
// DataFrame ↔ Dataset
val df: DataFrame = spark.read.parquet("...")
val ds: Dataset[Event] = df.as[Event] // DataFrame → Dataset
val df2: DataFrame = ds.toDF() // Dataset → DataFrame
// DataFrame/Dataset → RDD(损失优化)
val rdd: RDD[Row] = df.rdd // DataFrame → RDD (内部已是 UnsafeRow → Row)
val rdd2: RDD[Event] = ds.rdd // Dataset → RDD
// RDD → DataFrame/Dataset
val df3 = spark.createDataFrame(rdd, schema) // 手动指定 Schema
val ds3 = spark.createDataset(rdd) // 需要 Encoder
六、全链路执行------一条 SQL 的完整旅程
让我们追踪 SELECT uid, COUNT(*) FROM events WHERE dt='2024-01-01' GROUP BY uid 这条 SQL 的完整执行过程:

Step-by-Step
- Parser:ANTLR4 进行词法/语法分析,生成 AST → Unresolved LogicalPlan
- Analyzer:查询 Hive Metastore / SessionCatalog,解析 events 表的 schema,确定 uid 类型、dt 为 partition 列
- Optimizer (RBO) :
PushDownPredicate: dt='2024-01-01'→ 分区裁剪,只扫描dt=2024-01-01/目录ColumnPruning: uid→ 只读取 uid 列ReplaceDistinctWithAggregate→ 转换 COUNT
- Planner (CBO):根据 events 表大小,选择合适的 Join 和 Agg 策略(HashAggregate vs SortAggregate)
- WholeStageCodegen :将
Scan → Filter → Project → PartialAgg融合为一个 Java 方法 - DAGScheduler:切分为 Stage 0(Map)→ Stage 1(Reduce),生成 Task
- Executors:执行 Task,使用 Tungsten UnsafeRow 批量处理,结果返回 Driver
七、调优实战:榨干 SparkSQL 的性能
7.1 开启关键配置
properties
# Catalyst / CBO
spark.sql.cbo.enabled=true
spark.sql.cbo.joinReorder.enabled=true
spark.sql.statistics.histogram.enabled=true
# Adaptive Query Execution (Spark 3.0+)
spark.sql.adaptive.enabled=true
spark.sql.adaptive.coalescePartitions.enabled=true
spark.sql.adaptive.skewJoin.enabled=true
# WholeStageCodegen
spark.sql.codegen.wholeStage=true
spark.sql.codegen.maxFields=100
# Tungsten
spark.sql.tungsten.enabled=true
spark.sql.inMemoryColumnarStorage.batchSize=10000
7.2 使用 EXPLAIN 检查执行计划
sql
-- 查看物理计划(含 WholeStageCodegen 标记)
EXPLAIN COST SELECT uid, COUNT(*) FROM events WHERE dt='2024-01-01' GROUP BY uid;
-- 查看完整的优化阶段
EXPLAIN EXTENDED SELECT ...
重点关注:
- 是否存在 * (WholeStageCodegen 标记)------如缺失则可能 codegen 失败
- 是否有 BroadcastHashJoin------大表之间不应出现 ShuffleHashJoin
- Filter 是否被下推(PushedFilters)------如未下推,检查数据源是否支持
7.3 分区策略与数据源选择
- Parquet :列式存储、丰富统计信息、谓词下推效果好------OLAP 场景首选
- ORC:类似 Parquet,Hive 生态兼容性更好
- Delta Lake / Iceberg / Hudi:支持 ACID、Time Travel、Schema Evolution,适合生产级数仓
- 纯文本 (CSV/JSON):解析开销大,缺少统计信息,不适合大规模分析
八、总结
SparkSQL 的高性能并非单一技术的奇迹,而是 DataSource API、Catalyst Optimizer、Tungsten Engine、WholeStageCodegen 四层协同的成果:
- DataSource API 提供了统一的多源接入层,V2 API 进一步增强了列式读取和事务写入能力
- Catalyst Optimizer 通过四阶段(Analysis → RBO → CBO → CodeGen)将 SQL 逐步转换为高度优化的物理执行计划
- Tungsten 利用堆外内存和缓存友好的数据结构,将 CPU 和内存利用率提升到极致
- WholeStageCodegen 打破了 Volcano Iterator 模型的性能瓶颈,实现了算子级代码融合
理解这些底层机制,不仅能帮你写出更高效的 Spark 作业,更能在遇到性能瓶颈时,从执行计划中快速定位问题根源。当你下次面对一个慢查询时,不妨调出 EXPLAIN COST,对照本文的优化器管线,看看优化卡在了哪一步------这比盲目调参有效得多。
本文作者 :starzy
博客 :blog.starzy.cn
GitHub :starzy1990.github.io
专注 AI Agent · LangGraph · RAG · 大数据架构 · 数据工程实践
让 AI 真正落地,让数据创造价值