【从零开始学习数据仓库 | 第二篇】: 拉链表 & 数仓分层

什么是拉链表

拉链表 就是一张记录数据历史变化状态 的表。它不保存每天的全量快照 ,而是用两个时间字段(通常叫 start_date 和 end_date)来标记每一条记录的生命周期:某条数据从哪天开始生效,到哪天失效。

举个例子,用户表里有 10 亿条记录,每天只有 200 万条左右会发生新增或变更。如果每天存一份全量快照 ,存储成本极高,而且 99% 的数据都是重复的。拉链表只记录变化:当某个用户的手机号变了,就把他旧记录的那条"结束日期"改成今天,再插入一条新记录,开始日期是今天,结束日期设成一个极大值 (比如 2999-12-31),表示"当前有效"。

查历史快照时,用 start_date <= 查询日期 and end_date > 查询日期 就能精准命中那一天的状态。这就是拉链表 "用时间换空间"的核心逻辑。

拉链表的实现方式

先修改再插入

sql 复制代码
-- 第一步:闭链,只更新变化行
update dw_zipper
set endtime = '2024-01-01'
where endtime = '9999-12-31'
  and userid in (select userid from ods_zipper_update);

-- 第二步:开链,插入新增和变更后的记录
insert into dw_zipper
select userid, phone, '2024-01-02' as starttime, '9999-12-31' as endtime
from ods_zipper_update;

这种方式在支持行级更新 的存储上才可行(比如关系型数据库,或 Hudi /Iceberg 这类表格式)。逻辑分两步:

闭链 :把拉链表 中"还在开链 状态"(end_date 是极大值 )、且主键在今日增量 里出现过的记录,end_date 更新为当前日期。

开链插入 :把今日增量 中的新增和变更记录,以 start_date = 当前日期、end_date = 极大值 插入拉链表。

优点是只做定向的 update 和 insert ,不重写全表,I/O 消耗小。缺点是在传统 Hive 环境里做不到行级更新 ,而且两步操作不是原子的,中间失败需要额外处理。所以它更多出现在支持 upsert 的现代存储引擎里,不是传统数仓的默认选择。

先删除再插入

sql 复制代码
insert overwrite table dw_zipper
select userid, phone, starttime, endtime
from dw_zipper
where userid not in (select userid from ods_zipper_update)  -- "删除"将被覆盖的记录
union all
-- 历史中未变化的记录原样保留(上面已排除,这里补充新增和变更)
select userid, phone, '2024-01-02', '9999-12-31'
from ods_zipper_update;

逻辑是:把拉链表 中所有主键在今日增量里出现过的历史记录物理删除,再重新插入处理好的完整记录。

这种方式在早期用关系型数据库做拉链表 时比较常见,因为 delete + insert 的语义简单。但在大数据场景下问题很明显:delete 会扫全表,性能很差,中间失败恢复也麻烦。现在一般不推荐用于生产级拉链表。

日常拉链更新的核心逻辑记住一句话就够了:旧记录闭链 ,新记录开链 。区别只在于物理上怎么落地------传统 Hive 用覆盖写入 ,支持行级更新的存储用先改后插。

覆盖写入

sql 复制代码
-- 将老拉链表与增量表合并到临时表
insert overwrite table tmp_zipper
select userid, phone, starttime,
       if(b.userid is null, a.endtime, date_sub(b.starttime, 1)) as endtime
from (
    -- 老拉链表中被更新的记录:闭链
    select a.userid, a.phone, a.starttime,
           case when b.userid is not null and a.endtime = '9999-12-31'
                then date_sub(b.starttime, 1)
                else a.endtime end as endtime
    from dw_zipper a
    left join ods_zipper_update b on a.userid = b.userid
    where a.endtime = '9999-12-31' or b.userid is not null
    union all
    -- 增量数据:开新链
    select userid, phone, '2024-01-02' as starttime, '9999-12-31' as endtime
    from ods_zipper_update
) tmp;

-- 覆盖写回拉链表
insert overwrite table dw_zipper
select * from tmp_zipper;

这是 Hive 这类传统大数据环境里拉链表日常更新的标准做法,也是工程上最普遍的落地方式。

整体流程分三步:

第一步:增量采集。 从业务库拿到当天的增量数据(新增和变更的记录)。

第二步:合并计算。 把老拉链表 和增量表 做关联,用 case when 逻辑算出完整的新表数据:对于老表中主键在增量里出现过的"开链 记录"(end_date 是极大值 ),把 end_date 闭链 成当前日期;对于增量数据 中的新增和变更记录,以 start_date = 当前日期、end_date = 极大值 的形式开新链。两部分 UNION ALL 到一起,落到临时表。

第三步:覆盖写回。 用 INSERT OVERWRITE 把临时表的数据整体覆盖写入拉链表。

这种方式成为主流有几个现实原因:

Hive 早期不支持行级 UPDATE。传统 Hive 表只能做分区或整表覆盖,没法像关系型数据库那样精准 update 某几行。所以"闭链"这个动作只能在查询时用逻辑算出来,再整体覆盖回去。

覆盖写天然幂等,重跑安全。调度系统里任务失败重跑是常态。每天重写自己的分区或整表,跑多少次结果都一样,不会留下断链或重复数据。

实现简单,逻辑集中在一个 SQL 里。闭链和开链合并在一次查询中完成,不需要维护中间状态。

代价是每次都要重写整张拉链表,数据量大了以后 I/O 和计算资源消耗可观。后来 Hudi 这类支持 upsert 的存储格式出现,才让"只更新变化行"成为可能,不需要全表覆盖。

维度建模流程

Kimball

拉链表 服务于维度建模 中的"缓慢变化维 "处理。而 Kimball 的维度建模方法论,核心就是下面这四步:

第一步:选取业务过程 。 先想清楚你要分析什么。是"用户下单"这件事,还是"库存变更"这件事?业务过程 是事实表的来源,通常对应源系统里的一张流水表或事务表。

第二步:定义粒度 。 这是最关键的一步。粒度 决定了事实表 里一行代表什么。比如订单事实表 ,一行是订单头还是订单行?粒度越原子越好,因为原子数据能支撑任何上卷分析,反过来则不行。

第三步:选择维度 。 围绕业务过程 ,确定"谁、什么时间、在哪里、做了什么"。常见的维度 有日期、用户、商品、门店等。维度表承载的是描述性属性,比如用户的手机号、商品的类目。

第四步:选择事实 。 最后确定要度量什么。销售额、数量、次数、时长,这些是事实表里的数值型字段。

走完这四步,你得到的就是一张事实表 + 若干维度表的星型结构。而拉链表,通常用在维度表上:当用户维度里的手机号、地址这些属性会变化,你又需要追溯"某个时间点这个用户是什么状态"时,拉链表就是维度表的一种存储实现。

数仓分层

分层原因与优点

数仓分层的本质是解耦。不分层也能出报表,但前台业务、数据处理逻辑和底层数据库会完全绑死,任何一环变动都要推倒重来。分层的核心价值体现在几个方面:

清晰数据结构。每层有明确的作用域和职责,使用和维护时能快速定位数据在哪一层、该找哪张表。

数据血缘追踪。目标数据通常来自多张源表,出现异常时清晰的血缘关系可以快速定位问题源头,这也是元数据管理的基础。

减少重复开发。逐层加工,下层包含上层所需的加工数据,不同开发者不必各自从源系统重复抽取。

屏蔽底层变化。应用层数据经过多层加工,原始系统的字段变更、格式调整在中间层被消化掉,不会直接冲击上层报表和接口。

复杂问题拆解。把一步到位的清洗加工拆成多层小步骤,每层逻辑相对简单,出错时只需局部调整某一层,而不是排查一个巨大的黑盒。

传统数仓分层

ODS 数据准备层

贴源层 ,从业务库、日志、消息队列等数据源同步原始数据。这一层不做过多清洗,核心目标是保留原始面貌,同时把数据从业务系统解耦 出来,避免分析查询直接压到生产库上。数据来源方式包括 Sqoop 定时抽取、Canal 监听 binlog、Flume 采集日志等。

DW 数据仓库层

DIM 公共维度层

存储描述性信息,是分析数据的"坐标"。比如日期维表、地区维表、商品类目维表。DIM 层横跨多层,从 DWD 往上都会引用它。对于会变化的维度属性(如用户地址、手机号),通常用拉链表保留历史状态。

DWD 明细数据层

数仓中数据治理最密集的一层。主要工作包括:清洗脏数据(空值、异常枚举、超出范围的值)、统一格式(不同源系统的日期格式、布尔值表示)、敏感字段脱敏、维度退化 (把一些维度属性直接冗余到事实表 以减少关联)。粒度 通常与 ODS 保持一致,但数据质量显著提升。

DWS/DWM/DWB 数据汇总层/数据中间层

这一层的叫法在不同团队有差异。DWM (中间层)做轻度聚合,提升公共指标复用性,减少重复计算。DWS (服务层)按分析主题做汇总,形成宽表 ,比如用户主题宽表一行代表一个用户一天的行为汇总,包含登录次数、下单金额、支付次数等字段。DWB 在一些架构中承担基础数据层或轻度汇总的职责。实际落地时不一定三层全有,很多团队会根据业务复杂度裁剪。

DWT/DWS 数据服务层

DWT (主题层)在 DWS 按天汇总的基础上,把时间维度拉长,做累积汇总。一行代表一个主题对象从开始到当前的累积行为,比如用户全生命周期的累计下单金额、最近一次活跃日期等。DWS 和 DWT 的命名在不同文档中偶有交叉,核心区分在于:DWS 偏按周期汇总,DWT 偏累积快照。

ADS 数据应用层

面向具体应用场景的数据出口。直接对接报表、看板、接口、标签系统等。数据形式经过进一步加工聚合,结构更适合前端直接使用,不再保留明细粒度。

TMP 临时数据层

每一层计算过程中都会产生大量临时表,专设 TMP 层来存放这些中间结果。它的特点是临时性 和易变性 :用完即弃,或者仅服务于某次特定加工任务。单独划出 TMP 层的好处是避免临时表散落在各业务层中造成命名混乱 和权限失控 ,同时也便于统一清理和存储管理。实际规范中通常要求 TMP 表带明确的业务含义后缀,并设置生命周期,避免临时数据无限堆积。

阿里数仓分层

发展史

阿里数仓的模型体系不是一步设计出来的,而是随着业务压力和技术平台演进了三代。

第一代:完全应用驱动(Oracle 时代)。 最早的数据仓库建在 Oracle 上,只有两层:ODS 和 DSS。数据工程师把源系统数据同步到 ODS,然后直接基于 ODS 写统计逻辑出报表。没有系统化的模型方法论,纯粹是需求驱动,什么报表来了就写什么 SQL。历史数据处理零星用到维度建模 的缓慢变化维思路,但整体上谈不上架构设计。

第二代:四层模型架构(Greenplum 时代)。 数据量涨上来之后,Oracle 扛不住了,引入 Greenplum 做 MPP 计算。团队尝试把传统行业的 ER 模型 + 维度建模 搬过来,设计出 ODL(操作数据层)+ BDL(基础数据层)+ IDL(接口数据层)+ ADL(应用数据层)的四层结构。想法是好的:BDL 用 ER 模型 做数据整合,IDL 用维度建模 做集市层。但实际推进时遇到了大麻烦------互联网业务变化太快,人员流动也快,业务知识积累不够,ER 模型根本设计不出来,迟迟产不出结果。这件事留下的教训很直接:在快速变化的业务面前硬上 ER 模型,风险极高。

第三代:公共层模型体系(MaxCompute/Hadoop 时代)。 拥抱分布式计算平台后,阿里放弃了 ER 模型路线,转向以 Kimball 维度建模为核心理念,并在此基础上做了升级,形成了公共层模型数据架构体系。核心方法论叫 OneData,包含一致性指标定义体系、模型设计方法体系和配套工具。目的是解决多 BU 之间数据存储和计算的共享问题------当时数据每年增长近 2.5 倍,远超业务增速,成本压力倒逼架构统一。

模型层级

三层架构(官方建议)

阿里云 MaxCompute 文档建议自下而上分三层:

ODS(数据引入层):存放未经处理的原始数据,结构与源系统保持一致,是数仓的数据准备区。同时承担记录基础数据历史变化的职责。ODS 内部又分三块:数据准备区、离线数据区、准实时数据区。

CDM(数据公共层):由 ODS 加工而来,是模型建设的核心。目标是建立一致性维度、构建可复用的明细事实表、汇总公共粒度指标。CDM 包含三个子层:

DIM(公共维度层):基于维度建模理念建立整个企业的一致性维度,降低计算口径和算法不统一的风险。维度和维度逻辑表通常一一对应。

DWD(明细粒度事实层):以业务过程为驱动,构建最细粒度的明细事实表。允许将重要维度属性冗余到事实表中做宽表化处理,减少关联、提升易用性。

DWS(公共汇总粒度事实层):以分析主题对象为驱动,基于上层应用的指标需求,构建公共粒度的汇总指标事实表,以宽表形式物理化。目标是命名规范、口径一致,为上层提供可复用的公共指标。

ADS(数据应用层):存放数据产品个性化的统计指标数据,根据 CDM 和 ODS 加工生成,直接对接报表和接口。

DataWorks 内置的五层展示

DataWorks 工具里默认创建的分层更细,包括 ODS 、DIM 、DWD 、DWS 、ADS 五层。这和你上一篇里提到的传统数仓分层 (ODS /DW /ADS /TMP )在命名和切分上有差异,原因是阿里的体系把"公共层"的概念做得更重------DIM 、DWD 、DWS 在阿里体系里都属于 CDM 的范畴,是一个整体的"可复用公共数据层",而不是各自独立的加工阶段。实际建表时,ODS 层表名以 ods 开头,DWD 以 dwd 开头,DWS 以 dws 开头,ADS 以 ads 开头,DIM 以 dim 开头,命名规范 本身就是分层约束的一部分。

相关推荐
一木 之林1 小时前
Dify学习笔记 04 · 检索底座:Embedding、Rerank与向量数据库(第46-54集)
笔记·学习·embedding·dify
芯思路2 小时前
STM32开发学习笔记之十【AI部署】
笔记·stm32·学习
GlobalInfo2 小时前
机器人线缆用无卤TPU料市场分析报告深度版:产业洞察、技术演进与路径
大数据·人工智能·机器人
憧憬成为java架构高手的小白2 小时前
langgraph4j学习
学习
CCIE-Yasuo2 小时前
网络安全-学习web开发-列表、标签
前端·学习·web安全
旖旎夜光3 小时前
LeetCode 315: 算右侧小于当前元素的个数(归并排序) —— 题解
c++·学习·算法·leetcode·排序算法
MARSERERER3 小时前
Flink将Hive数据导入StarRocks中任务重跑如何避免数据重复?
大数据
电子科技圈3 小时前
2026 RISC-V中国峰会观察:编译调试工具链的“在场节奏”,释放了什么信号?
大数据·人工智能·设计模式·软件工程·软件构建·代码规范·risc-v
紧固件情报站3 小时前
紧固件报价逻辑重构:成本、合规与智能博弈
大数据·人工智能·重构