一、数据仓库分层概述
数据仓库分层是数据架构的核心设计,合理的分层能:
- 降低复杂度:逐层处理,减少依赖
- 提高复用性:中间层可供多个应用使用
- 便于数据溯源:问题定位更简单
- 隔离变化:上游变化不影响下游
二、分层架构
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:应用数据层,报表定制
最佳实践:
- 合理设计分层,减少数据冗余
- 统一命名规范,便于理解
- 建立数据质量监控
- 做好数据血缘追踪
个人观点,仅供参考