什么是拉链表
拉链表 就是一张记录数据历史变化状态 的表。它不保存每天的全量快照 ,而是用两个时间字段(通常叫 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 开头,命名规范 本身就是分层约束的一部分。