第17课:PySpark|离线数仓分层建模、开发流程与企业业务项目实战

在这里插入图片描述


一、课前导读

经过前面16节课的学习,你已经掌握了PySpark的核心编程模型、SQL查询、内置函数、数据源读写以及性能调优的基础知识。但是,这些零散的知识点如何串联起来,构建一个完整的企业级数据仓库项目?在实际工作中,数据仓库不是简单的数据堆砌,而是需要合理的分层设计、规范的开发流程、可靠的调度依赖以及严格的数据质量保障。

很多初学者掌握了Spark的技术细节,却在面对真实业务场景时无从下手:应该先处理哪张表?ODS层、DWD层、DWS层分别做什么?如何设计ETL脚本的代码结构?多个任务之间的依赖关系如何管理?如何保证数据一致性?这些都是数仓开发中的核心问题。

本节课将带你完整地走一遍PySpark离线数仓项目的开发流程。我们会从一个典型的电商业务需求出发,按照数仓分层建模的原则,设计ODS(操作数据存储层)、DWD(明细数据仓库层)、DWS(数据仓库服务层,轻度汇总)、ADS(应用数据服务层)四层模型。然后,针对每一层编写PySpark ETL脚本,演示数据抽取、清洗、转换、加载的全过程。同时,我们将讨论调度系统(如Airflow)下的任务依赖设计、数据质量校验、性能优化等实战要点。

学完这节课,你将具备独立设计和开发PySpark离线数仓项目的能力,能够快速适应企业大数据开发岗位的要求。

二、学习目标

完成本节课的学习后,你将能够:

  1. 理解数仓分层架构:说清楚ODS、DWD、DWS、ADS每层的职责和分层原则
  2. 掌握数仓建模方法:了解维度建模的事实表、维度表设计,以及星型模型、雪花模型
  3. 设计PySpark ETL流程:按照数仓分层组织代码,实现数据抽取、清洗、转换、加载
  4. 处理缓慢变化维度(SCD):使用拉链表处理维度变化
  5. 构建数据质量校验:在ETL过程中添加空值检查、唯一性校验、业务规则校验
  6. 设计任务依赖:理解不同层级的依赖关系,配合调度系统设置上下游
  7. 优化数仓性能:分区设计、分桶、文件格式选择、数据倾斜处理
  8. 完成企业级实战项目:从零实现一个电商数仓项目(用户行为、订单、商品分析)

三、核心理论知识点

知识点说明
数仓分层ODS(原始数据)、DWD(明细数据)、DWS(轻度汇总)、ADS(应用数据)
维度建模事实表(度量) + 维度表(描述环境),星型模型、雪花模型
缓慢变化维度(SCD)处理维度属性随时间变化的策略,常用SCD Type2(拉链表)
ETL开发流程数据探查 → 清洗规则定义 → 转换逻辑 → 加载策略
分区设计按日期分区,动态分区写入,避免小文件
数据质量空值检查、唯一键检查、枚举值检查、数据量波动监控
任务依赖基于时间调度(如每天凌晨)与基于数据依赖(等待上游表就绪)
数仓工具栈Spark + Hive(元数据)+ HDFS/Parquet + 调度系统(Airflow/DolphinScheduler)

四、原理通俗讲解

4.1 为什么要分层?

想象一下,你是一个大仓库的管理员,如果没有货架和分类,所有货物随意堆砌,找一件商品会非常困难。数仓分层就像是给数据仓库设计了货架系统:

  • ODS层:收货区。货物(原始数据)到达后,原封不动地放在这里,保留原始格式。
  • DWD层:加工区。将货物拆箱、检验、贴标,去掉破损的,修正错误的,整理成标准格式。
  • DWS层:分类货架。按商品类别、按品牌汇总,统计每个货架的库存量,方便快速查询。
  • ADS层:取货窗口。根据销售员的需求,将特定商品组合打包,直接拿到前台销售。

分层带来的好处:

  • 解耦:当上游源系统变化时,只需要调整ODS→DWD的逻辑,不影响下游。
  • 复用:DWD层的明细数据可以被多个DWS层的指标复用。
  • 维护:每层职责清晰,出现问题可以快速定位。
  • 效率:ADS层可以专门优化查询,避免复杂关联。

4.2 维度建模的核心概念

  • 事实表:记录业务过程的事件,如“用户下单”事实包含订单ID、商品ID、金额、时间等。事实表通常是稀疏的,数据量巨大,每天增量。
  • 维度表:描述业务的实体属性,如商品维度(商品ID、名称、品类、价格)、用户维度(用户ID、年龄、注册时间)。维度表相对稳定,数据量较小。
  • 星型模型:一个事实表连接多个维度表,形如星星。这种模型查询性能好,是数仓的主流。

4.3 ETL设计原则

  • 增量 vs 全量:ODS层通常按分区存储(如每天一个分区),DWD层可考虑拉链表。
  • 分批处理:使用spark.sql.shuffle.partitions控制并行度,避免小文件。
  • 数据质量:在ETL过程中校验(如过滤空值),并在写入前进行计数校验。
  • 幂等性:ETL任务应支持重跑而不产生重复数据(写入使用覆盖或按分区覆盖)。

五、重点概念拆解

5.1 数仓分层详细设计

命名规范表类型存储格式分区方式更新策略
ODSods_业务系统_表名外部表原格式(Text/JSON)或Parquetdt(日期)全量或增量覆盖
DWDdwd_主题_表名内部表Parquet + Snappydt增量追加或拉链表
DWSdws_主题_统计粒度内部表Parquetdt分区覆盖
ADSads_应用名内部表Parquet按需分区覆盖或全量

5.2 缓慢变化维度(SCD)处理

以用户维度为例,用户的地址可能会变化。如果直接更新维度表中的地址,历史订单关联的用户地址也会改变,导致统计失真。通常使用SCD Type2:保留历史记录,添加start_dateend_dateis_current字段。

# 拉链表更新逻辑示例(伪代码)
new_dim = (新增用户 + 变化用户标记失效)(未变化用户)

简化:每天全量刷新维度表(如果维度数据量不大),按最新数据覆盖。

5.3 数据质量校验

在ETL脚本最后,添加校验代码:

  • 记录数校验:assert df.count() > 0
  • 主键唯一性:df.groupBy("key").count().filter("count > 1").count() == 0
  • 重要字段空值率:df.filter(col("key").isNull()).count() < threshold

5.4 任务依赖设计

典型的每日调度依赖:

ODS层任务(多个源表并行) → DWD层(等待ODS分区就绪) → DWS层(等待DWD分区就绪) → ADS层

使用Airflow的ExternalTaskSensorSparkSubmitOperatordepends_on_past控制。

六、易错点避坑

6.1 分区字段未裁剪导致全表扫描

在读取ODS层时,必须加上分区过滤:spark.sql("SELECT * FROM ods_orders WHERE dt='2024-01-01'"),否则会读取所有分区,导致性能下降。

6.2 小文件问题

在DWD层写入时,如果每个分区内文件数量过多(数百个),会影响下游读取效率和NameNode内存。解决方案:

  • 写入前使用repartition(num_partitions)控制分区数。
  • 或使用coalesce减少文件数。
  • 设置spark.sql.shuffle.partitions为合适值(如200)。

6.3 动态分区写入产生大量小文件

df.write.partitionBy("dt").parquet(...) 会根据dt列的值生成目录,但如果df中每个分区的数据量不均匀,可能导致某些分区只有极少量数据,产生小文件。可先repartition按dt列重分区:df.repartition(col("dt")).write.partitionBy("dt")...

6.4 数据倾斜导致任务失败

在DWS层按商品维度聚合时,某些热门商品可能导致倾斜。使用加盐或spark.sql.adaptive.skewJoin.enabled=true处理。

七、完整实战案例:电商数仓项目

本案例模拟一个电商平台,业务数据包含:用户表、订单表、订单明细表、商品表。我们将构建从ODS到ADS的完整数仓,实现如下指标:

  1. 每日订单总额、订单数、支付用户数(ADS层)
  2. 商品类目销售额排行(DWS层)
  3. 用户等级购买力分析(DWD层关联用户维度)

所有数据使用PySpark处理,写入Hive表。

7.1 环境准备与数据模拟

# ============== ecommerce_warehouse.py ==============
# 功能:PySpark离线数仓分层建模与企业项目实战
# 模拟电商数据并完成ODS→DWD→DWS→ADS全流程

from pyspark.sql import SparkSession
from pyspark.sql.functions import *
from pyspark.sql.types import *
from pyspark.sql.window import Window
import random
from datetime import datetime, timedelta
import os
import shutil

# 创建SparkSession,启用Hive支持
spark = SparkSession.builder \
    .appName("EcommerceWarehouse") \
    .master("local[4]") \
    .config("spark.sql.warehouse.dir", "/tmp/hive_warehouse") \
    .config("spark.sql.catalogImplementation", "hive") \
    .config("spark.sql.adaptive.enabled", "true") \
    .config("spark.sql.sources.partitionOverwriteMode", "dynamic") \
    .enableHiveSupport() \
    .getOrCreate()

sc = spark.sparkContext
sc.setLogLevel("WARN")

print("=" * 80)
print("PySpark 离线数仓分层建模与企业项目实战")
print("=" * 80)

# 创建数据库
spark.sql("CREATE DATABASE IF NOT EXISTS ecommerce")
spark.sql("USE ecommerce")

# ========== 模拟源数据(实际应从业务系统抽取) ==========
# 为简化,在代码中生成模拟数据,并写入ODS层
print("生成模拟源数据...")

# 商品维度(稳定)
products = [(i, f"product_{i}", random.choice(["电子", "服装", "食品", "家居"]), round(random.uniform(10, 2000), 2))
            for i in range(1, 101)]
product_schema = StructType([
    StructField("product_id", IntegerType()),
    StructField("product_name", StringType()),
    StructField("category", StringType()),
    StructField("price", DoubleType())
])
product_df = spark.createDataFrame(products, product_schema)

# 用户维度(稳定)
users = [(i, f"user_{i}", random.randint(18, 60), random.choice(["普通会员", "黄金会员", "钻石会员"]))
         for i in range(1, 1001)]
user_schema = StructType([
    StructField("user_id", IntegerType()),
    StructField("user_name", StringType()),
    StructField("age", IntegerType()),
    StructField("vip_level", StringType())
])
user_df = spark.createDataFrame(users, user_schema)

# 订单主表(每天增量)
orders_data = []
order_details_data = []
start_date = datetime(2024, 1, 1)
order_id = 1
for day in range(30):
    cur_date = start_date + timedelta(days=day)
    dt = cur_date.strftime("%Y-%m-%d")
    # 每天1000-3000订单
    daily_orders = random.randint(1000, 3000)
    for _ in range(daily_orders):
        user_id = random.randint(1, 1000)
        order_amount = round(random.uniform(20, 5000), 2)
        status = random.choice(["paid", "cancelled"])
        orders_data.append((order_id, user_id, order_amount, status, cur_date, dt))
        # 每个订单1-5个明细
        detail_num = random.randint(1, 5)
        for __ in range(detail_num):
            product_id = random.randint(1, 100)
            quantity = random.randint(1, 10)
            price = round(random.uniform(10, 2000), 2)  # 实际应从商品表取,这里简化
            order_details_data.append((order_id, product_id, quantity, price, dt))
        order_id += 1

orders_schema = StructType([
    StructField("order_id", IntegerType()),
    StructField("user_id", IntegerType()),
    StructField("amount", DoubleType()),
    StructField("status", StringType()),
    StructField("order_date", DateType()),
    StructField("dt", StringType())
])
order_df = spark.createDataFrame(orders_data, orders_schema)

order_detail_schema = StructType([
    StructField("order_id", IntegerType()),
    StructField("product_id", IntegerType()),
    StructField("quantity", IntegerType()),
    StructField("price", DoubleType()),
    StructField("dt", StringType())
])
order_detail_df = spark.createDataFrame(order_details_data, order_detail_schema)

# 写入ODS层(按dt分区)
print("写入ODS层...")
order_df.write.mode("overwrite").partitionBy("dt").saveAsTable("ods_orders")
order_detail_df.write.mode("overwrite").partitionBy("dt").saveAsTable("ods_order_detail")
product_df.write.mode("overwrite").saveAsTable("ods_product")
user_df.write.mode("overwrite").saveAsTable("ods_user")

print("ODS层表创建完成:")
spark.sql("SHOW TABLES IN ecommerce").show(10)

# ========== ODS → DWD 数据清洗 ==========
print("\n" + "=" * 80)
print("1. ODS层 → DWD层:数据清洗与明细层构建")
print("=" * 80)

# 定义清洗函数,处理空值、异常值、格式转换
def clean_orders(spark, dt):
    df = spark.table("ods_orders").filter(col("dt") == dt)
    # 过滤金额<=0或状态异常的订单
    cleaned = df.filter((col("amount") > 0) & (col("status").isin("paid", "cancelled")))
    # 添加衍生列:订单年份、月份、日期
    cleaned = cleaned.withColumn("order_year", year(col("order_date"))) \
                     .withColumn("order_month", month(col("order_date"))) \
                     .withColumn("order_day", dayofmonth(col("order_date")))
    return cleaned

def clean_order_detail(spark, dt):
    df = spark.table("ods_order_detail").filter(col("dt") == dt)
    # 过滤数量<=0
    cleaned = df.filter(col("quantity") > 0)
    # 计算明细金额
    cleaned = cleaned.withColumn("detail_amount", round(col("quantity") * col("price"), 2))
    return cleaned

def clean_user(spark):
    # 用户维度:处理空值、统一vip_level
    df = spark.table("ods_user")
    df = df.fillna({"age": 0, "vip_level": "普通会员"})
    # 去重(取最新,但用户表通常全量)
    df = df.dropDuplicates(["user_id"])
    return df

def clean_product(spark):
    df = spark.table("ods_product")
    df = df.fillna({"category": "其他"})
    return df

# 按天处理(示例处理最近一天的数据)
latest_dt = spark.sql("SELECT MAX(dt) FROM ods_orders").collect()[0][0]
print(f"处理日期: {latest_dt}")

dwd_orders = clean_orders(spark, latest_dt)
dwd_order_detail = clean_order_detail(spark, latest_dt)
dwd_user = clean_user(spark)
dwd_product = clean_product(spark)

# 写入DWD层(订单和订单明细按dt分区,用户和商品全量拉取)
dwd_orders.write.mode("overwrite").partitionBy("dt").saveAsTable("dwd_orders")
dwd_order_detail.write.mode("overwrite").partitionBy("dt").saveAsTable("dwd_order_detail")
dwd_user.write.mode("overwrite").saveAsTable("dwd_user")
dwd_product.write.mode("overwrite").saveAsTable("dwd_product")

print("DWD层表创建完成")

# ========== DWD → DWS 轻度汇总 ==========
print("\n" + "=" * 80)
print("2. DWD层 → DWS层:商品类目日销售汇总")
print("=" * 80)

# 统计每日每个类目的销售额、订单数、购买用户数
# 关联订单、订单明细、商品
dws_category_sales = spark.sql(f"""
    SELECT 
        o.dt,
        p.category,
        COUNT(DISTINCT o.order_id) as order_cnt,
        COUNT(DISTINCT o.user_id) as user_cnt,
        SUM(d.detail_amount) as total_sales
    FROM dwd_orders o 
    JOIN dwd_order_detail d ON o.order_id = d.order_id AND o.dt = d.dt
    JOIN dwd_product p ON d.product_id = p.product_id
    WHERE o.status = 'paid'
    AND o.dt = '{latest_dt}'
    GROUP BY o.dt, p.category
""")

# 写入DWS层(按dt分区覆盖)
dws_category_sales.write.mode("overwrite").partitionBy("dt").saveAsTable("dws_category_sales")

print("DWS层表创建完成")
dws_category_sales.show()

# 也可按用户等级统计
dws_user_level_sales = spark.sql(f"""
    SELECT 
        o.dt,
        u.vip_level,
        COUNT(DISTINCT o.user_id) as paying_users,
        SUM(o.amount) as total_amount
    FROM dwd_orders o
    JOIN dwd_user u ON o.user_id = u.user_id
    WHERE o.status = 'paid' AND o.dt = '{latest_dt}'
    GROUP BY o.dt, u.vip_level
""")
dws_user_level_sales.write.mode("overwrite").partitionBy("dt").saveAsTable("dws_user_level_sales")

# ========== DWS → ADS 应用层 ==========
print("\n" + "=" * 80)
print("3. DWS层 → ADS层:指标集市")
print("=" * 80)

# 每日核心指标报表
ads_daily_metrics = spark.sql(f"""
    SELECT 
        dt,
        SUM(total_sales) as gmv,
        SUM(order_cnt) as total_orders,
        SUM(user_cnt) as total_buyers
    FROM dws_category_sales
    WHERE dt = '{latest_dt}'
    GROUP BY dt
""")

# 销售额Top3品类
ads_top_categories = spark.sql(f"""
    SELECT dt, category, total_sales, order_cnt
    FROM dws_category_sales
    WHERE dt = '{latest_dt}'
    ORDER BY total_sales DESC
    LIMIT 3
""")

# 写入ADS层(通常ADS层表可以不用分区或按日期分区)
ads_daily_metrics.write.mode("overwrite").saveAsTable("ads_daily_metrics")
ads_top_categories.write.mode("overwrite").saveAsTable("ads_top_categories")

print("ADS层表创建完成")
print("每日核心指标:")
ads_daily_metrics.show()
print("Top3品类:")
ads_top_categories.show()

# ========== 数据质量校验 ==========
print("\n" + "=" * 80)
print("4. 数据质量校验")
print("=" * 80)

def check_non_empty(table_name, dt=None):
    query = f"SELECT COUNT(*) FROM {table_name}"
    if dt:
        query += f" WHERE dt = '{dt}'"
    count = spark.sql(query).collect()[0][0]
    assert count > 0, f"{table_name} 数据为空!"
    print(f"✓ {table_name} 记录数: {count}")

def check_primary_key(table_name, key_col, dt=None):
    query = f"SELECT {key_col}, COUNT(*) as cnt FROM {table_name}"
    if dt:
        query += f" WHERE dt = '{dt}'"
    query += f" GROUP BY {key_col} HAVING cnt > 1"
    dup = spark.sql(query).count()
    assert dup == 0, f"{table_name} 主键 {key_col} 存在重复!"
    print(f"✓ {table_name} 主键唯一性校验通过")

check_non_empty("dwd_orders", latest_dt)
check_non_empty("dws_category_sales", latest_dt)
check_primary_key("dwd_orders", "order_id", latest_dt)
print("所有数据质量校验通过")

# ========== 性能优化示例:小文件合并 ==========
print("\n" + "=" * 80)
print("5. 性能优化:小文件合并")
print("=" * 80)

# 模拟读取某分区数据后,重分区减少输出文件数
def compact_partition(table_name, partition_col, partition_value, target_partitions=4):
    df = spark.table(table_name).filter(col(partition_col) == partition_value)
    # 重新分区
    df_repart = df.coalesce(target_partitions)
    # 临时写入新目录
    temp_dir = f"/tmp/compact_{table_name}_{partition_value}"
    df_repart.write.mode("overwrite").parquet(temp_dir)
    # 覆盖原表分区(Hive表需手动修复,此处简化)
    spark.sql(f"ALTER TABLE {table_name} DROP IF EXISTS PARTITION ({partition_col}='{partition_value}')")
    df_repart.write.mode("append").insertInto(table_name)
    print(f"表 {table_name} 分区 {partition_col}={partition_value} 已合并为 {target_partitions} 个文件")

# 示例:合并dwd_orders某个分区(视情况执行)
# compact_partition("dwd_orders", "dt", latest_dt, 4)

# ========== 调度依赖设计说明 ==========
print("\n" + "=" * 80)
print("6. 调度依赖设计(以Airflow为例)")
print("=" * 80)
print("""
任务依赖DAG:
ODS_orders (Spark)  --> DWD_orders (Spark) --> DWS_category_sales --> ADS_daily_metrics
ODS_order_detail  --> DWD_order_detail --|
ODS_product --> DWD_product (全量,无依赖)
ODS_user --> DWD_user

在Airflow中可以使用ExternalTaskSensor等待上游分区就绪。
每个Spark任务可封装为SparkSubmitOperator。
""")

# ========== 清理资源 ==========
print("\n清理临时文件...")
# 注意:Hive warehouse数据保留,仅演示环境清理
spark.stop()
print("✅ 电商数仓项目实战完成")

八、代码逐行解析

8.1 ODS层写入

order_df.write.mode("overwrite").partitionBy("dt").saveAsTable("ods_orders")
  • 使用saveAsTable直接创建Hive表,元数据保存在Hive Metastore中。
  • partitionBy("dt")按日期分区,方便增量处理。

8.2 DWD层清洗逻辑

cleaned = df.filter((col("amount") > 0) & (col("status").isin("paid", "cancelled")))
  • 过滤无效数据。
  • 添加衍生列:年月日,便于后续统计。

8.3 DWS层轻度汇总

使用Spark SQL进行多表关联聚合,比DataFrame DSL更简洁。注意使用分区裁剪:WHERE o.dt = '{latest_dt}'

8.4 数据质量校验

  • check_non_empty:确保ETL执行后数据非空。
  • check_primary_key:检查主键唯一性(如订单ID)。

8.5 小文件合并

df_repart = df.coalesce(target_partitions)

减少输出文件数,避免小文件过多。

九、业务场景落地应用

9.1 增量ETL与全量ETL的选择

  • 事实表(订单、日志)通常按天增量处理。
  • 维度表(用户、商品)如果每天全量覆盖,资源消耗大,可采用拉链表。

9.2 数据回溯重跑

当上游数据错误需要重跑某天数据时,应设计幂等任务:使用INSERT OVERWRITE分区覆盖,而非INSERT INTO追加。

9.3 数据质量监控

可在ETL任务完成后,将质量指标(记录数、空值率)写入监控系统(如MySQL),通过Grafana展示。

十、常见报错排查

10.1 Table already exists 当使用saveAsTable

解决:使用mode("overwrite")覆盖表。

10.2 Partition spec {dt=xxx} already exists 动态分区写入

解决:设置spark.sql.sources.partitionOverwriteMode=dynamic,并确保写入时指定分区列。

10.3 读取Hive表时权限不足

解决:检查HDFS目录权限,或配置spark.sql.warehouse.dir为有写权限的目录。

10.4 数据倾斜导致某个Task运行极慢

解决:在DWS层聚合时,使用spark.sql.adaptive.skewJoin.enabled=true,或手动加盐。

十一、本节课知识点总结

数仓分层职责速记

核心操作存储粒度示例表
ODS原始数据同步日志/增量ods_orders
DWD清洗、转换、维度退化明细dwd_orders
DWS轻度汇总(按维度)日汇总dws_category_sales
ADS指标加工,面向应用汇总/报表ads_daily_metrics

ETL开发流程

  1. 数据探查(了解源数据质量)
  2. 设计清洗规则(空值、异常值、去重)
  3. 实现转换逻辑(关联、聚合、窗口)
  4. 数据加载(分区覆盖/追加)
  5. 数据质量校验
  6. 调度配置

性能优化要点

  • 分区剪枝:查询时务必带上分区字段
  • 小文件合并:写入前coalescerepartition
  • 谓词下推:尽可能在SQL中加过滤条件
  • 广播小表:维度表小于100MB时使用/*+ BROADCAST */

十二、课后思考作业

作业一:理论理解题

  1. 请阐述数仓中为什么需要DWD层?DWD层与ODS层的核心区别是什么?

  2. 缓慢变化维度(SCD)Type2的实现原理是什么?在PySpark中如何实现拉链表更新?

  3. 动态分区覆盖与静态分区覆盖的区别,以及各自的使用场景。

作业二:代码实践题

  1. 基于本课示例,扩展一个DWS表:按用户年龄分段(20以下、20-30、30-40、40以上)统计每日消费总额。

  2. 编写一个通用的PySpark函数,给定目标表名、分区列、期望记录数阈值,自动进行小文件合并。

  3. 实现一个数据质量校验函数,检查DWD层订单明细表与订单主表的金额一致性(订单主表的amount应与关联的明细金额之和相等)。

作业三:场景应用题

某物流公司每天产生数百万条运输记录,数据源为Kafka(实时)和每日全量MySQL快照。需要构建一个离线数仓,支持以下分析:

  • 每日每个仓库的发货单量、平均运输时长
  • 每个司机的运输里程、超时次数
  • 按周统计各区域订单量趋势

请设计数仓分层(ODS、DWD、DWS、ADS),写出核心字段和每层的处理逻辑,并给出关键的PySpark ETL伪代码。

作业四:拓展研究

  1. 研究Apache Hudi或Delta Lake等数据湖技术,了解它们如何解决数据更新和增量查询问题,并与传统Hive数仓对比。

  2. 学习一种调度系统(如Airflow),搭建简单的Spark任务DAG,实现本课数仓项目的自动化调度。

  3. 设计一套数据质量校验框架,支持表级别(记录数、唯一键)、字段级别(空值率、枚举值分布),并输出HTML报告。


提交方式:本次作业要求提交代码和设计文档,鼓励将整个数仓项目打包为一个Git仓库,包含ETL脚本、SQL定义、调度配置。

扩展阅读

  • 《数据仓库工具箱》——Kimball维度建模权威指南
  • Spark官方文档:Hive Tables
  • 《大数据之路:阿里巴巴大数据实践》

通过本节课的学习,你已经掌握了用PySpark构建企业级离线数仓的全套流程。从分层设计到ETL开发,再到性能优化和质量保障,你具备了独立承担大数据仓库开发任务的能力。后续可以根据业务需求,进一步学习实时数仓、数据湖等更高级的内容。我们下节课将进入Spark Streaming实时流计算的学习,敬请期待!


🔗《20节课 PySpark 从入门到精通》系列课程导航

去订阅

🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas.Sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值