
文章目录
一、课前导读
在实际的企业大数据开发中,数据从来不会只存在于一个地方。你可能会遇到这样的场景:从Hive中读取用户画像数据,关联MySQL中的订单维表,再结合实时Kafka中的点击流,最后将分析结果写入Parquet格式的数据湖。这就涉及到了Spark与多种数据源的交互能力。
PySpark内置了强大的数据源API,支持读写CSV、JSON、Parquet、ORC、Avro等文件格式,同时也支持JDBC关系型数据库、Hive数据仓库、HBase、Cassandra等系统。然而,不同数据源的特性差异巨大:CSV虽然通用但性能差,Parquet列式存储性能好但生态相对封闭,JDBC需要处理连接池和并发读取问题,Hive集成则需要注意元数据同步。如果对数据源的配置参数不熟悉,轻则性能低下,重则任务失败或数据损坏。
本节课将系统讲解PySpark中最常用的五大数据源:CSV、JSON、Parquet、Hive、MySQL(JDBC)。我们会深入每个数据源的核心参数、最佳实践以及易错点,并通过一个完整的实战案例演示:从MySQL读取业务表,关联Hive中的维度表,经过清洗转换后保存为Parquet,同时导出部分结果到JSON。学完这节课,你将能够灵活地在各种存储系统之间搬运和处理数据。
二、学习目标
完成本节课的学习后,你将能够:
- 掌握CSV文件读写:设置分隔符、header、schema推断、处理空值、压缩等
- 掌握JSON文件读写:处理嵌套JSON、多行JSON、模式定义
- 掌握Parquet文件读写:了解列式存储优势,设置分区、压缩格式
- 集成Hive数据仓库:启用Hive支持,读写Hive表,管理分区
- 使用JDBC连接关系型数据库:并行读取、写入模式、事务控制
- 理解数据源选项:根据不同数据源配置相应的性能参数
- 处理Schema演化:处理不同批次数据的Schema变化
- 性能优化:分区读取、批量写入、谓词下推、列剪枝等
三、核心理论知识点
| 数据源 | 核心参数 | 性能特点 | 适用场景 |
|---|---|---|---|
| CSV | header, inferSchema, sep, quote, escape, mode | 通用但慢,需要解析 | 数据交换、外部系统导入导出 |
| JSON | multiline, primitivesAsString, prefersDecimal | 灵活但占用空间大 | 半结构化数据、API日志 |
| Parquet | compression, partitionBy, mergeSchema | 列式存储、高性能、压缩率高 | 数据湖、分析型场景 |
| Hive | partitionOverwriteMode, enableHiveSupport | 集成Hive元数据,支持SQL | 数据仓库、已有Hive系统 |
| JDBC | url, dbtable, fetchsize, batchsize, isolationLevel | 需要控制并发,易成瓶颈 | 关系型数据库导入导出 |
四、原理通俗讲解
4.1 数据源API的统一抽象
Spark的数据源API遵循一个统一的接口:DataFrameReader和DataFrameWriter。无论底层是什么格式,你都使用spark.read.format(...).option(...).load()和df.write.format(...).option(...).save()。这种设计使得切换数据源变得非常简单,只需要改变format和部分option即可。
4.2 列式存储 vs 行式存储
CSV、JSON是典型的行式存储:每一行的数据在物理上是连续存放的。读取时,即使只需要一列,也必须扫描整行。而Parquet是列式存储:每列的数据连续存放。查询时只需读取需要的列,并且可以利用列的同质性进行高效压缩(如字典编码、RLE),因此性能远超行式存储。在企业大数据平台中,Parquet已经成为事实标准。
4.3 谓词下推与列剪枝
Spark读取数据源时,会尽可能将过滤条件(谓词)推送到数据源本身执行。例如,读取Parquet时,Spark会读取文件的元数据(每个行组的统计信息如min/max),直接跳过不符合条件的数据块。列剪枝是指只读取查询中实际用到的列。这两种优化大幅减少了I/O。
JDBC数据源也支持谓词下推:Spark会根据filter条件生成带WHERE子句的SQL,推送到数据库执行,减少数据传输量。
4.4 分区读取与并行度
对于JDBC数据源,Spark可以同时建立多个数据库连接,根据分区列将数据划分成多个分区并行读取,极大提高读取速度。需要合理设置分区键(最好是数值型、分布均匀的列,如自增ID或时间戳)和分区数。
对于文件数据源,文件本身的分块(如HDFS块大小)决定了分区数;也可以通过spark.sql.files.maxPartitionBytes控制每个分区读取的最大字节数。
五、重点概念拆解
5.1 CSV读写详解
读取CSV:
df = spark.read.csv(
path="path/to/file.csv",
header=True, # 第一行作为列名
inferSchema=True, # 自动推断类型(开销大,建议显式指定)
sep=",", # 分隔符
quote='"', # 引号字符
escape='\\', # 转义符
nullValue="NULL", # 表示空值的字符串
mode="PERMISSIVE", # 解析模式:PERMISSIVE/DROPMALFORMED/FAILFAST
multiLine=False # 是否允许多行(跨行记录)
)
写入CSV:
df.write.csv(
path="output.csv",
header=True,
sep=",",
mode="overwrite" # overwrite/append/ignore/error
)
注意:CSV写入默认会产生多个part文件(与分区数相同),如需单文件可使用coalesce(1).write...,但会引发Shuffle。
5.2 JSON读写详解
读取JSON:
df = spark.read.json(
path="path/to/*.json",
multiline=False, # 每条记录是否跨多行(否则每行一个JSON)
primitivesAsString=False, # 是否将原始类型(数字、布尔)当作字符串
allowComments=False # 是否允许注释
)
写入JSON:
df.write.json("output.json", mode="overwrite")
JSON格式可读性好,但体积大、解析慢,一般用于调试或数据交换。
5.3 Parquet读写详解
Parquet是Spark默认的数据源格式(spark.sql.sources.default,默认为parquet)。
读取:
df = spark.read.parquet("path/to/table.parquet")
写入:
df.write.parquet(
"output.parquet",
mode="overwrite",
compression="snappy" # snappy, gzip, lzo, none
)
分区写入:
df.write.partitionBy("dt", "hour").parquet("path")
这将按dt和hour字段创建目录层级,例如dt=2024-01-01/hour=10/part-xxx.snappy.parquet。分区剪枝可以大幅提升查询速度。
Schema合并:当读取多个不同Schema的Parquet文件时,可以设置spark.sql.parquet.mergeSchema=true自动合并Schema(但性能开销大)。
5.4 Hive集成
使用Hive集成需要创建SparkSession时启用enableHiveSupport(),并配置Hive Metastore。
spark = SparkSession.builder \
.appName("HiveDemo") \
.config("spark.sql.warehouse.dir", "/user/hive/warehouse") \
.config("hive.metastore.uris", "thrift://metastore-host:9083") \
.enableHiveSupport() \
.getOrCreate()
读取Hive表:
df = spark.table("db.table_name")
df = spark.sql("SELECT * FROM db.table_name WHERE dt='2024-01-01'")
写入Hive表:
df.write.mode("overwrite").saveAsTable("db.table_name")
df.write.insertInto("db.table_name", overwrite=True)
分区操作:
df.write.mode("overwrite").partitionBy("dt").saveAsTable("db.partition_table")
spark.sql("ALTER TABLE db.partition_table ADD IF NOT EXISTS PARTITION (dt='2024-01-01')")
注意:Spark默认覆盖整张表,如果只想覆盖特定分区,需要设置spark.sql.sources.partitionOverwriteMode=dynamic。
5.5 JDBC读写详解
读取MySQL:
df = spark.read.format("jdbc") \
.option("url", "jdbc:mysql://host:3306/db") \
.option("dbtable", "table_name") \
.option("user", "root") \
.option("password", "pwd") \
.option("driver", "com.mysql.cj.jdbc.Driver") \
.option("fetchsize", "1000") \
.option("numPartitions", "10") \
.option("partitionColumn", "id") \
.option("lowerBound", "1") \
.option("upperBound", "10000000") \
.load()
并行读取原理:Spark会根据partitionColumn、lowerBound、upperBound和numPartitions生成多个子查询,例如:
SELECT * FROM table WHERE id >= 1 AND id < 1000000
SELECT * FROM table WHERE id >= 1000000 AND id < 2000000
...
每个子查询在一个Executor上独立执行,提高读取速度。注意partitionColumn应是数值型、索引列,分布均匀。
写入MySQL:
df.write.format("jdbc") \
.option("url", "jdbc:mysql://host:3306/db") \
.option("dbtable", "table_name") \
.option("user", "root") \
.option("password", "pwd") \
.option("batchsize", "10000") \
.option("isolationLevel", "READ_COMMITTED") \
.mode("append") \
.save()
六、易错点避坑
6.1 CSV自动推断Schema开销大
对于大文件,inferSchema=true会额外读取一遍文件的前若干行来推断类型,可能导致性能下降。建议先通过printSchema确认,然后手动定义StructType。
6.2 JSON多行文件设置错误
如果JSON记录跨多行(常见于导出的缩进格式),必须设置multiline=true,否则每行会被当作一条独立记录解析失败。
6.3 Parquet分区写入后查询不读取分区列
写入时使用partitionBy,分区列会成为目录层级,但Spark SQL在读取时会自动识别。如果手动读取路径/path/dt=2024-01-01,分区列dt不会出现在Schema中,需要额外处理。应使用spark.read.parquet("/path"),Spark会自动发现分区。
6.4 Hive写入覆盖整表而非分区
默认saveAsTable + mode("overwrite")会覆盖整张表,丢失其他分区数据。解决方法:
- 使用
insertInto+overwrite=True,且要求目标表存在。 - 设置
spark.sql.sources.partitionOverwriteMode=dynamic,只覆盖写入的分区。
6.5 JDBC读取时分区键选择不当
如果partitionColumn选择不均匀(如时间戳列,最近的数据远多于早期),或存在大量NULL值,会导致数据倾斜。建议使用自增主键或均匀分布的数值列。
6.6 JDBC写入时频繁commit导致性能差
默认batchsize=1000,如果每条记录都commit,性能极差。适当增大batchsize(如10000),并确保数据库的rewriteBatchedStatements=true(MySQL)可大幅提升写入速度。
七、完整实战案例
本案例将模拟一个典型的ETL流程:
- 从MySQL的
orders表和users表读取业务数据(模拟每日增量) - 从Hive的
dim_product维度表读取商品信息 - 进行数据清洗和关联,计算每个用户的销售额和订单数
- 将结果写入Parquet格式的数据湖,按日期分区
- 同时将销售额前100的用户导出为JSON文件,用于前端展示
# ============== multi_datasource_demo.py ==============
# 功能:PySpark多数据源读写实战(CSV/JSON/Parquet/Hive/MySQL)
# 模拟环境:需要MySQL和Hive服务(或使用本地Derby测试)
# 为简化演示,MySQL和Hive部分使用内存模拟或本地文件替代,但展示完整代码
from pyspark.sql import SparkSession
from pyspark.sql.functions import col, sum as spark_sum, count, round, when, to_date, lit
from pyspark.sql.types import StructType, StructField, IntegerType, StringType, DoubleType, DateType
import pymysql
import os
import shutil
# ========== 1. 创建SparkSession(启用Hive支持) ==========
spark = SparkSession.builder \
.appName("MultiDataSourceDemo") \
.master("local[4]") \
.config("spark.sql.warehouse.dir", "/tmp/spark-warehouse") \
.config("spark.sql.catalogImplementation", "hive") \ # 启用Hive支持
.config("spark.sql.parquet.compression.codec", "snappy") \
.config("spark.sql.sources.partitionOverwriteMode", "dynamic") \
.enableHiveSupport() \
.getOrCreate()
sc = spark.sparkContext
sc.setLogLevel("WARN")
print("=" * 80)
print("PySpark 多数据源读写实战(CSV/JSON/Parquet/Hive/MySQL)")
print("=" * 80)
# ========== 2. 准备模拟数据(因环境限制,使用本地文件代替真实MySQL/Hive) ==========
# 实际生产中将替换为真实JDBC和Hive Metastore连接
print("\n准备模拟数据...")
# 模拟MySQL中的orders表数据
orders_data = [
(1, 101, 1500.0, "2024-01-01"), (2, 102, 2200.0, "2024-01-01"),
(3, 101, 800.0, "2024-01-02"), (4, 103, 3100.0, "2024-01-02"),
(5, 101, 900.0, "2024-01-03"), (6, 104, 1200.0, "2024-01-03")
]
orders_schema = StructType([
StructField("order_id", IntegerType()),
StructField("user_id", IntegerType()),
StructField("amount", DoubleType()),
StructField("order_date", DateType())
])
orders_df = spark.createDataFrame(orders_data, schema=orders_schema)
# 模拟MySQL中的users表
users_data = [(101, "Alice", "alice@example.com"), (102, "Bob", "bob@example.com"),
(103, "Charlie", "charlie@example.com"), (104, "David", "david@example.com")]
users_schema = StructType([
StructField("user_id", IntegerType()),
StructField("name", StringType()),
StructField("email", StringType())
])
users_df = spark.createDataFrame(users_data, schema=users_schema)
# 模拟Hive中的维度表 product
product_data = [(1, "Laptop", "Electronics"), (2, "Phone", "Electronics"), (3, "Book", "Media")]
product_schema = StructType([
StructField("product_id", IntegerType()),
StructField("product_name", StringType()),
StructField("category", StringType())
])
product_df = spark.createDataFrame(product_data, product_schema)
# 为了演示,将product_df写入Hive表
spark.sql("CREATE DATABASE IF NOT EXISTS demo")
product_df.write.mode("overwrite").saveAsTable("demo.dim_product")
print("Hive表 demo.dim_product 创建成功")
# 模拟MySQL的JDBC(实际应配置真实连接,这里展示代码,但不执行)
# jdbc_url = "jdbc:mysql://localhost:3306/retail"
# jdbc_properties = {"user": "spark", "password": "spark", "driver": "com.mysql.cj.jdbc.Driver"}
# ========== 3. 从不同数据源读取 ==========
print("\n" + "=" * 80)
print("步骤1: 读取多源数据")
print("=" * 80)
# 3.1 读取CSV文件(模拟外部日志)
csv_path = "/tmp/sample_logs.csv"
with open(csv_path, "w") as f:
f.write("user_id,action,timestamp\n101,login,2024-01-01 10:00:00\n102,view,2024-01-01 10:05:00")
log_df = spark.read.csv(csv_path, header=True, inferSchema=True)
print("CSV读取样例:")
log_df.show()
# 3.2 读取JSON文件(模拟API响应)
json_path = "/tmp/sample_config.json"
with open(json_path, "w") as f:
f.write('{"config_id": 1, "setting": "debug", "enabled": true}\n')
f.write('{"config_id": 2, "setting": "prod", "enabled": false}\n')
config_df = spark.read.json(json_path)
print("JSON读取样例:")
config_df.show()
# 3.3 读取Parquet文件(演示写入后再读)
parquet_temp = "/tmp/sample_parquet"
orders_df.write.mode("overwrite").parquet(parquet_temp)
parquet_df = spark.read.parquet(parquet_temp)
print("Parquet读取样例:")
parquet_df.show(2)
# 3.4 读取Hive表
hive_product_df = spark.table("demo.dim_product")
print("Hive表读取样例:")
hive_product_df.show()
# 3.5 读取JDBC(模拟代码,实际需配置)
# jdbc_orders = spark.read.format("jdbc").option("url", jdbc_url).option("dbtable", "orders").option("user", ...).load()
# 这里用已有orders_df代替
jdbc_orders = orders_df
# ========== 4. 数据清洗与转换 ==========
print("\n" + "=" * 80)
print("步骤2: 数据清洗与关联")
print("=" * 80)
# 清洗订单:过滤金额<=0的订单(示例无)
cleaned_orders = orders_df.filter(col("amount") > 0)
# 关联用户表
order_user_df = cleaned_orders.join(users_df, on="user_id", how="inner")
# 添加衍生列:订单年份月份
order_user_df = order_user_df.withColumn("year_month",
when(col("order_date").isNotNull(),
to_date(col("order_date")).substr(1, 7)).otherwise(lit("unknown")))
print("关联后数据:")
order_user_df.show()
# 计算用户级指标
user_stats = order_user_df.groupBy("user_id", "name", "email") \
.agg(
spark_sum("amount").alias("total_amount"),
count("order_id").alias("order_count")
) \
.withColumn("avg_order_amount", round(col("total_amount") / col("order_count"), 2))
print("用户统计结果:")
user_stats.show()
# 可继续关联Hive维度表(示例不做)
# ========== 5. 写入不同数据源 ==========
print("\n" + "=" * 80)
print("步骤3: 写入多数据源")
print("=" * 80)
# 5.1 写入CSV(演示单文件输出)
output_csv = "/tmp/output_csv"
user_stats.coalesce(1).write.mode("overwrite").csv(output_csv, header=True)
print(f"CSV写入完成: {output_csv}")
# 5.2 写入JSON(按分区输出)
output_json = "/tmp/output_json"
user_stats.write.mode("overwrite").json(output_json)
print(f"JSON写入完成: {output_json}")
# 5.3 写入Parquet(分区存储,按year_month分区,但当前没有该列,演示分区)
# 先将user_stats添加一个日期列用于演示分区
user_stats_with_date = user_stats.withColumn("stat_date", lit("2024-01-01"))
output_parquet = "/tmp/output_parquet"
user_stats_with_date.write.mode("overwrite") \
.partitionBy("stat_date") \
.parquet(output_parquet)
print(f"Parquet分区写入完成: {output_parquet}")
# 5.4 写入Hive表
user_stats.write.mode("overwrite").saveAsTable("demo.user_stats")
print("Hive表 demo.user_stats 写入完成")
# 验证Hive表
spark.sql("SELECT * FROM demo.user_stats").show()
# 5.5 写入JDBC(模拟代码,实际需配置)
# user_stats.write.format("jdbc").option("url", jdbc_url).option("dbtable", "user_stats_summary").mode("append").save()
# print("JDBC写入完成(模拟)")
# ========== 6. 高级特性演示 ==========
print("\n" + "=" * 80)
print("步骤4: 高级数据源特性")
print("=" * 80)
# 6.1 CSV高级选项:处理坏记录
csv_bad_path = "/tmp/bad_records.csv"
with open(csv_bad_path, "w") as f:
f.write("id,name\n1,Alice\n2,Bob,extra\nbad,line\n3,Charlie")
bad_df = spark.read.option("mode", "PERMISSIVE") \
.option("columnNameOfCorruptRecord", "_corrupt_record") \
.csv(csv_bad_path, header=True)
print("模式处理坏记录:")
bad_df.show(truncate=False)
bad_df.filter(col("_corrupt_record").isNotNull()).show()
# 6.2 Parquet Schema合并演示
parquet_path1 = "/tmp/parquet_merge/part1"
df1 = spark.createDataFrame([(1, "a")], ["id", "name"])
df1.write.parquet(parquet_path1)
parquet_path2 = "/tmp/parquet_merge/part2"
df2 = spark.createDataFrame([(2, "b", 100)], ["id", "name", "value"])
df2.write.parquet(parquet_path2)
# 关闭mergeSchema读取会失败
try:
merged = spark.read.parquet(parquet_path1, parquet_path2)
print("未开启mergeSchema,结果:")
merged.printSchema()
except Exception as e:
print("未开启mergeSchema报错:", e)
# 开启mergeSchema读取
spark.conf.set("spark.sql.parquet.mergeSchema", "true")
merged = spark.read.parquet(parquet_path1, parquet_path2)
print("开启mergeSchema后Schema:")
merged.printSchema()
merged.show()
spark.conf.set("spark.sql.parquet.mergeSchema", "false") # 恢复
# 6.3 JDBC并行读取示例(伪代码)
# 假设我们需要从MySQL读取大表orders,按order_id分区
# jdbc_options = {
# "url": jdbc_url,
# "dbtable": "orders",
# "partitionColumn": "order_id",
# "lowerBound": "1",
# "upperBound": "10000000",
# "numPartitions": "10",
# "fetchsize": "10000"
# }
# large_orders = spark.read.format("jdbc").options(**jdbc_options).load()
# ========== 7. 数据源选项汇总表 ==========
print("\n" + "=" * 80)
print("步骤5: 常用数据源选项参考")
print("=" * 80)
print("""
CSV: header, inferSchema, sep, quote, escape, nullValue, dateFormat, timestampFormat, mode
JSON: multiline, primitivesAsString, prefersDecimal, allowComments, dateFormat
Parquet: compression (snappy/gzip/lzo), mergeSchema, enableVectorizedReader
Hive: partitionOverwriteMode, hive.exec.dynamic.partition.mode
JDBC: url, dbtable, user, password, driver, fetchsize, batchsize, isolationLevel, numPartitions, partitionColumn, lowerBound, upperBound
""")
# ========== 8. 清理临时文件 ==========
print("\n清理临时数据...")
shutil.rmtree(csv_path, ignore_errors=True)
shutil.rmtree(json_path, ignore_errors=True)
shutil.rmtree(parquet_temp, ignore_errors=True)
shutil.rmtree(output_csv, ignore_errors=True)
shutil.rmtree(output_json, ignore_errors=True)
shutil.rmtree(output_parquet, ignore_errors=True)
shutil.rmtree("/tmp/parquet_merge", ignore_errors=True)
shutil.rmtree(csv_bad_path, ignore_errors=True)
# 注意:Hive表数据存储在/tmp/spark-warehouse,可手动清理
spark.stop()
print("\n✅ 多数据源读写实战完成")
八、代码逐行解析
8.1 启用Hive支持
.config("spark.sql.catalogImplementation", "hive") \
.enableHiveSupport()
这两行必须同时使用。enableHiveSupport()告诉Spark使用Hive的解析器和元数据服务。还需要配置hive.metastore.uris指向真实的Metastore服务,否则默认使用内嵌Derby。
8.2 CSV读取坏记录处理
.option("mode", "PERMISSIVE")
.option("columnNameOfCorruptRecord", "_corrupt_record")
当某行解析失败时,整行内容会被放入_corrupt_record列,其他列置为null。方便后续分析坏数据。
8.3 Parquet Schema合并
spark.conf.set("spark.sql.parquet.mergeSchema", "true")
当读取多个目录下的Parquet文件且Schema不完全相同时,开启此参数会自动合并所有出现过的列(缺失列填充null)。注意合并有性能开销。
8.4 分区写入
df.write.partitionBy("stat_date").parquet(path)
写入后目录结构为/path/stat_date=2024-01-01/part-...。查询时使用WHERE stat_date='2024-01-01'会自动进行分区剪枝。
8.5 JDBC并行读取
需提供分区列、范围及分区数。Spark会生成类似WHERE partitionColumn BETWEEN lowerBound AND (lowerBound+step)的多个查询,每个查询在一个Task中执行。
九、业务场景落地应用
9.1 场景一:数据湖增量ETL
每日将MySQL业务表增量数据同步到HDFS Parquet格式的数据湖。使用spark.read.jdbc按时间戳列增量读取,然后写入Parquet,按日期分区,压缩使用snappy。
9.2 场景二:Hive数仓与外部数据源关联
营销部门需要分析用户行为与CRM系统的会员等级(存储在MySQL)。使用Spark SQL直接关联Hive表与MySQL临时表(通过JDBC),避免数据导出。
9.3 场景三:日志文件格式转换
将历史遗留的大量CSV日志转换为Parquet,提升查询效率。使用spark.read.csv读取,直接df.write.parquet,可调整分区和压缩。
9.4 场景四:实时数据写入Hive
使用Structured Streaming从Kafka消费数据,经处理后通过foreachBatch写入Hive分区表,实现准实时数据仓库。
十、常见报错排查
10.1 AnalysisException: Table or view not found 读取Hive表
原因:未启用Hive支持,或数据库名/表名不正确,或Metastore连接失败。
解决:检查enableHiveSupport(),使用spark.catalog.listTables()查看表是否存在。
10.2 Caused by: java.sql.SQLException: No suitable driver found JDBC
原因:缺少JDBC驱动jar包。
解决:将驱动jar放入Spark的jars目录,或使用--jars参数提交,或在代码中通过spark.sparkContext.addJar()动态添加。
10.3 写入JDBC时出现Batch update returned unexpected row count
原因:表有触发器或约束导致实际更新行数与预期不符。
解决:设置.option("truncate", "false")或调整batchsize。
10.4 Parquet读取时出现Parquet column cannot be converted 类型不兼容
原因:Schema演化不兼容,例如int类型变为string。
解决:在读取时指定spark.sql.parquet.int96AsTimestamp等兼容参数,或重建数据。
10.5 Hive写入覆盖动态分区时丢失其他分区
原因:默认partitionOverwriteMode=static,会覆盖整表。
解决:设置spark.sql.sources.partitionOverwriteMode=dynamic。
十一、本节课知识点总结
数据源对比矩阵
| 数据源 | 读速度 | 写速度 | 压缩率 | Schema支持 | 适用场景 |
|---|---|---|---|---|---|
| CSV | 慢 | 中等 | 无/需外部 | 弱(需推断) | 临时数据交换 |
| JSON | 慢 | 慢 | 无 | 中等 | API日志、调试 |
| Parquet | 快 | 中等 | 高 | 强(合并、演化) | 分析、存储 |
| Hive | 快 | 中等 | 高 | 强(Metastore) | 数仓、SQL分析 |
| JDBC | 中等 | 慢(写入) | 无 | 强 | 业务系统交互 |
最佳实践推荐
- 优先使用Parquet:分析型场景首选,性能好,压缩高。
- CSV/JSON用于导入导出:对外数据交换使用,注意Schema显式定义。
- Hive集成:利用现有数仓资产,但注意动态分区覆盖设置。
- JDBC读取:合理设置分区列和并行度,避免锁表;写入使用批量提交。
关键参数速记
- CSV/JSON:
mode控制坏数据处理。 - Parquet:
compression选择snappy平衡速度与大小。 - Hive:
partitionOverwriteMode=dynamic覆盖分区。 - JDBC:
fetchsize控制读取批次,batchsize控制写入批次。
十二、课后思考作业
作业一:理论理解题
-
为什么Parquet格式比CSV更适合大数据分析?请从存储、读取、压缩三个角度说明。
-
Spark JDBC读取时,如何实现并行读取?分区列的选择需要满足什么条件?
-
解释Hive的动态分区插入和静态分区插入的区别,以及在Spark中如何配置动态分区覆盖。
作业二:代码实践题
-
编写一个PySpark程序,将本地CSV文件(含header,100万行)转换为Parquet格式,并压缩为snappy。要求:
- 读取时显式定义Schema
- 写入时按
date列分区(模拟一个日期列) - 最后读取Parquet验证数据量
-
模拟JDBC读取:创建MySQL表
test_table(id int, name varchar(100), value double),插入10000条随机数据。使用Spark JDBC读取,设置numPartitions=4,分区列为id,观察Spark UI中Task数量。 -
使用Spark读写Hive表:创建一个外部表,指向Parquet格式的目录,然后通过Spark SQL进行查询和插入。演示动态分区插入。
作业三:场景应用题
某电商公司每日产生如下数据:
- MySQL
orders表(订单),每天增量约500万条,保留最近3个月数据,主键order_id,有索引。 - Hive
ods_user_behavior表(用户点击行为),按天分区,Parquet格式。 - 需要每日计算每个用户的订单转化率(下单用户/活跃用户),输出到Hive
ads_user_conversion,按天分区。
请设计PySpark ETL方案,包括:
- 如何高效读取MySQL增量数据(避免全表扫描)
- 如何优化读取Hive大表(分区剪枝)
- 写出核心代码,包括数据读取、关联、聚合、写入Hive
- 说明如何配置Spark以支持Hive动态分区写入
作业四:拓展研究
-
研究Parquet的谓词下推原理:如何通过Row Group的统计信息跳过无关数据块?阅读Parquet官方文档。
-
调研JDBC写入时的
isolationLevel参数对性能和数据一致性的影响。 -
尝试使用
spark.read.format("jdbc").option("fetchsize", ...)读取大表,对比不同fetchsize下的性能表现,给出推荐值。
提交方式:本次作业要求提交代码和运行结果截图,以及理论题的答案。鼓励在实际集群或Docker环境中测试JDBC和Hive部分。
扩展阅读:
- Spark官方文档:Data Sources
- 《Hive编程指南》第11章(Hive与Spark集成)
- MySQL官方文档:JDBC驱动配置
通过本节课的学习,你已经掌握了PySpark读写多种数据源的核心技能。在企业数据开发中,灵活运用这些数据源API能够构建高效的数据管道。下一节课我们将学习PySpark离线数仓分层建模、开发流程与企业业务项目实战,带你从零开始构建一个完整的大数据项目。我们下节课见!
🔗《20节课 PySpark 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

370

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



