实际上不太喜欢研究数仓建模相关的理论。感觉像是在看英文语法书,一点也不Geek。但是真看起来,还是有一些要点值得被记录。
数据仓库领域数据建模通常采用维度建模。维度模型将复杂的业务通过事实和维度两个概念进行呈现。
事实通常对应业务过程,就是具体的用户行为或者发生的事件。电商交易中的下单,取消订单,付款,退单等,都是业务过程。
维度通常对应业务过程发生时所处的环境,就是行为或者事件相关的属性。比如下单这个业务过程包含了以下维度:Date(日期),Customer(顾客),Product(产品),Location(地区)等。
维度建模以数据分析作为出发点,为数据分析服务,因此它关注的重点是如何更快的完成需求分析,以及如何较好的提升大规模复杂查询的性能。
事实表
事实表是包含具体业务过程的表。包含与该业务过程有关的维度引用(维度表外键)以及该业务过程的度量(通常是数字类型字段)。
维度引用可以用来后续关联维度表获知更具体的维度信息。
事实表中也有可能不包含所谓的业务过程度量,比如关注一个作者,点赞一个帖子这种业务过程。而下单,通常会包含商品数量以及订单金额这种业务过程的度量。
事实表有三种类型:分别是事务型事实表、周期快照事实表和累积快照事实表,每种事实表都具有不同的特点和适用场景。
事务型事实表
数仓中最常见的就是事务型事实表。用于分析与各业务过程相关的各项统计指标,由于其保存了最细粒度的记录,可以提供最大限度的灵活性,可以支持无法预期的各种细节层次的统计需求。
设计事务事实表时一般可遵循以下四个步骤:选择业务过程→声明粒度→确认维度→确认事实
- 选择一个个不可拆分的行为事件作为业务过程。例如电商交易中的下单,取消订单,付款,退单等。
- 声明粒度,一个业务过程也可能用不同的粒度来表示。比如下单可能包含多个商品,一个订单既可以用一行数据来表达,也可以被拆分成多条数据,每一条数据只记录一个商品。通常应尽可能选择最细粒度,以此来应各种细节程度的需求。
- 确认维度,确定事实表中应该包含哪些维度信息。维度的丰富程度就决定了维度模型能够支持的指标丰富程度。
- 确定事实。此处的事实一词,指的是每个业务过程的度量值。可以分为:
- 可加事实:比如订单金额,观看时长
- 半可加事实:比如库存,余额。可以按照商品或者用户维度进行累加,但是不能按照时间维度进行累加。
- 不可加事实:比如比率,转化率。不可加事实通常需要转化为可加事实,例如比率可转化为分子和分母。
事务型事实表的不足
理论上事务型事实表可以支撑与各业务过程相关的各种统计粒度的需求。但对于某些特定类型的需求,其逻辑可能会比较复杂,或者效率会比较低下。
存量型指标
例如商品库存,账户余额等。以账户余额为例,事务型事实表记录的是用户账户每一笔支出和收入。假定现有一个需求,要求统计截至当日的各用户账户的余额。则需要扫描全量用户有史以来的全表数据聚合才能得到统计结果。可以看到事务型事实表对于这个需求而言并不是一个好的方案。
多事务关联统计
例如,现需要统计最近30天,用户下单到支付的时间间隔的平均值。统计思路应该是找到下单事务事实表和支付事务事实表,过滤出最近30天的记录,然后按照订单id对两张事实表进行关联,之后用支付时间减去下单时间,然后再求平均值。
逻辑上虽然并不复杂,但是其效率较低,应为下单事务事实表和支付事务事实表均为大表,大表join大表的操作应尽量避免。
为了解决以上两个问题,引入周期快照事实表和累积快照事实表。
周期快照事实表
周期快照事实表以具有规律性的、可预见的时间间隔来记录事实,主要用于分析一些存量型(例如商品库存,账户余额)或者状态型(空气温度,行驶速度)指标。
对于商品库存、账户余额这些存量型指标,业务系统中通常就会计算并保存最新结果,所以定期同步一份全量数据到数据仓库,构建周期型快照事实表,就能轻松应对此类统计需求,而无需再对事务型事实表中大量的历史记录进行聚合了。
对于空气温度、行驶速度这些状态型指标,由于它们的值往往是连续的,我们无法捕获其变动的原子事务操作,所以无法使用事务型事实表统计此类需求。而只能定期对其进行采样,构建周期型快照事实表。
周期快照事实表设计的时候除了要考虑事务型事实表遵循的4个步骤,另外还需要考虑采样周期。通常采样周期选择”天“,以一天为周期进行快照。当然也有按周,按月,按季度的周期快照。
周期快照事实表和事务型事实表的一个关键区别在于密度。事务事实表本质是稀疏的,当天只有有业务过程发生才会记录相关的数据。而对于周期快照事实表,实际上是稠密的,即使当天没有业务过程发生,仍然会全量记录。比如余额数据,即使用户当天并未有收入或者支出相关的行为发生,周期快照事实表仍然会记录一条数据,即使是与前一天数据完全相同。
注意事项:
事务与快照成对设计
通常在数仓维度建模的时候,为了更好的满足业务过程度量值的分析,往往要求事务型事实表和周期快照事实表成对设计。周期快照事实表主要是为了满足存量或状态型数据分析任务。
附加事实
快照事实表在确定状态度量时, 一般都是保存采样周期结束时的状态度量。但是也有分析需求需要关注上一个采样周期结束时的状态度量,而又不愿意多次使用快照事实表,因此一般在设计周期快照事实表时会附加一些上一个采样周期的状态度量。
累积快照事实表
累计快照事实表是基于一个业务流程中的多个关键业务过程联合处理而构建的事实表,如交易流程中的下单、支付、发货、确认收货业务过程。
累积快照事实表通常具有多个日期字段,每个日期对应业务流程中的一个关键业务过程。
| 订单ID | 用户ID | 下单日期 | 支付日期 | 发货日期 | 确认收货日期 | 订单金额 | 支付金额 |
|---|---|---|---|---|---|---|---|
| 1001 | 0001 | 2022-01-01 | 2022-01-02 | 2022-01-03 | 2022-01-04 | 2000 | 2000 |
累积快照事实表主要用于分析业务过程之间的时间间隔等需求。例如前文提到的用户下单到支付的平均时间间隔,使用累积快照事实表进行统计,就能避免两个事务事实表的关联操作,从而变得十分简单高效。
事务型事实表和周期快照事实表数据只存在插入操作,但是累计快照事实表不仅存在插入操作还存在更新操作。
在实际应用中,业务流程中所包含的业务过程可能并不固定,甚至相当复杂。仍以下单为例,可能并非是下单→支付→发货→收货这么简单直接。中间也可能包含退款、申诉、取消订单等等一些不确定的业务过程。所以在设计和使用累计快照事实表的时候需要格外小心。
物理实现
第一种方式是全量表的形式。此全量表一般为日期分区表,每天的分区存储昨天的全量数据和当天的增量数据合并的结果,保证每条记录的状态最新。此种方式适用于全量数据较少的情况。如果数据量很大,此全量表数据量不断膨胀,存储了大量永远不再更新的历史数据,对ETL 和分析统计性能影响较大。
第二种方式是全量表的变化形式。此种方式主要针对事实表数据量很大的情况。较短生命周期的业务实体一般从产生到消亡都有一定的时间间隔,可以测算此时间间隔,或者根据商业用户的需求确定一个相对较大的时间间隔。比如针对交易订单,我们以200 天作为订单从产生到消亡的最大间隔。设计最近200 天的交易订单累积快照事实表,每天的分区存储最近200 天的交易订单;而200 天之前的订单则按照gmt_create 创建分区存储在归档表中。此方式存在的一个问题是200 天的全量表根据商业需求需要保留多天的分区数据,而由于数据量较大,存储消耗较大。
第三种方式是以业务实体的结束时间分区。每天的分区存放当天结束的数据,设计一个时间非常大的分区,比如3000-12-31 ,存放截至当前未结束的数据。由于每天将当天结束的数据归档至当天分区中,时间非常大的分区数据量不会很大, ETL 性能较好;并且无存储的浪费, 对于业务实体的某具体实例,在该表的全量数据中唯一。比如对于交易订单,在交易累积快照事实表中唯一。
针对第三种方式,可能存在极特殊情况,即业务系统无法标识业务实体的结束时间:
- 使用相关业务系统的业务实体的结束标志作为此业务系统的结束标志。比如针对物流订单,可以使用交易订单。理论上, 交易订单完结了,则物流订单已经完结。
- 和前端业务系统确定口径或使用前端归档策略。累积快照事实表针对业务实体一般是具有较短生命周期的,和前端业务系统确定口径,确定从业务实体的产生到消亡的最大间隔。
维度表
规范化与反规范化
规范化是指使用一系列范式设计数据库的过程,其目的是减少数据冗余,增强数据的一致性。通常情况下,规范化之后,一张表的字段会拆分到多张表。
反规范化是指将多张表的数据冗余到一张表,其目的是减少join操作,提高查询性能。在设计维度表时,如果对其进行规范化,得到的维度模型称为雪花模型,如果对其进行反规范化,得到的模型称为星型模型。
数据仓库系统的主要目的是用于数据分析和统计,所以是否方便用户进行统计分析决定了模型的优劣。采用雪花模型,用户在统计分析的过程中需要大量的关联操作,使用复杂度高,同时查询性能很差,而采用星型模型,则方便、易用且性能好。所以出于易用性和性能的考虑,维度表在设计时一般存在大量反规范化。
维度变化
维度属性通常不是静态的,而是会随时间变化的,数据仓库的一个重要特点就是反映历史的变化,所以如何保存维度的历史状态是维度设计的重要工作之一。保存维度数据的历史状态,通常有以下两种做法,分别是全量快照表和拉链表。
全量快照表
离线数据仓库的计算周期通常为每天一次,所以可以每天保存一份全量的维度数据。这种方式的优点和缺点都很明显。优点是简单而有效,开发和维护成本低,且方便理解和使用。缺点是浪费存储空间,尤其是当数据的变化比例比较低时。
拉链表
拉链表,记录每条信息的生命周期,一旦一条记录的生命周期结束,就重新开始一条新的记录,并把当前日期放入生效开始日期。如果当前信息至今有效,在生效结束日期中填入一个极大值(如9999-12-31 )。
| 用户ID | 姓名 | 手机号码 | 开始日期 | 结束日期 |
|---|---|---|---|---|
| 0001 | 李四 | 136**1111 | 2022-01-01 | 2022-01-02 |
| 0001 | 李四 | 137**2222 | 2022-01-03 | 2022-01-09 |
| 0001 | 李四 | 138**1234 | 2022-01-10 | 9999-12-31 |
拉链表适合于:数据会发生变化,但是变化频率并不高的维度(即:缓慢变化维) 。比如:用户信息会发生变化,但是每天变化的比例不高。如果数据量有一定规模,按照每日全量的方式保存效率很低。 比如:1亿用户*365天,每天做一份全量用户信息表效率就会很低。
拉链表在具体使用过程中可以通过:生效开始日期<=某个日期 且 生效结束日期>=某个日期 ,得到某个时间点的数据全量切片:
SELECT * FROM user_info WHERE start_date <= '2022-01-01' AND end_date >= '2022-01-01'