大数据-233 离线数仓 留存率怎么做:DWS 明细建模 + ADS 聚合落表(Hive/脚本实战)

TL;DR

  • 场景:离线数仓统计 1/2/3 日会员留存数与留存率,基于新增与启动日志。
  • 结论:DWS 先做"新增日×留存天数"的留存明细,ADS 再按新增日聚合并补齐分母算留存率。
  • 产出:Hive DWS/ADS 表结构、装载脚本、计算链路与关键连线修正点。

留存会员

  • 留存会员与留存率:某段时间的新增会员,经过一段时间后,仍继续使用应用认为是留存会员,这部分会员占当时新增会员的比例为留存率。
  • 需求:1日、2日、3日的会员留存率和会员留存率
  • 10W新会员:dws_member_add_day(dt=08-01)明细
  • 3w:特点是1号是新会员,在2日启动了(2日的启动日志)dws_member_start_day

DWS 的作用

统一数据模型

将原始数据(ODS层)按照一定的逻辑模型进行整合、清洗、加工,形成标准化的数据结构。 支持对数据的多维度、多粒度分析。

支持业务场景

满足企业对历史数据的查询和分析需求。 支持 OLAP(在线分析处理)操作,如聚合查询、钻取和切片。

数据细化与分类

将数据按照主题域(如销售、财务、库存等)分类,便于管理和查询。 通常保持较高的细节粒度,便于灵活扩展。

数据准确性与一致性

经过处理的数据经过校验,确保逻辑关系正确,能够为下游提供准确的一致性数据。

创建DWS层表

sql 复制代码
-- 会员留存明细
drop table if exists dws.dws_member_retention_day;
create table dws.dws_member_retention_day
(
  `device_id` string,
  `uid` string,
  `app_v` string,
  `os_type` string,
  `language` string,
  `channel` string,
  `area` string,
  `brand` string,
  `add_date` string comment '会员新增时间',
  `retention_date` int comment '留存天数'
)COMMENT '每日会员留存明细'
PARTITIONED BY (`dt` string)
stored as parquet;

执行结果如下图所示:

加载DWS层数据

shell 复制代码
vim /opt/wzk/hive/dws_load_member_retention_day.sh

写入的内容如下所示:

sql 复制代码
#!/bin/bash
source /etc/profile
if [ -n "$1" ] ;then
do_date=$1
else
do_date=`date -d "-1 day" +%F`
fi
sql="
drop table if exists tmp.tmp_member_retention;
create table tmp.tmp_member_retention as
(
  select t2.device_id,
  t2.uid,
  t2.app_v,
  t2.os_type,
  t2.language,
  t2.channel,
  t2.area,
  t2.brand,
  t2.dt add_date,
  1
  from dws.dws_member_start_day t1 join dws.dws_member_add_day
  t2 on t1.device_id=t2.device_id
  where t2.dt=date_add('$do_date', -1)
  and t1.dt='$do_date'
  union all
  select t2.device_id,
  t2.uid,
  t2.app_v,
  t2.os_type,
  t2.language,
  t2.channel,
  t2.area,
  t2.brand,
  t2.dt add_date,
  2
  from dws.dws_member_start_day t1 join dws.dws_member_add_day
  t2 on t1.device_id=t2.device_id
  where t2.dt=date_add('$do_date', -2)
  and t1.dt='$do_date'
  union all
  select t2.device_id,
  t2.uid,
  t2.app_v,
  t2.os_type,
  t2.language,
  t2.channel,
  t2.area,
  t2.brand,
  t2.dt add_date,
  3
  from dws.dws_member_start_day t1 join dws.dws_member_add_day
  t2 on t1.device_id=t2.device_id
  where t2.dt=date_add('$do_date', -3)
  and t1.dt='$do_date'
);
insert overwrite table dws.dws_member_retention_day
partition(dt='$do_date')
select * from tmp.tmp_member_retention;
"
hive -e "$sql"

写入的内容如下所示:

ADS 作用

聚合和简化数据

将 DWS 层中多表、多主题域的数据聚合成简单易用的表或视图。 直接输出满足业务需求的数据结果。

面向业务应用

通过设计宽表或高性能视图,直接支持具体的业务场景和报表需求。 响应快速查询需求,如实时数据的展示。

数据分发与集成

为前端的 BI 工具、报表系统或 API 服务提供高效的查询接口。 能够通过缓存机制或物化视图加速查询性能。

轻量化与高性能

尽量减少数据量,保留业务最关心的关键指标。 采用预聚合、预计算等技术提升查询效率。

创建ADS层表

sql 复制代码
-- 会员留存数
drop table if exists ads.ads_member_retention_count;
create table ads.ads_member_retention_count
(
  `add_date` string comment '新增日期',
  `retention_day` int comment '截止当前日期留存天数',
  `retention_count` bigint comment '留存数'
) COMMENT '会员留存数'
partitioned by(dt string)
row format delimited fields terminated by ',';

-- 会员留存率
drop table if exists ads.ads_member_retention_rate;
create table ads.ads_member_retention_rate
(
  `add_date` string comment '新增日期',
  `retention_day` int comment '截止当前日期留存天数',
  `retention_count` bigint comment '留存数',
  `new_mid_count` bigint comment '当日会员新增数',
  `retention_ratio` decimal(10,2) comment '留存率'
) COMMENT '会员留存率'
partitioned by(dt string)
row format delimited fields terminated by ',';

执行结果如下图所示:

加载ADS层数据

shell 复制代码
vim /opt/wzk/hive/ads_load_member_retention.sh

写入的内容如下所示:

sql 复制代码
#!/bin/bash
source /etc/profile
if [ -n "$1" ] ;then
  do_date=$1
else
  do_date=`date -d "-1 day" +%F`
  fi
  sql="
insert overwrite table ads.ads_member_retention_count
partition (dt='$do_date')
select add_date, retention_date,
count(*) retention_count
from dws.dws_member_retention_day
where dt='$do_date'
group by add_date, retention_date;
insert overwrite table ads.ads_member_retention_rate
partition (dt='$do_date')
select t1.add_date,
t1.retention_day,
t1.retention_count,
t2.cnt,
t1.retention_count/t2.cnt*100
from ads.ads_member_retention_count t1 join
ads.ads_new_member_cnt t2 on t1.dt=t2.dt
where t1.dt='$do_date';
"
hive -e "$sql"

备注:最后一条SQL的连线条件应该为:t1.add_date=t2.dt。 写入的内容如下所示:

暂时小结

到此,我们的数仓基本上构建完毕,后续将数据导入进行详细的处理。 离线数仓(Data Warehouse)通常分为多个层次,以便更好地组织数据。其中,DWS(Data Warehouse Service)和 ADS(Application Data Service)是数据仓库中两个重要的逻辑层级,分别承担不同的功能。

  • DWS:是数仓的核心层,旨在加工、汇总和提供高复用的数据服务。
  • ADS:是数仓的应用层,专注于满足特定业务场景的需求。

WS(Data Warehouse Service)------服务层或中间层 定义:DWS 通常是离线数仓中的汇总层或服务层,主要对数据进行整理、加工和聚合,为上层的数据分析或应用提供服务。

ADS(Application Data Service)------应用层 定义:ADS 是数仓中的应用层,通常是直接面向业务需求,提供特定场景下所需的分析结果或服务。

DWS 与 ADS 的关系

  • DWS 是 ADS 的上游:DWS 提供的标准化、可复用的数据服务是 ADS 构建的基础。
  • 功能定位不同:DWS 更偏向数据整合与标准化,而 ADS 更关注业务需求的个性化满足。
  • 数据粒度:DWS 的数据粒度更细,ADS 通常是基于 DWS 的聚合和过滤,面向特定需求输出。

错误速查

症状 根因 定位 修复
ADS 留存率算不出来/留存率异常偏大偏小 留存率 SQL join 条件写错,把 t1.dt=t2.dt 误当新增日对齐 对比 ads_member_retention_count 的 add_date 与 ads_new_member_cnt 的 dt 是否一致 将连线改为 t1.add_date = t2.dt
DWS 留存明细数据异常膨胀 dws_member_add_day 与 start_day 可能存在同一 device_id 多条记录,join 产生倍增 检查两张明细表在 dt 分区内 device_id 是否唯一;抽样 count(*) vs count(distinct device_id) 在 join 前做去重/主键约束(按 device_id/uid 选最新或 distinct),或改为 semi join/exists 口径
留存天数统计缺失(例如只算到 3 日) 脚本写死 union all 1/2/3 看 dws_load_member_retention_day.sh 中 union 段 抽象为 N 日留存:用维表/数字表生成 retention_day,再 date_add 动态计算
留存口径偏差(跨设备、跨账号) 用 device_id 作为主键,会员维度可能应以 uid 为准(或两者组合) 对比业务定义:会员=账号还是设备;看 add_day 的 uid 是否稳定 明确口径:若以 uid 留存,join key 改为 uid;若以设备留存,uid 仅作属性
留存率小数精度/百分比显示不正确 retention_count/t2.cnt*100 可能触发整除或精度不足;decimal(10,2) 存储但计算未 cast 看 Hive 中表达式类型推断与最终 retention_ratio 使用 cast:cast(t1.retention_count as decimal(18,4))/cast(t2.cnt as decimal(18,4))*100,并按需要 round
分区覆盖/数据丢失 insert overwrite 覆盖分区;脚本默认 do_date=-1,误跑会覆盖历史 看 insert overwrite + dt='$do_date' 保留 overwrite 但加跑批保护:参数必填或写保护;或改为 insert into + 幂等去重策略
临时表重复创建影响性能 每天 drop/create tmp 表,且 union all 三次扫描 start_day/add_day explain/执行日志观察扫描次数与 shuffle 改为一次扫描:先取 do_date 启动集合,再与 add_day 通过 case when 计算 retention_day,或用多条件 join + lateral view/维表
Parquet 与行格式表混用导致性能不一致 DWS 用 parquet,ADS 用 textfile(fields terminated by ','),查询链路可能退化 观察 ADS 查询性能、压缩率、文件数 ADS 也用 parquet/orc;若为导出给外部系统再落 textfile,通过中间层导出

其他系列

🚀 AI篇持续更新中(长期更新)

AI炼丹日志-29 - 字节跳动 DeerFlow 深度研究框斜体样式架 私有部署 测试上手 架构研究 ,持续打造实用AI工具指南! AI研究-132 Java 生态前沿 2025:Spring、Quarkus、GraalVM、CRaC 与云原生落地

💻 Java篇持续更新中(长期更新)

Java-218 RocketMQ Java API 实战:同步/异步 Producer 与 Pull/Push Consumer MyBatis 已完结,Spring 已完结,Nginx已完结,Tomcat已完结,分布式服务已完结,Dubbo已完结,MySQL已完结,MongoDB已完结,Neo4j已完结,FastDFS 已完结,OSS已完结,GuavaCache已完结,EVCache已完结,RabbitMQ已完结,RocketMQ正在更新... 深入浅出助你打牢基础!

📊 大数据板块已完成多项干货更新(300篇):

包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈! 大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT案例 详解

相关推荐
春日见8 小时前
拉取与合并:如何让个人分支既包含你昨天的修改,也包含 develop 最新更新
大数据·人工智能·深度学习·elasticsearch·搜索引擎
青云计划8 小时前
知光项目知文发布模块
java·后端·spring·mybatis
Victor3569 小时前
MongoDB(9)什么是MongoDB的副本集(Replica Set)?
后端
Victor3569 小时前
MongoDB(8)什么是聚合(Aggregation)?
后端
Elastic 中国社区官方博客10 小时前
如何防御你的 RAG 系统免受上下文投毒攻击
大数据·运维·人工智能·elasticsearch·搜索引擎·ai·全文检索
yeyeye11110 小时前
Spring Cloud Data Flow 简介
后端·spring·spring cloud
Tony Bai11 小时前
告别 Flaky Tests:Go 官方拟引入 testing/nettest,重塑内存网络测试标准
开发语言·网络·后端·golang·php
YangYang9YangYan11 小时前
2026中专大数据与会计专业数据分析发展路径
大数据·数据挖掘·数据分析
+VX:Fegn089511 小时前
计算机毕业设计|基于springboot + vue鲜花商城系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计