【架构实战】数据仓库分层架构(ODS/DWD/DWS/ADS)

一、数据仓库分层概述

数据仓库分层是数据架构的核心设计,合理的分层能:

  • 降低复杂度:逐层处理,减少依赖
  • 提高复用性:中间层可供多个应用使用
  • 便于数据溯源:问题定位更简单
  • 隔离变化:上游变化不影响下游

二、分层架构

1. 经典四层架构

复制代码
┌─────────────────────────────────────────────────┐
│                   数据源层(ODS)                │
│         Operational Data Store - 原始数据层     │
└─────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────┐
│                数据明细层(DWD)                 │
│      Data Warehouse Detail - 明细事实表          │
└─────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────┐
│                数据汇总层(DWS)                 │
│       Data Warehouse Summary - 汇总宽表         │
└─────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────┐
│                数据应用层(ADS)                 │
│         Application Data Store - 应用数据层     │
└─────────────────────────────────────────────────┘

2. 各层职责

层级 全称 职责 特点
ODS 操作数据存储 原始数据,保留历史 数据原样,不做转换
DWD 数据明细层 清洗、标准化、一致性 原子指标,明细事实
DWS 数据汇总层 轻度汇总,主题宽表 通用汇总,应用复用
ADS 数据应用层 业务定制,报表数据 最终结果,面向应用

三、ODS层(原始数据层)

1. 特点

  • 数据原样存储:不做任何清洗转换
  • 保留历史变更:如拉链表、CDC数据
  • 多数据源整合:MySQL、Kafka、埋点日志等
  • 数据可回溯:出现问题可重新计算

2. 建表语句

sql 复制代码
-- ODS层订单表
CREATE TABLE ods_order (
    id BIGINT COMMENT '订单ID',
    order_no STRING COMMENT '订单号',
    user_id BIGINT COMMENT '用户ID',
    shop_id BIGINT COMMENT '商家ID',
    order_status INT COMMENT '订单状态',
    pay_amount DECIMAL(12,2) COMMENT '支付金额',
    order_time TIMESTAMP COMMENT '下单时间',
    pay_time TIMESTAMP COMMENT '支付时间',
    source_type STRING COMMENT '来源类型',
    _source_table STRING COMMENT '源表名',
    _load_time TIMESTAMP COMMENT '数据加载时间',
    _update_time TIMESTAMP COMMENT '数据更新时间'
) COMMENT 'ODS层订单表'
PARTITIONED BY (dt STRING)
STORED AS PARQUET
LOCATION '/warehouse/ods/order';

-- ODS层埋点日志表
CREATE TABLE ods_app_event (
    event_id STRING COMMENT '事件ID',
    event_name STRING COMMENT '事件名称',
    user_id STRING COMMENT '用户ID',
    device_id STRING COMMENT '设备ID',
    platform STRING COMMENT '平台',
    app_version STRING COMMENT 'App版本',
    event_time TIMESTAMP COMMENT '事件时间',
    properties STRING COMMENT '事件属性JSON',
    _load_time TIMESTAMP COMMENT '加载时间'
) COMMENT 'ODS层App事件表'
PARTITIONED BY (dt STRING)
STORED AS PARQUET
LOCATION '/warehouse/ods/app_event';

3. 数据加载

python 复制代码
# 使用Spark加载ODS数据
def load_ods_data():
    spark = SparkSession.builder.getOrCreate()
    
    # 从MySQL加载订单数据
    orders_df = spark.read \
        .format("jdbc") \
        .option("url", "jdbc:mysql://mysql:3306/order_db") \
        .option("dbtable", "orders") \
        .option("user", "root") \
        .option("password", "password") \
        .load()
    
    # 添加元数据字段
    orders_df = orders_df \
        .withColumn("_source_table", lit("orders")) \
        .withColumn("_load_time", current_timestamp()) \
        .withColumn("_update_time", current_timestamp())
    
    # 写入ODS层
    orders_df.write \
        .format("parquet") \
        .mode("overwrite") \
        .partitionBy("dt") \
        .saveAsTable("ods_order")

四、DWD层(明细数据层)

1. 特点

  • 数据清洗:去除脏数据、异常值
  • 标准化:字段命名、单位统一
  • 一致性:维度数据一致性处理
  • 拉链存储:保留历史变更

2. 建表语句

sql 复制代码
-- DWD层订单明细事实表
CREATE TABLE dwd_order_detail (
    order_id BIGINT COMMENT '订单ID',
    order_no STRING COMMENT '订单号',
    user_id BIGINT COMMENT '用户ID',
    user_name STRING COMMENT '用户姓名',
    user_phone STRING COMMENT '用户手机',
    shop_id BIGINT COMMENT '商家ID',
    shop_name STRING COMMENT '商家名称',
    category_id BIGINT COMMENT '商家品类ID',
    category_name STRING COMMENT '商家品类名称',
    order_status STRING COMMENT '订单状态',
    order_status_name STRING COMMENT '订单状态名称',
    order_amount DECIMAL(12,2) COMMENT '订单金额',
    discount_amount DECIMAL(12,2) COMMENT '优惠金额',
    pay_amount DECIMAL(12,2) COMMENT '实付金额',
    pay_type STRING COMMENT '支付方式',
    order_time TIMESTAMP COMMENT '下单时间',
    pay_time TIMESTAMP COMMENT '支付时间',
    cancel_time TIMESTAMP COMMENT '取消时间',
    start_date STRING COMMENT '有效期开始日期',
    end_date STRING COMMENT '有效期结束日期',
    is_current INT COMMENT '是否最新(1是0否)',
    _load_time TIMESTAMP COMMENT '数据加载时间'
) COMMENT 'DWD层订单明细表'
STORED AS PARQUET
LOCATION '/warehouse/dwd/order_detail';

-- DWD层用户维度表(拉链)
CREATE TABLE dwd_user_dimension (
    user_id BIGINT COMMENT '用户ID',
    user_name STRING COMMENT '用户姓名',
    phone STRING COMMENT '手机号',
    register_time TIMESTAMP COMMENT '注册时间',
    user_level STRING COMMENT '用户等级',
    start_date STRING COMMENT '有效期开始日期',
    end_date STRING COMMENT '有效期结束日期',
    is_current INT COMMENT '是否当前',
    _load_time TIMESTAMP COMMENT '加载时间'
) COMMENT 'DWD层用户维度表'
STORED AS PARQUET
LOCATION '/warehouse/dwd/user_dimension';

3. 数据清洗

python 复制代码
def clean_order_data(df):
    # 去除重复
    df = df.dropDuplicates(["order_id"])
    
    # 处理空值
    df = df.fillna({
        "discount_amount": 0,
        "pay_time": "1970-01-01 00:00:00"
    })
    
    # 数据标准化
    df = df.withColumn("phone",
        regexp_replace(col("phone"), "[^0-9]", ""))
    
    df = df.withColumn("phone",
        concat(lit("86"), col("phone")))
    
    # 异常值处理
    df = df.filter(
        (col("pay_amount") >= 0) &
        (col("pay_amount") < 1000000) &
        (col("order_status").isin([1, 2, 3, 4, 5, 6]))
    )
    
    # 枚举值映射
    status_map = {
        1: "待支付",
        2: "已支付",
        3: "已完成",
        4: "已取消",
        5: "退款中",
        6: "已退款"
    }
    
    for status, name in status_map.items():
        df = df.withColumn("order_status_name",
            when(col("order_status") == status, name)
            .otherwise(col("order_status_name")))
    
    return df

4. 拉链处理

python 复制代码
def slowly_changing_dimension(df, pk, start_col, end_col, scd_cols):
    """
    拉链表处理
    """
    # 读取历史数据
    history_df = spark.read.parquet(f"/warehouse/dwd/{table_name}")
    
    # 找出变化的数据
    changed_df = df.alias("new") \
        .join(history_df.alias("old"),
              df[pk] == history_df[pk], "left") \
        .filter(
            # 新增记录
            history_df[pk].isNull() |
            # 变化记录
            | any(col(f"new.{c}") != col(f"old.{c}") 
                 for c in scd_cols)
        ) \
        .select("new.*")
    
    # 关闭历史记录
    history_df = history_df.join(
        changed_df.select(pk), pk, "leftanti"
    ).withColumn(end_col, current_date())
    
    # 新增记录设置有效期
    changed_df = changed_df \
        .withColumn(start_col, current_date()) \
        .withColumn(end_col, lit("9999-12-31"))
    
    # 合并
    result = history_df.union(changed_df)
    
    return result

五、DWS层(数据汇总层)

1. 特点

  • 轻度汇总:按主题按天/周/月汇总
  • 通用性:可供多个应用复用
  • 宽表设计:减少Join,提高查询性能
  • 预计算:减少实时计算压力

2. 建表语句

sql 复制代码
-- DWS层订单汇总宽表(每日)
CREATE TABLE dws_order_daily (
    shop_id BIGINT COMMENT '商家ID',
    shop_name STRING COMMENT '商家名称',
    category_id BIGINT COMMENT '品类ID',
    category_name STRING COMMENT '品类名称',
    order_date STRING COMMENT '统计日期',
    order_count INT COMMENT '订单数',
    order_user_count INT COMMENT '下单用户数',
    order_amount DECIMAL(14,2) COMMENT '订单金额',
    pay_count INT COMMENT '支付订单数',
    pay_user_count INT COMMENT '支付用户数',
    pay_amount DECIMAL(14,2) COMMENT '支付金额',
    refund_count INT COMMENT '退款订单数',
    refund_amount DECIMAL(14,2) COMMENT '退款金额',
    new_user_count INT COMMENT '新用户下单数',
    _load_time TIMESTAMP COMMENT '加载时间'
) COMMENT 'DWS层订单汇总表'
PARTITIONED BY (dt STRING)
STORED AS PARQUET
LOCATION '/warehouse/dws/order_daily';

-- DWS层用户行为宽表
CREATE TABLE dws_user_behavior_daily (
    user_id BIGINT COMMENT '用户ID',
    user_name STRING COMMENT '用户姓名',
    user_level STRING COMMENT '用户等级',
    register_date STRING COMMENT '注册日期',
    behavior_date STRING COMMENT '行为日期',
    pv INT COMMENT '浏览商品数',
    cart_count INT COMMENT '加购次数',
    order_count INT COMMENT '下单次数',
    order_amount DECIMAL(12,2) COMMENT '下单金额',
    pay_count INT COMMENT '支付次数',
    pay_amount DECIMAL(12,2) COMMENT '支付金额',
    _load_time TIMESTAMP COMMENT '加载时间'
) COMMENT 'DWS层用户行为表'
PARTITIONED BY (dt STRING)
STORED AS PARQUET
LOCATION '/warehouse/dws/user_behavior_daily';

3. 数据汇总

python 复制代码
def build_order_daily():
    spark = SparkSession.builder.getOrCreate()
    
    # 读取DWD层数据
    order_df = spark.read.parquet("/warehouse/dwd/order_detail")
    
    # 过滤当日数据
    today = "2024-01-15"
    order_today = order_df.filter(col("dt") == today)
    
    # 汇总计算
    daily_stats = order_today.groupBy("shop_id", "category_id", "order_date") \
        .agg(
            count("*").alias("order_count"),
            countDistinct("user_id").alias("order_user_count"),
            sum("order_amount").alias("order_amount"),
            count(when(col("pay_time").isNotNull(), 1)).alias("pay_count"),
            sum(when(col("pay_time").isNotNull(), col("pay_amount"))).alias("pay_amount"),
            count(when(col("order_status") == 4, 1)).alias("refund_count"),
            sum(when(col("order_status") == 4, col("pay_amount"))).alias("refund_amount")
        )
    
    # 写入DWS层
    daily_stats.write \
        .format("parquet") \
        .mode("overwrite") \
        .partitionBy("dt") \
        .saveAsTable("dws_order_daily")

六、ADS层(数据应用层)

1. 特点

  • 面向应用:根据业务需求定制
  • 直接可查:供报表、数据产品使用
  • 性能优先:预计算好结果

2. 报表宽表

sql 复制代码
-- ADS层商家经营报表
CREATE TABLE ads_shop_report (
    shop_id BIGINT COMMENT '商家ID',
    shop_name STRING COMMENT '商家名称',
    category_name STRING COMMENT '品类',
    region_name STRING COMMENT '地区',
    report_date STRING COMMENT '报表日期',
    order_count INT COMMENT '订单数',
    order_amount DECIMAL(14,2) COMMENT '订单金额',
    pay_rate DECIMAL(6,4) COMMENT '支付转化率',
    avg_order_amount DECIMAL(12,2) COMMENT '客单价',
    refund_rate DECIMAL(6,4) COMMENT '退款率',
    new_user_count INT COMMENT '新用户数',
    old_user_count INT COMMENT '老用户数',
    repeat_rate DECIMAL(6,4) COMMENT '复购率',
    _load_time TIMESTAMP COMMENT '加载时间'
) COMMENT 'ADS层商家经营报表'
STORED AS PARQUET
LOCATION '/warehouse/ads/shop_report';

3. 报表计算

python 复制代码
def build_shop_report():
    spark = SparkSession.builder.getOrCreate()
    
    # 读取DWS层数据
    order_daily = spark.read.parquet("/warehouse/dws/order_daily")
    shop_df = spark.read.parquet("/warehouse/dwd/shop_dimension")
    
    # 计算报表
    report = order_daily.alias("o") \
        .join(shop_df.alias("s"), "shop_id", "left") \
        .groupBy("o.shop_id", "report_date") \
        .agg(
            sum("order_count").alias("order_count"),
            sum("order_amount").alias("order_amount"),
            avg(when(col("pay_count") > 0, col("pay_count") / col("order_count"))).alias("pay_rate"),
            avg("order_amount" / col("order_count")).alias("avg_order_amount")
        )
    
    report.write \
        .format("parquet") \
        .mode("overwrite") \
        .saveAsTable("ads_shop_report")

七、总结

数据仓库分层是数据架构的基础:

  • ODS:原始数据层,保留历史
  • DWD:明细数据层,清洗标准化
  • DWS:汇总数据层,主题宽表
  • ADS:应用数据层,报表定制

最佳实践:

  1. 合理设计分层,减少数据冗余
  2. 统一命名规范,便于理解
  3. 建立数据质量监控
  4. 做好数据血缘追踪

个人观点,仅供参考

相关推荐
名字不好奇3 小时前
Claude Code工作原理深度解析:从技术架构到设计哲学
人工智能·架构
一条咸鱼_SaltyFish3 小时前
DDD 架构重构实践:AI Skills 如何赋能DDD设计与重构
java·人工智能·ai·重构·架构·ddd·领域驱动设计
33三 三like3 小时前
GraphRAG 架构在养老志愿服务推荐中的创新应用:当知识图谱遇见大语言模型
语言模型·架构·知识图谱
企鹅的蚂蚁4 小时前
【ESP32-S3 深度实战】从小智AI底层移植到自定义LVGL表情:M5Stack CoreS3 避坑与架构指南
人工智能·架构
自然语4 小时前
人工智能之数字生命 认知架构白皮书 第4章
人工智能·架构
观无4 小时前
微服务下的跨域问题
微服务·云原生·架构
观无6 小时前
微服务架构核心技术知识全景总结
微服务·云原生·架构
那我懂你的意思啦6 小时前
微服务学习+商城
学习·微服务·架构