前两篇我们把 Spark 的整体架构和 RDD 讲完了。这一篇转向 Spark SQL------也就是 Spark 里专门用来处理结构化数据的模块。你会搞明白 DataFrame 是什么、SQL 风格和 DSL 风格各自的写法、窗口函数和炸裂函数怎么用、以及 Spark 怎么和 MySQL、Hive 这类外部系统打交道。
一、Spark SQL 是干什么的?
Spark SQL 是 Spark 用于处理结构化数据的模块。它的核心价值在于:能把写好的 SQL 语句转换成 RDD 去执行,因此在同样一份 Hive 任务上,运行速度比 Hive 这类传统工具更快。
- 处理对象是结构化数据------有明确字段和类型的数据,比如一张学生表、一份订单流水。
- 运行环境是 SparkSession(这一点和 Spark Core 用 SparkContext 不同,后面会讲)。
- 两种写法:SQL 风格和 DSL 风格,等价于"写 SQL"还是"写代码"两种口味。
二、两个核心抽象:DataFrame 与 DataSet
Spark SQL 的世界由两个核心抽象撑起来,理解它们就理解了 Spark SQL。
|---------|-----------------|----------------|
| 对比项 | DataFrame | DataSet |
| 定位 | Spark SQL 的基础抽象 | 更高级的抽象 |
| 形态 | 分布式、带分区的数据容器 | 与 DataFrame 类似 |
| 类比 | 像传统数据库里的二维表格 | 带类型约束的表格 |
| 特点 | 记录了数据的结构信息 | 类型更严格 |
可以这样记:DataFrame 像一张"带表头的二维表",一行一行整齐对齐,而且能自动记住每一列是什么类型;DataSet 则在此基础上把类型约束得更死一些。
需要澄清的一个常见误解:DataSet 这个概念源自 Scala。在 PySpark 里并没有提供强类型的 DataSet,而是直接用 DataFrame 来实现类似的功能。所以你在 PySpark 里看到的就是 DataFrame。
三、运行入口:SparkSession
在 Spark Core 里,我们用 SparkContext 来创建 RDD;而在 Spark SQL 里,对应的入口是 SparkSession。它可以理解为 Spark 应用的"总控台",既管理 DataFrame 的读取与操作,也兼容 SQL 查询。
from pyspark.sql import SparkSession
spark = (SparkSession.builder
.master("local\*")
.appName("spark-sql-demo")
.getOrCreate())
四、两种数据处理风格:SQL 与 DSL
同样是查数据,Spark SQL 提供了两条路径,殊途同归。
|---------|------------------------|----------------------------|
| 对比项 | SQL 风格 | DSL 风格 |
| 写法 | 用 spark.sql() 写 SQL 语句 | 用 DataFrame API 链式调用 |
| 常用方法 | sql()、createTempView() | select、filter、groupBy、join |
| 优点 | 贴近 SQL 生态,逻辑直观 | 类型提示友好,便于组合与复用 |
| 企业实践 | 适合已有 SQL 资产的场景 | 更倾向使用 DSL |
同样一句统计需求,两种写法
------ SQL 风格 ------
spark.sql("select dept, count(*) as cnt from staff group by dept").show()
------ DSL 风格 ------
df.select("dept").groupBy("dept").count().show()
五、SQL 风格下的三类基础操作
1. 字段计算
用 SELECT 语句对表里的字段做数学运算,并给算出来的新字段起个别名。
select name, age, age + 1 as age_add, age - 1 as age_sub
from student
2. 筛选数据
用 WHERE 子句按条件过滤,比如查年龄大于 20 的学生。
select name, age from student where age > 20
3. 分组统计
用 GROUP BY 分组,再配合 COUNT 统计每组的数量,这是最常见的一类统计需求。
select dept, count(*) as cnt from staff group by dept
六、DSL 风格三个实战场景
1. 读 CSV 并定义表结构
DSL 编程的第一步通常是读数据。用 read_csv 读取时可以用 schema 参数明确指定表结构,这样字段类型才是确定的,不会被默认推断带偏。
from pyspark.sql.types import StructType, StructField, StringType, IntegerType
schema = StructType(
StructField("id", StringType(), True),
StructField("name", StringType(), True),
StructField("age", IntegerType(), True),
StructField("country", StringType(), True),
)
df = spark.read.csv("hdfs://node1/students.csv", header=True, schema=schema)
2. 窗口函数:row_number() 做排名
窗口函数最常见的用途是排名。row_number() 会给每行数据在它的分组范围内编号,配合 partition by(按什么分组)和 order by(按什么排序)两个子句,就能实现各种维度的排名,比如"按国家分别排名"。
from pyspark.sql.window import Window
w = Window.partitionBy("country").orderBy("age", desc=True)
df.withColumn("rank", row_number().over(w)).show()
3. 炸裂函数:explode 把一行拆成多行
有一类很常见的数据形态:几个值被塞在同一个字段里,比如一个字符串是 "apple banana"。如果想按单词统计次数,就要用 explode 把它"炸"开成多行,再做分组计数。
df.createOrReplaceTempView("fruit") # 建临时视图
spark.sql("""
select word, count(*) as cnt
from fruit
lateral view explode(split(words, ' ')) t as word
group by word
""").show()
这里也体现了两种风格的差异:同样的逻辑,DSL 写法大致是 explode 拆列 → groupBy 计数 → 聚合,SQL 写法则借助 lateral view explode 完成拆列。两种风格各有优劣,企业实践中更倾向于使用 DSL。
4. 表连接:join 与字段别名
两张表有共同键(比如 id)时,可以用 join 把它们连起来,常用的是内连接(inner join),只保留两边都能匹配上的行。
result = df_a.join(df_b, on="id", how="inner")
两张表有同名字段(name、id)时,用 select + alias 区分来源
result.select(
col("a.name").alias("name_a"),
col("b.name").alias("name_b"),
"id"
).show()
七、RDD 与 DataFrame 的相互转换
RDD 和 DataFrame 是同一份数据的两种视图,它们之间可以来回切换,是打通 Spark Core 与 Spark SQL 的桥梁。
|-----------------|------------------------------------------|---------|
| 转换方向 | 怎么做 | 要点 |
| RDD → DataFrame | 先把 RDD 转成键值对,再用 createDataFrame 传 Schema | 必须定义表结构 |
| DataFrame → RDD | 直接取 df.rdd 属性 | 非常简单 |
RDD → DataFrame:先转 KV 格式,再带 Schema 创建
kv_rdd = sc.textFile("hdfs://node1/students.txt").map(lambda line: line.split(",")) \
.map(lambda x: (x0, x1, int(x2)))
df = spark.createDataFrame(kv_rdd, schema)
DataFrame → RDD:取 rdd 属性即可
back_rdd = df.rdd
Schema 怎么构建
Schema 是把 RDD 变成结构化数据的关键一步。它使用 StructType 类来定义,里面每一列是一个 StructField,由若干个 StructField 组成的列表就是一个完整的 Schema。常用类型有 StringType(字符串)、IntegerType(整数)等。
八、自定义函数(UDF)
当内置函数满足不了复杂业务需求时,可以自己写函数。用法上有两个要点:
- 定义方式很自由:可以写普通 Python 函数、用 lambda 表达式,或者写用户自定义函数(UDF)。
- 关键一步:使用前必须通过 spark.udf.register() 注册,注册后才能在 SQL 查询里被识别和调用。
- 注册时可以顺便指定一个别名,方便在 SQL 语句中直接引用。
1. 定义函数
def add_tag(age):
return "young" if age < 20 else "old"
2. 注册(并起别名),之后才能在 SQL 里用
spark.udf.register("add_tag", add_tag)
3. 在 SQL 中调用
spark.sql("select name, add_tag(age) as tag from student").show()
九、Spark SQL 的数据源
1. Parquet
Parquet 是一种高效的列式存储格式,压缩比高、查询性能好,是 Spark 生态里最常用的存储格式之一。
2. JDBC:读写关系型数据库
Spark SQL 支持通过 JDBC 协议读写 MySQL、PostgreSQL 这类关系型数据库。
读取:需要 JDBC URL、用户名、密码、驱动类名等参数
df = spark.read.jdbc(
url="jdbc:mysql://node1:3306/mydb",
table="student",
properties={"user": "root", "password": "******",
"driver": "com.mysql.cj.jdbc.Driver"}
)
写入:同样要配 URL、表名和写入模式
df.write.jdbc(
url="jdbc:mysql://node1:3306/mydb",
table="student_out",
mode="overwrite", # 可选 overwrite / append 等
properties={"user": "root", "password": "******",
"driver": "com.mysql.cj.jdbc.Driver"}
)
- 写入模式(mode)决定写入行为,比如 overwrite 是覆盖目标表,append 是追加数据。
- 如果目标表还不存在,Spark 写入时会自动创建一张结构对应的表。
十、Spark 与 Hive 的整合
Spark 和 Hive 整合有两种模式,方向不同、分工不同。
|---------------|--------------------|--------------------------|
| 模式 | 谁执行 | 说明 |
| Spark on Hive | Spark 引擎执行 HiveQL | 用 Spark 的速度跑 Hive 写的 SQL |
| Hive on Spark | Hive 引擎执行 Spark 任务 | 用 Hive 的调度管理 Spark 任务 |
整合的前提有三个:Hive 服务已经正常启动、Spark 侧配置好 Hive 元数据存储(metastore)的地址、并且把 MySQL 驱动 JAR 包放到 Spark 的 lib 目录下,让 Hive 能正确连上元数据库。
连上之后,就可以用 Spark SQL 在 Hive 里建表、插数据、查数据;同样的操作也可以用 Python 代码来实现,两种方式最终都是走 Spark 的执行引擎。
十一、常见故障的排查思路
最后说两个绕不开的"坑",了解一下原理就不容易慌。
1. 安全模式
Hadoop 的 NameNode 在系统冷启动后有时会进入"安全模式",此时服务看起来启不来、数据也无法正常读写。处理思路是先用专门命令把安全模式关掉,再重新启动服务即可。
2. 数据丢失
如果虚拟机直接关机导致进程被强行中断,可能引发 HDFS 上的数据丢失。处理思路是清理掉 HDFS 里相关的临时文件(比如 blk 临时文件)以及 Hive 的历史服务数据,然后重新启动服务,让集群恢复到干净状态。
十二、一句话记住 Spark SQL
Spark SQL 用 DataFrame 把分布式数据变成了"带表头的表格",用 SparkSession 作为入口,支持 SQL 风格和 DSL 风格两种写法;数据可以来自 CSV、JSON、Parquet,也可以通过 JDBC 连到 MySQL,甚至和 Hive 打通。工具链齐了,剩下的就是熟练度和业务理解了。