
文章目录
一、课前导读
在上一节课中,我们了解了Spark SQL的整体架构以及DataFrame相比RDD的巨大优势。你已经知道DataFrame是带Schema的分布式数据集,并且Catalyst优化器能让它跑得飞快。但你可能仍然有一个核心疑问:“我该如何在实际工作中创建DataFrame?创建之后,又该如何操作它?”
这是一个非常实际的问题。在企业大数据开发中,数据源五花八门:业务系统的MySQL表、埋点日志的JSON文件、数据仓库的Hive表、实时消息队列的流数据……如果不能熟练地从各种数据源创建DataFrame,后续的分析就无从谈起。同样,DataFrame提供了极其丰富的API——select、filter、groupBy、join、withColumn等,如果只掌握皮毛,写出的代码可能又长又难读,甚至性能低下。
本节课就是你的“DataFrame操练手册”。我们将系统讲解DataFrame的多种创建方式(从集合、RDD、文件、JDBC、Hive等),深入剖析DataFrame的内部数据结构(Row、Column、StructType),并通过大量实战案例带你掌握最常用的基础操作。学完这节课,你将能够流畅地将任何数据源转为DataFrame,并使用DSL风格进行数据清洗和转换。
二、学习目标
完成本节课的学习后,你将能够:
- 掌握DataFrame的多种创建方式:从列表、元组、RDD、外部文件(CSV/JSON/Parquet)、JDBC数据库、Hive表等创建DataFrame
- 理解DataFrame的数据结构:熟悉
Row对象、Column表达式、StructType和StructField定义Schema - 熟练使用基础操作:
show、printSchema、select、filter/where、withColumn、drop、orderBy、groupBy、agg、join等 - 区分不同列操作:掌握
col、expr、字符串列名等不同写法的使用场景 - 处理嵌套结构:能够访问
StructType中的子字段和ArrayType中的元素 - 优化DataFrame代码:避免常见性能陷阱,如误用UDF、频繁collect等
三、核心理论知识点
| 知识点 | 说明 |
|---|---|
| 创建DataFrame | 多种输入源:本地集合、RDD、外部文件、JDBC、Hive |
| Schema定义 | 使用StructType和StructField定义Schema,或自动推断 |
| Row对象 | DataFrame中的一行数据,可通过索引或列名访问字段 |
| Column对象 | 列表达式,通过col("name")或df["name"]获取 |
| 常用操作 | show、select、filter、withColumn、groupBy、agg、join等 |
| 列操作函数 | alias、cast、when、otherwise、内置函数等 |
| 缺失值处理 | dropna、fillna、replace |
| 去重与采样 | distinct、dropDuplicates、sample |
| 数据集操作 | union、intersect、except |
| 与RDD互转 | df.rdd、spark.createDataFrame(rdd, schema) |
四、原理通俗讲解
4.1 DataFrame的创建:数据源的“通用转换器”
可以把SparkSession看作一个“数据转换工厂”,它提供了read接口,能够从各种数据源读取数据,统一转换为DataFrame。无论源文件是CSV、JSON、Parquet,还是关系型数据库,你都可以用几乎相同的代码完成读取。
例如:
df_csv = spark.read.csv("path/to/file.csv", header=True)
df_json = spark.read.json("path/to/file.json")
df_parquet = spark.read.parquet("path/to/file.parquet")
df_jdbc = spark.read.format("jdbc").option("url", "jdbc:mysql://...").option("dbtable", "table").load()
这种统一的API大大降低了学习成本。同时,write接口也对称地支持多种格式输出。
4.2 DataFrame的数据结构:二维表 + 强类型元数据
DataFrame在逻辑上就是一张数据库表:行(Row)和列(Column)。每个列都有明确的数据类型(String、Integer、Double、Array、Map、Struct等)。这种带Schema的结构使得Spark可以进行很多智能优化,比如只读取需要的列(列剪枝),只读取符合条件的分区(分区剪枝)等。
物理上,DataFrame底层是由RDD[Row] + Schema信息组成的,但Row对象并不存储列的类型信息——类型信息完全由StructType管理。这意味着在访问列数据时,你需要按照正确的类型提取,例如row.getInt(0)或row["age"]返回的是Python对象但带类型。
4.3 DataFrame的操作:构建逻辑计划树
当你写df.select("name").filter(df.age > 18)时,Spark并不是立即执行,而是构建一棵抽象语法树(AST)。例如上述代码会生成一个逻辑计划:Project(name) ← Filter(age > 18) ← DataFrame。Catalyst优化器会改写这棵树,比如将Filter下推到数据源(谓词下推),然后生成物理计划并执行。整个过程对用户透明,但你写的代码越简洁,优化器越容易发挥威力。
五、重点概念拆解
5.1 创建DataFrame的各种方式
5.1.1 从本地集合创建
# 方式1:自动推断类型
data = [(1, "Alice", 25), (2, "Bob", 30)]
df = spark.createDataFrame(data, schema=["id", "name", "age"])
# 方式2:显式指定Schema
from pyspark.sql.types import StructType, StructField, IntegerType, StringType
schema = StructType([
StructField("id", IntegerType(), True),
StructField("name", StringType(), True),
StructField("age", IntegerType(), True)
])
df = spark.createDataFrame(data, schema)
5.1.2 从RDD创建
rdd = sc.parallelize([(1, "Alice", 25), (2, "Bob", 30)])
# 方法1:toDF
df = rdd.toDF(["id", "name", "age"])
# 方法2:createDataFrame
df = spark.createDataFrame(rdd, schema)
5.1.3 从外部文件创建
# CSV
df = spark.read.csv("/path/file.csv", header=True, inferSchema=True)
# JSON
df = spark.read.json("/path/*.json")
# Parquet(最推荐)
df = spark.read.parquet("/path/table.parquet")
# Text(每行一个字符串)
df = spark.read.text("/path/file.txt")
5.1.4 从JDBC创建
df = spark.read.format("jdbc") \
.option("url", "jdbc:mysql://localhost:3306/db") \
.option("dbtable", "table") \
.option("user", "root") \
.option("password", "pwd") \
.load()
5.1.5 从Hive表创建
spark.sql("USE mydatabase")
df = spark.table("my_table")
# 或直接SQL
df = spark.sql("SELECT * FROM my_table")
5.2 DataFrame的重要属性
df.columns:返回列名列表df.dtypes:返回(列名, 类型)列表df.schema:返回StructType对象df.isLocal:是否本地(测试用)df.rdd:底层的RDD
5.3 常用基础操作
| 操作 | 示例 | 说明 |
|---|---|---|
| 查看数据 | df.show(10, truncate=False) | 显示前N行,可控制截断 |
| Schema | df.printSchema() | 打印结构 |
| 选择列 | df.select("col1", "col2") | 返回新DataFrame |
| 添加列 | df.withColumn("new", col("col1") + 1) | 返回新DF |
| 删除列 | df.drop("col1") | 返回新DF |
| 重命名列 | df.withColumnRenamed("old", "new") | - |
| 过滤 | df.filter(col("age") > 18) 或 df.where(...) | - |
| 去重 | df.distinct() 或 df.dropDuplicates(["col"]) | - |
| 排序 | df.orderBy("age", ascending=False) | - |
| 分组 | df.groupBy("dept").agg(avg("salary")) | - |
| 采样 | df.sample(0.1, seed=42) | 10%采样 |
| 限制行数 | df.limit(10) | - |
5.4 列表达式详解
在DataFrame操作中,列可以用多种方式表示:
- 字符串:
"age" col函数:col("age")df["age"]df.age(不推荐,容易与DataFrame方法冲突)- 字符串表达式:
expr("age + 1")
其中col和expr最灵活,expr可以写复杂表达式,如expr("CASE WHEN age > 18 THEN 'adult' ELSE 'minor' END")。
5.5 内置函数
pyspark.sql.functions提供了丰富的内置函数,例如:
- 聚合:
sum,avg,count,min,max,collect_list,collect_set - 窗口:
row_number,rank,lag,lead - 集合:
array_contains,size,explode - 字符串:
concat,split,regexp_extract,trim,upper,lower - 日期:
to_date,year,month,date_add - 条件:
when,otherwise,coalesce,isnull
使用内置函数比UDF快几个数量级,应优先使用。
六、易错点避坑
6.1 频繁使用collect()或toLocalIterator()
在大数据集上调用collect()会将所有数据拉取到Driver内存,极易OOM。仅在确认结果很小或为了调试时使用。
6.2 混淆filter和where
两者完全等价,where是filter的别名,可以根据习惯选择。
6.3 误用selectExpr
selectExpr接受SQL表达式字符串,如df.selectExpr("id", "age + 1 as age_plus_one"),比select加alias更简洁,但要注意SQL注入风险(一般内部使用没问题)。
6.4 忽视Schema定义导致的性能问题
如果从RDD创建DataFrame时不提供Schema,Spark会通过采样(sampleRatio参数控制)来推断类型。对于大数据集,采样开销大且可能推断不准。最佳实践是显式定义StructType。
6.5 未处理CSV中的空值或坏记录
CSV读取时,默认将无法解析的值转为null,但你可能需要更多控制:.option("mode", "DROPMALFORMED")或.option("mode", "FAILFAST")。
6.6 对DataFrame进行逐行操作
如使用df.foreach(lambda row: ...)效率极低,因为会触发大量对象创建。应使用foreachPartition批量操作或直接使用DataFrame内置函数。
七、完整实战案例
本案例将演示从各种数据源创建DataFrame,并执行常见的数据清洗和分析操作,覆盖select、filter、groupBy、join、withColumn、dropDuplicates等,最后保存结果。
# ============== dataframe_basics_demo.py ==============
# 功能:PySpark DataFrame 多种创建方式与基础操作实战
# 涵盖:创建、结构查看、选择、过滤、添加列、聚合、连接、写入等
from pyspark.sql import SparkSession
from pyspark.sql.types import StructType, StructField, IntegerType, StringType, DoubleType, ArrayType
from pyspark.sql.functions import col, expr, when, avg, count, sum as spark_sum, concat, lit, year, month
import random
import os
# ========== 1. 创建SparkSession ==========
spark = SparkSession.builder \
.appName("DataFrameBasics") \
.master("local[4]") \
.config("spark.sql.shuffle.partitions", "4") \
.getOrCreate()
sc = spark.sparkContext
sc.setLogLevel("WARN")
print("=" * 80)
print("PySpark DataFrame 多种创建方式与基础操作实战")
print("=" * 80)
# ========== 2. 创建DataFrame的各种方式 ==========
print("\n步骤1: 创建DataFrame")
print("-" * 60)
# 2.1 从列表创建(自动推断Schema)
data1 = [(1, "Alice", 100, "HR"), (2, "Bob", 200, "IT"), (3, "Cathy", 150, "IT")]
df1 = spark.createDataFrame(data1, schema=["id", "name", "score", "dept"])
print("方式1: 从列表+列名创建")
df1.show()
# 2.2 显式定义Schema(推荐,性能好且精确)
schema2 = StructType([
StructField("id", IntegerType(), False),
StructField("name", StringType(), True),
StructField("salary", DoubleType(), True),
StructField("dept", StringType(), True)
])
data2 = [(4, "David", 2500.0, "Finance"), (5, "Ella", 3000.0, "HR")]
df2 = spark.createDataFrame(data2, schema2)
print("方式2: 显式Schema")
df2.printSchema()
df2.show()
# 2.3 从RDD[Row]创建
from pyspark.sql import Row
rdd = sc.parallelize([Row(id=6, name="Frank", salary=2800.0, dept="IT"),
Row(id=7, name="Grace", salary=3100.0, dept="HR")])
df3 = spark.createDataFrame(rdd)
print("方式3: 从RDD[Row]创建")
df3.show()
# 2.4 从RDD[tuple] + toDF
rdd_tup = sc.parallelize([(8, "Henry", 2700.0, "Finance"), (9, "Ivy", 2900.0, "IT")])
df4 = rdd_tup.toDF(["id", "name", "salary", "dept"])
print("方式4: RDD.toDF")
df4.show()
# 2.5 从CSV文件(先准备一个测试CSV)
csv_path = "/tmp/employees.csv"
with open(csv_path, "w") as f:
f.write("id,name,salary,dept\n")
f.write("10,John,3200,HR\n11,Jane,2800,IT\n12,Jack,4000,Finance\n")
df_csv = spark.read.csv(csv_path, header=True, inferSchema=True)
print("方式5: 从CSV读取")
df_csv.show()
# 2.6 从JSON文件(生成简单JSON)
json_path = "/tmp/employees.json"
with open(json_path, "w") as f:
f.write('{"id":13,"name":"Kevin","salary":3600,"dept":"IT"}\n')
f.write('{"id":14,"name":"Lisa","salary":4100,"dept":"HR"}\n')
df_json = spark.read.json(json_path)
print("方式6: 从JSON读取")
df_json.show()
# 2.7 从Parquet(先写入再读,演示)
parquet_path = "/tmp/employees_parquet"
df1.write.mode("overwrite").parquet(parquet_path)
df_parquet = spark.read.parquet(parquet_path)
print("方式7: 从Parquet读取")
df_parquet.show(2)
# ========== 3. 合并多个DataFrame(union) ==========
print("\n" + "=" * 80)
print("步骤2: 合并DataFrame (union)")
print("=" * 80)
# 注意:union要求两个DataFrame的列数、顺序、类型一致,不一致需先select调整
# 统一列顺序
df1_aligned = df1.select("id", "name", "score", "dept")
df2_aligned = df2.select("id", "name", "salary", "dept").withColumnRenamed("salary", "score")
df_combined = df1_aligned.union(df2_aligned)
print("合并后DataFrame:")
df_combined.show()
# ========== 4. 基础操作:查看Schema和数据 ==========
print("\n" + "=" * 80)
print("步骤3: 查看数据结构")
print("=" * 80)
print("列名:", df_combined.columns)
print("数据类型:", df_combined.dtypes)
print("Schema详细:")
df_combined.printSchema()
print("前3行数据:")
df_combined.show(3, truncate=False)
# ========== 5. 选择列和表达式 ==========
print("\n" + "=" * 80)
print("步骤4: 选择列 (select, selectExpr)")
print("=" * 80)
# 普通选择
df_combined.select("id", "name").show(3)
# 使用col函数和表达式
df_combined.select(col("id"), col("name"), expr("score * 1.1 as bonus")).show(3)
# 使用selectExpr写SQL表达式
df_combined.selectExpr("id", "name", "score", "CASE WHEN score >= 3000 THEN 'High' ELSE 'Low' END as level").show(3)
# ========== 6. 添加/删除/重命名列 ==========
print("\n" + "=" * 80)
print("步骤5: 列操作 (withColumn, drop, withColumnRenamed)")
print("=" * 80)
# 添加列:score转换为千分位显示(字符串),增加奖金列
df_with_new = df_combined \
.withColumn("score_k", col("score") / 1000) \
.withColumn("bonus", when(col("score") > 3000, col("score") * 0.1).otherwise(col("score") * 0.05)) \
.withColumn("full_name", concat(col("name"), lit(" (Employee)"))) \
.drop("score_k") # 演示drop,先加再删
print("添加多列后:")
df_with_new.select("name", "score", "bonus", "full_name").show()
# 重命名列
df_renamed = df_combined.withColumnRenamed("score", "salary")
print("重命名后列名:", df_renamed.columns)
# ========== 7. 过滤数据 ==========
print("\n" + "=" * 80)
print("步骤6: 过滤 (filter / where)")
print("=" * 80)
# 多种过滤方式
filtered1 = df_combined.filter(col("score") > 2800)
filtered2 = df_combined.where(df_combined.score > 2800)
filtered3 = df_combined.filter("score > 2800")
print("score > 2800 的记录:")
filtered1.show()
# 复合条件:IT部门且score>2500
filtered_complex = df_combined.filter((col("dept") == "IT") & (col("score") > 2500))
print("IT部门且score>2500:")
filtered_complex.show()
# 使用isin
filtered_in = df_combined.filter(col("dept").isin(["IT", "HR"]))
print("部门为IT或HR:")
filtered_in.show(2)
# ========== 8. 去重与采样 ==========
print("\n" + "=" * 80)
print("步骤7: 去重 (distinct, dropDuplicates) 与采样")
print("=" * 80)
# 创建包含重复数据的DataFrame
dup_data = [(1, "Alice"), (1, "Alice"), (2, "Bob")]
dup_df = spark.createDataFrame(dup_data, ["id", "name"])
print("原始重复数据:")
dup_df.show()
print("distinct去重:")
dup_df.distinct().show()
print("按id去重(保留第一个):")
dup_df.dropDuplicates(["id"]).show()
# 采样
sample_df = df_combined.sample(fraction=0.5, withReplacement=False, seed=42)
print("50%采样结果(行数可能减少):")
sample_df.show()
# ========== 9. 排序与限制 ==========
print("\n" + "=" * 80)
print("步骤8: 排序 (orderBy, sort) 与限制 (limit)")
print("=" * 80)
# 按score降序,然后按name升序
sorted_df = df_combined.orderBy(col("score").desc(), col("name").asc())
print("按score降序排序:")
sorted_df.show()
# 取前3条
limit_df = df_combined.limit(3)
print("前3条:")
limit_df.show()
# ========== 10. 分组聚合 ==========
print("\n" + "=" * 80)
print("步骤9: 分组聚合 (groupBy, agg)")
print("=" * 80)
# 简单分组计数
dept_count = df_combined.groupBy("dept").count()
print("各部门人数:")
dept_count.show()
# 多种聚合指标
dept_stats = df_combined.groupBy("dept").agg(
avg("score").alias("avg_score"),
spark_sum("score").alias("total_score"),
count("*").alias("cnt")
)
print("各部门统计:")
dept_stats.show()
# 使用pivot(类似Excel透视表)- 需要先准备数据
# 假设有年份部门薪水数据
pivot_data = [(2020, "IT", 1000), (2020, "HR", 800), (2021, "IT", 1200), (2021, "HR", 900)]
pivot_df = spark.createDataFrame(pivot_data, ["year", "dept", "salary"])
pivot_result = pivot_df.groupBy("year").pivot("dept").sum("salary")
print("透视表(每年各部门薪水总和):")
pivot_result.show()
# ========== 11. 连接操作 (Join) ==========
print("\n" + "=" * 80)
print("步骤10: 连接DataFrame (join)")
print("=" * 80)
# 创建部门奖金系数表
bonus_df = spark.createDataFrame([("IT", 0.2), ("HR", 0.15), ("Finance", 0.25)], ["dept", "bonus_rate"])
print("部门奖金系数表:")
bonus_df.show()
# 内连接
joined = df_combined.join(bonus_df, on="dept", how="inner")
print("内连接结果:")
joined.select("id", "name", "dept", "score", "bonus_rate").show()
# 左连接
left_joined = df_combined.join(bonus_df, on="dept", how="left")
print("左连接(保留所有员工):")
left_joined.select("id", "name", "dept", "score", "bonus_rate").show()
# 带条件的连接(非等值连接)
# 例如:分数在某个区间匹配奖金等级(略)
# ========== 12. 处理缺失值 ==========
print("\n" + "=" * 80)
print("步骤11: 缺失值处理 (dropna, fillna)")
print("=" * 80)
# 创建含有空值的DataFrame
null_data = [(1, "Alice", None), (2, None, 200), (3, "Bob", 300)]
null_df = spark.createDataFrame(null_data, ["id", "name", "score"])
print("原始含空值数据:")
null_df.show()
print("删除任何包含空值的行:")
null_df.dropna().show()
print("删除name列全为空的行(只检查指定列):")
null_df.dropna(subset=["name"]).show()
print("填充空值:")
null_df.fillna({"name": "Unknown", "score": 0}).show()
# ========== 13. 与RDD互转示例 ==========
print("\n" + "=" * 80)
print("步骤12: DataFrame <-> RDD 转换")
print("=" * 80)
# DataFrame -> RDD
rdd_converted = df_combined.rdd
print("转换后的RDD分区数:", rdd_converted.getNumPartitions())
print("RDD第一行:", rdd_converted.first())
# RDD -> DataFrame (需指定schema)
new_df = spark.createDataFrame(rdd_converted, schema=df_combined.schema)
print("RDD转回DataFrame后Schema一致:", new_df.schema == df_combined.schema)
# ========== 14. 写入数据到外部存储 ==========
print("\n" + "=" * 80)
print("步骤13: 写入数据")
print("=" * 80)
output_dir = "/tmp/df_output"
# 写入Parquet(推荐)
df_combined.write.mode("overwrite").parquet(f"{output_dir}/parquet")
print(f"Parquet输出到 {output_dir}/parquet")
# 写入JSON
df_combined.write.mode("overwrite").json(f"{output_dir}/json")
print(f"JSON输出到 {output_dir}/json")
# 写入CSV(需要合并文件,默认会分多部分)
df_combined.coalesce(1).write.mode("overwrite").csv(f"{output_dir}/csv", header=True)
print(f"CSV输出到 {output_dir}/csv")
# 清理临时文件
import shutil
shutil.rmtree(output_dir, ignore_errors=True)
# ========== 15. 性能优化提示 ==========
print("\n" + "=" * 80)
print("步骤14: 性能优化小贴士")
print("=" * 80)
print("""
- 尽量使用内置函数,避免UDF(Python UDF会破坏Catalyst优化)
- 使用select时只选择需要的列,减少数据传输
- filter尽可能提前,实现谓词下推
- join时广播小表:`df_big.join(broadcast(df_small), "key")`
- 合理设置`spark.sql.shuffle.partitions`(默认200,可根据数据量调整)
- 使用`df.cache()`缓存重复使用的DataFrame
""")
# ========== 16. 清理资源 ==========
spark.stop()
print("\n✅ DataFrame基础操作演示完成")
八、代码逐行解析
8.1 创建DataFrame部分
spark.createDataFrame是最核心的方法,第一个参数可以是列表、RDD,第二个参数是schema(可以是列名列表或StructType)。- 显式定义
StructType时,StructField包含名称、类型、是否可为空。可以为复杂类型嵌套。 - 从RDD[Row]创建时,Row对象可以像字典一样访问字段,Spark会自动推断Schema。
8.2 合并DataFrame
union要求两个DataFrame的Schema完全一致(包括列顺序)。如果不一致,需要用select调整顺序或添加缺失列。unionByName可以根据列名合并(Spark 3.x支持),更安全但略慢。
8.3 列操作
withColumn用于添加或替换列,第一个参数是列名,第二个是列表达式。when+otherwise实现条件分支,比在Python中写UDF高效得多。drop可以一次删除多列:df.drop("col1", "col2")。
8.4 过滤
filter和where完全相同,可接受字符串表达式或Column对象。- 使用
&(与)、|(或)、~(非)组合多个条件,注意括号。
8.5 聚合与连接
groupBy返回GroupedData对象,再调用agg或内置聚合函数。pivot是高级功能,用于将行转列,但可能产生大量列,需注意性能。join支持inner、left、right、outer等类型,可指定多个连接键:on=["key1", "key2"]。
8.6 缺失值处理
dropna的thresh参数:至少有多少个非空值才保留行;subset指定检查的列。fillna可以传入字典,按列填充不同值。
九、业务场景落地应用
9.1 场景一:数据湖中的批量数据读取
某公司每天将业务数据写入S3的Parquet文件,按日期分区(/data/dt=2024-01-01/)。使用Spark SQL可以高效读取指定日期范围的数据。
df = spark.read.parquet("/data")
df.filter((col("dt") >= "2024-01-01") & (col("dt") <= "2024-01-31")) \
.groupBy("product_id").agg(sum("revenue")).show()
9.2 场景二:多源数据融合
实时任务需要将Kafka中的用户行为数据(JSON)与MySQL中的用户维度表关联。
# 读取Kafka(结构化流,略),转为DataFrame
# 读取MySQL用户表
users_df = spark.read.format("jdbc").options(
url="jdbc:mysql://db:3306/warehouse",
dbtable="users",
user="spark",
password="xxx"
).load()
# 流式DataFrame与静态DataFrame做lookup join
joined = streaming_df.join(broadcast(users_df), "user_id", "left")
9.3 场景三:数据质量检查
利用dropna、fillna和自定义检查规则,生成数据质量报告。
# 统计每列缺失率
total = df.count()
null_counts = df.select([(count(when(col(c).isNull(), c)) / total).alias(c) for c in df.columns])
9.4 场景四:生成报表并导出
将聚合结果写入Excel(需转Pandas)或数据库。
result_df = df.groupBy("category").sum("sales")
result_df.write.jdbc(url="jdbc:mysql://dw:3306/report", table="daily_sales", mode="overwrite")
十、常见报错排查
10.1 AnalysisException: cannot resolve 'xxx'
原因:引用的列名不存在,或列名大小写写错(Spark默认区分大小写,除非配置spark.sql.caseSensitive=false)。
解决:检查列名,使用df.columns查看。
10.2 TypeError: condition should be string or Column
原因:filter中使用了Python布尔表达式如df.age > 18(df.age是Column对象,但>返回Column,可以这样写,但容易混淆)。实际错误可能是将and、or用于Column对象。
解决:使用&、|运算符,并用括号括住每个条件。
10.3 Py4JError 在collect时
原因:UDF或表达式中有非法操作(如除零),导致Executor端Python进程崩溃。
解决:检查UDF内部异常处理,或用when避免除零等。
10.4 读取CSV时数据全变为null
原因:CSV中某些列的类型无法推断,比如数值列中包含空字符串。或者分隔符错误、转义符问题。
解决:显式指定schema,设置.option("mode", "PERMISSIVE")或.option("nullValue", "")。
10.5 org.apache.spark.SparkException: Job aborted due to stage failure
原因:Shuffle阶段数据倾斜或节点失败。
解决:检查数据是否倾斜,增加分区或使用加盐打散。
十一、本节课知识点总结
DataFrame创建方式一览
| 数据源 | 代码示例 |
|---|---|
| 本地集合 | spark.createDataFrame([...], schema) |
| RDD | rdd.toDF(cols) 或 spark.createDataFrame(rdd, schema) |
| CSV | spark.read.csv(path, header=True, inferSchema=True) |
| JSON | spark.read.json(path) |
| Parquet | spark.read.parquet(path) |
| JDBC | spark.read.format("jdbc").option(...).load() |
| Hive | spark.table("db.table") 或 spark.sql("SELECT ...") |
常用操作速查
| 操作 | 方法 |
|---|---|
| 选择列 | df.select("col1", "col2") |
| 添加列 | df.withColumn("new", expr) |
| 过滤 | df.filter(col("age") > 18) |
| 聚合 | df.groupBy("key").agg(sum("value")) |
| 连接 | df1.join(df2, "key", "inner") |
| 排序 | df.orderBy(col("col").desc()) |
| 去重 | df.distinct() |
| 采样 | df.sample(0.1) |
性能最佳实践
- 尽量使用内置函数而非UDF
- 使用
select只取需要的列 - 在join前对小表使用
broadcast - 合理设置
spark.sql.shuffle.partitions - 对重复使用的DataFrame执行
cache - 避免
collect()大数据
十二、课后思考作业
作业一:理论理解题
-
为什么显式提供Schema比自动推断性能更好?自动推断会有什么问题?
-
DataFrame的
union和unionByName有什么区别?分别适用于什么场景? -
请解释
df.filter("age > 18")和df.filter(col("age") > 18)在内部处理上有什么差异?哪种更推荐?
作业二:代码实践题
-
读取一个CSV文件(可从Kaggle下载任意数据集),完成以下操作:
- 打印Schema和前10行
- 选出3列,并添加一列(例如将某列值乘以2)
- 按某列分组,计算平均值和计数
- 将结果保存为Parquet文件
-
创建两个DataFrame:员工表(id, name, dept_id)和部门表(dept_id, dept_name)。使用join将它们关联,并展示结果。
-
编写一个函数,接收DataFrame,自动计算每列的缺失率,并返回缺失率超过30%的列名列表。
作业三:场景应用题
某电商平台每天产生大量订单日志(JSON格式),包含字段:order_id、user_id、amount、status、create_time。现需要每日统计:
- 当天订单总数、总金额、用户数(去重)
- 按小时统计订单量分布
- 筛选出金额大于1000的高价值订单,单独输出
请设计基于DataFrame的方案,写出核心代码,并说明如何优化读取JSON文件的性能。
作业四:拓展研究
-
阅读
pyspark.sql.functions中至少10个内置函数的文档,编写示例代码验证其功能。 -
研究Spark SQL的
catalog对象(spark.catalog),了解如何列出数据库、表、函数等元数据。 -
对比
df.write.partitionBy和df.write.bucketBy的区别,分别在什么场景下使用?
提交方式:本次作业要求提交可运行的Python脚本和运行结果截图。鼓励使用真实数据集进行练习。
扩展阅读:
- Spark官方DataFrame API文档:pyspark.sql module
- 《Spark: The Definitive Guide》第8-12章
通过本节课的学习,你已经掌握了DataFrame的创建和基础操作,能够处理大部分日常的数据提取、转换、加载任务。下一节课我们将深入学习PySpark SQL的完整语法,包括过滤、分组、聚合、排序、多表关联等高级查询技巧,让你像操作数据库一样灵活处理分布式数据。我们下节课见!
🔗《20节课 PySpark 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

1276

被折叠的 条评论
为什么被折叠?



