维度设计基础
维度是维度建模的基础和灵魂。维度建模中,将度量称为“事实”,将环境描述为“维度”,维度是用于分析事实所需要的多样环境。维度和维度属性是维度的两个核心概念,如何构建维度的属性是维度设计中需要关注的。
维度具有层次结构,维度中的一些描述属性以层次方式或一对多方式相互关联。比如淘宝商品维度,有卖家、类目、品牌等父层次。对于层次结构,是采用雪花模式进行规范化处理还是将维度的属性层次合并到单个维度中进行反规范化处理,需要进行取舍。
最后,介绍维度一致性及其重要意义。
维度的基本概念
- 维度具有层次结构,维度中的一些描述属性以层次方式或一对多方式相互关联.比如淘宝商品维度,又卖家、类目、品牌等父层次.对于层次结构,是采用雪花模型进行规范化出来还是采用新型模型将维度属性层次合并到单个维度中进行反规范化出来,需要进行取舍
- 维度是维度建模的基础和灵魂。维度建模中,将度量称为“事实”,将环境描述为“维度”,维度是用于分析事实所需要的多样环境。例如,在分析交易过程时,可以通过买家、卖家、商品和时间等维度描述交易发生的环境。
- 维度所包含的表示维度的列,称为维度属性。维度属性是查询约束条件、分组和报表标签生成的基本来源,是数据易用性的关键。例如在查询请求中,我们取某类目的商品,取正常状态的商品等,是通过约束商品类目属性和商品状态属性来实现的;例如,我们统计淘宝不同商品类目每日成交金额,是通过商品维度的类目属性进行分组的;例如,我们在报表中看到的类目、BC类型(B指天猫、C指集市)等,都是维度属性。所以维度的作用一般有以下几点,查询约束、分类汇总以及排序等。
- 如何获取维度或它们的属性?如上面提到的,一方面,可以在报表中获取;另一方面,可以在和业务人员的交谈中能够发现维度或它们的属性。因为它们经常出现在查询或报表请求中的"按照"(by)语句内。例如,用户需要"按照"月份和产品来查看销售情况。用户描述其业务的自然方法应该作为维度或维度属性包括在维度模型中。
- 维度使用主键标示其唯一性,主键也是确保与之相连的任何事实表之间存在引用完整性的基础。主键有两种方式:代理键和自然键,它们都是用于标示某维度的具体值。但代理键是不具有业务含义的键,一般用于处理缓慢变化维度;自然键是具有业务含义的键。如商品,在ETL过程中,对商品维表的每一行,可以生成一个唯一的代理键与之对应;商品本身的自然键可能是商品ID等。其实对于前台应用系统,商品ID是代理键;而对于数据仓库系统来说,商品ID则属于自然键。
维度的基本设计方法
维度的设计过程就是确定维度属性的过程,如何生成维度属性、生成的维度属性的优劣,决定了维度使用的方便性,成为数据仓库易用性的关键。正如Kimball所说,数据仓库的能力直接与维度属性的质量和深度成正比。
下面,以淘宝的商品维度为例进行维度设计方法的进行详细说明。
- 第一步:选择维度或新建维度。作为维度建模的核心,我们在企业级的数据仓库中必须保证维度的唯一性。以淘宝商品维度为例,我们有且只允许有一个维度定义。(这里说的只有一个维度定义,就是对于商品来说,只有一个维表来描述商品的维度)
- 第二步:确定主维度表。此处的主维度表一般是ODS表,直接同步自业务系统。以淘宝商品维度为例,s_auction_auctions是从前台商品中心系统同步的商品表,此表即是主维度表。
- 第三步:确定相关维度表。数据仓库是业务源系统的数据整合,不同业务系统或者同一业务系统中的表之间存在关联性。根据对业务的梳理,确定哪些表和主维度表存在关联关系,并选择其中的某些表用于生成维度属性。以淘宝商品维度为例,根据业务逻辑梳理,可以得到商品和类目、SPU、卖家、店铺等维度存在关联关系。
- 第四步:确定维度属性。本步骤主要包括两个阶段,第一阶段是主维度表中选择维度属性或生成新的维度属性;第二阶段是从相关维度表中选择维度属性或生成新的维度属性。以淘宝商品维度为例。以淘宝商品维度为例,从主维度表(s_auction_auctions)和类目、SPU、卖家、店铺等相关维度表选择维度属性或生成新的维度属性。
确定维度属性的几点TIPS:
- (1)维度属性的作用和重要性已经多次提及,尽可能生成丰富的维度属性。比如淘宝商品维度约有近百个维度属性,为下游的数据统计、分析、探查提供了良好的基础。
- (2)尽可能多给出包括一些富有意义的文字性描述,属性不应该是编码而应该是真正的文字。阿里巴巴维度建模中,一般是编码和文字同时存在,比如商品维度中的商品ID和商品标题,类目ID和类目名称等。ID一般用于不同表之前的关联,而名称一般用于报表标签。
- (3)区分数值型属性和事实。
- 数值型字段是作为事实还是维度属性,可以参考字段的一般用途。如果通常是用于查询约束条件或分组统计,则是作为维度属性;如果通常是用于参与度量的计算,则是作为事实。比如商品价格,可以用于查询约束条件或统计价格区间的商品数量,此时是作为维度属性使用;也可以用于统计某类目下商品的平均价格,此时是作为事实使用。另一方面,如果数值型字段是离散值,则作为维度属性存在的可能性较大;如果数值型字段是连续值,则作为度量存在的可能性较大,但并不绝对,需要同时参考字段的具体用途。
- (4)尽量沉淀出通用的维度属性
- 有些维度属性获取需要进行比较复杂的逻辑处理,有需要通过多表关联得到,也有单表的不同字段混合处理得到,或者对单表的某个字段进行解析得到。此时,需要将尽可能多的通用的维度属性进行沉淀。一方面,可以提高下游使用的方便性,减少复杂度;另一方面,避免下游使用解析时由于各自逻辑不同而导致的口径不一致。例如,淘宝商品的property字段,使用key:value方式存储多个商品属性。商品品牌就存存储在此字段中,而商品品牌是重要的分组统计和查询约束的条件,所以需要将品牌解析出来,作为品牌属性存在。例如,商品是否在线,即淘宝网站是否可以查看到此商品,是重要的查询约束的条件,但是无法直接获取需要进行加工,加工逻辑是:商品状态0和1且商品上架时间小于等于当前时间,则是在线商品,否则是非在线商品。所以需要封装商品是否在线的逻辑作为一个单独的属性字段。
维度的层次结构
- 维度中的一些描述属性以层次方式或一对多方式相互关联,可以被理解为包含连续主从关系的属性层次。层次的最底层代表维度中描述最低级别的详细信息,最高层次代表最高级别的概要信息。维度常常有多个这样的嵌入式层次结构。比如淘宝商品维度,有卖家、类目、品牌等。商品属于类目、类目属于行业,其中类目的最低级别是叶子类目、叶子类目属于二级类目、二级类目属于一级类目。
- 在属性的层次结构中进行钻取是数据钻取的方法之一。关于钻取的定义,我们不做介绍,可以参考相关书籍。通过具体的例子,我们来看如何在层次结构中进行钻取。
- 假设我们已有一个淘宝交易订单创建事实表。现在统计2015年双11的下单GMV,我们得到一行记录;沿着层次向下钻取,添加行业,得到行业实例个数的记录数;继续沿着层次向下钻取,添加一级类目,得到一级类目实例个数的记录数。可以看到,通过向报表中添加连续的维度细节级别实现在层次结构中进行钻取。
规范化和反规范化
- 当属性层次被实例化为一系列维度,而不是单一的维度时,此模式被称为雪花模式。大多数联机事务处理系统(OLTP)的底层数据结构在设计时采用此种规范化技术,通过规范化处理将重复属性移至其自身所属的表中,删除冗余数据。
- 此种方法用在OLTP系统中可以有效避免数据冗余导致的不一致性。比如在OLTP系统中,存在商品表和类目表,且商品表中冗余有类目表的属性字段,假设对某类目进行更新,则必须更新商品表和类目表,且由于商品和类目是一对多的关系,商品表可能每次需要更新几十万甚至上百万条记录,这是不合理的。而对于联机分析处理系统(OLAP),数据是稳定的,不存在OLTP系统中存在的问题。
- 对于淘系商品维度,如果采用雪花模式进行规范化处理,将表现为如下形式:
- 将维度的属性层次合并到单个维度中的操作称为反规范化。分析系统的主要目的是用于数据分析和统计,如何更方便用户进行统计分析决定了分析系统的优劣。采用雪花模式,用户在统计分析的过程中需要大量的关联操作,使用复杂度高,同时查询性能很差;采用反规范化处理,方便易用且性能好。
对于淘宝商品维度,如果采用反规范化处理,将表现为如下形式: - 如上所述,从用户的角度来看这简化了模型,并且使数据库查询优化器的连接路径比完全规范化的模型简化许多。反规范化的维度仍包含与规范化模型同样的信息和关系,从分析角度来看,没有丢失任何信息,但复杂性降低了。
- 采用雪花模式,除了可以节约一部分存储,对于OLAP系统来说没有其它效用。而现阶段存储的成本非常低。基于易用性和性能考虑,维度表一般是很不规范化的。实际应用中,几乎总是使用维度表的空间来换取简明性和查询性能。
一致性维度和交叉探查
- 构建企业级的数据仓库不可能一蹴而就,一般采用迭代式的构建过程。但单独构建存在的问题是形成独立型数据集市,导致严重的不一致性。Kimball的数据仓库总线架构提供了一种分解企业级数据仓库规划任务的合理方法。通过构建企业范围内一致性的维度和事实来构建总线架构。
- 数据仓库总线架构重要基石之一就是一致性维度。在针对不同数据域进行迭代构建或并行构建时,存在很多需求是对于不同数据域的业务过程或者同一数据域的不同业务过程合并在一起观察;比如对于日志数据域,我们统计了商品维度的最近一天访问PV和UV数;对于交易数据域,我们统计了商品维度的最近一天的下单GMV。现在我们将不同数据域的商品的事实合并在一起进行数据探查,如计算转化率等,我们称为交叉探查。
- 如果不同数据域计算过程使用的维度不一致,就会导致交叉探查存在问题。当存在重复的维度,但维度属性或维度属性的值不一致,会导致交叉探查无法进行或交叉探查结果错误。如上所示,假设日志数据域统计商品维度的最近一天PV和UV使用的商品维度1,交易数据域统计商品维度使用的是商品维度2。商品维度1包含维度属性BC类型,而商品维度2无此属性,则无法在BC类型上进行交叉探查;商品维度1商品上架时间这一维度属性时间格式是字符串格式yyyy-MM-dd HH:mm:ss,商品维度2商品上架时间这一维度属性时间格式是Unix timestamp,进行交叉探查时如果需要根据商品上架时间做限制,则复杂性较高;商品维度1不包含阿里旅行的商品,商品维度2包含全部淘系商品,探查也无法进行;还有很多种形式的不一致,不再一一列举,基本可以划分维度格式和内容的不一致这两种类型。
- 我们对维度不一致进行了详细分析,下面我们总结一下维度一致性的几种表现形式:
- (1)共享维度表,比如在阿里巴巴的数据仓库中,商品、卖家、买家、类目等维度,有且只有一个。所以基于这些公共维度进行的交叉探查,不会存在任何问题。
- (2)一致性上卷,其中一个维度的维度属性是另一个维度的维度属性的子集,且两个维度的公共维度属性结构和内容相同。比如在阿里巴巴的商品体系中,有商品维度和类目维度,其中类目维度的维度属性是商品维度的维度属性的子集,且有相同维度属性和维度属性值。这样基于类目维度进行不同业务过程的探查也不会存在任何问题。
- (3)交叉属性,两个维度具有部分相同的维度属性。比如在商品维度中具有类目属性,在卖家维度中具有主营类目属性,两个维度具有相同的类目属性,则可以在相同的类目属性上进行不同业务过程的交叉探查。
维度设计高级主题
- 集成是数据仓库的四个特性中最重要的一点,维度设计中需要考虑如何集成来自应用的大量分散的操作型环境的数据。
- 维度有两种拆分方式,水平拆分和垂直拆分。水平拆分通常基于维度类别或类型进行细分。
- 垂直拆分通常基于扩展性、产出时间、易用性等方面的进行考虑。主维度表存放稳定、产出时间早、热度高的属性;从维度表存放变化较快、产出时间晚、热度低的属性。
- 阿里巴巴历史截止当前的淘系商品约有几百亿的记录,在数据仓库占用约18T的存储。面对如此庞大的数据量,如何设计模型、如何降低存储、如何让下游方便获取数据,成为必须要解决的问题。数据归档是解决此问题的有效方法之一
维度整合
我们先来看下数据仓库的定义:数据仓库是一个面向主题的、集成的、非易失的且随时间变化的数据集合,用来支持管理人员的决策。其中集成是数据仓库的四个特性中最重要的一点。
数据仓库的重要数据来源是大量的、分散的面向应用的操作型环境。不同的应用在设计过程中,可以自由决策,主要满足本应用的需求,很少会考虑和其它系统进行数据集成。应用的差异具体表现在如下几个方面:
(1)应用在编码、命名习惯、度量单位等方面会存在很大差异。比如不同应用对于用户的性别编码不同:有0和1,有F和M等;不同应用用户ID含义相同但字段名称不同:有user,有user_id等;不同应用对于金额的度量单位不同:有元,有分等等。
(2)应用基于性能和扩展性的考虑,或者随技术架构的演变,或者随业务的发展,采用不同的物理实现。拆分至不同类型数据库,部分数据采用关系型数据库存储(如Oracle、Mysql等),部分数据采用NoSQL数据库存储(如Hbase、Tair等)。拆分成同一类型数据库中多张的物理表,比如淘宝商品,有商品主表和商品扩展表,商品主表存商品基本信息;商品扩展表存储商品特殊信息,如不同产品线定制化的信息等;比如淘宝会员,有会员主表和会员扩展表,会员主表存用户基本信息;会员扩展表存储用户扩展信息,如用户的各种标签信息等。
所以数据由面向应用的操作型环境进入数据仓库后,需要进行数据集成。将面向应用的数据转换为面向主题数据仓库数据,本身就是一种集成。具体体现在如下几个方面:
(1)命名规范的统一。表名、字段名等统一;
(2)字段类型的统一。相同和相似字段的字段类型的统一;
(3)公共代码及代码值的统一。公共代码及标志性字段的数据类型、命名方式等的统一;
(4)业务含义相同的表的统一。主要依据高内聚、低耦合的理念,在物理实现中,将业务关系大、源系统影响差异小的进行整合;业务关系小、源系统影响差异大的进行分而置之。通常有如下集成方式:
a、采用主从表的设计方式,两表或多表都有的字段放在主表中(主要基本信息),从属信息分别放在各自的从表中。对于主表中的主键,要么采用复合主键,源主键和系统或表区别标志;要么采用唯一主键,“源主键||系统或表区别标志”生成新的主键。通常建议采用复合主键的方式。
b、直接合并,共有信息和个性信息都放在同一个表中。如果表字段的重合度较低,会出现大量空值,对于存储和易用性会有影响,需谨慎选择。
c、不合并,源表的表结构及主键等差异很大,无法合并,使用数据仓库里的多个表存放各自数据。
维度表的整合涉及的内容和上面介绍的几个方面相同,下面重点看一下表级别的整合,有两种表现形式。
第一种是垂直整合,即不同的来源表包含同一数据集,只是存储的信息不同。比如淘宝会员在源系统有多张表,会员基础信息表、会员扩展信息表、淘宝会员等级信息表、天猫会员等级信息表。这些表都属于会员相关信息表,依据维度设计方法,尽量整合至会员维度模型中,丰富其维度属性。
豆包:垂直整合:
假设一个电商企业有多个业务系统,其中关于客户信息在源系统中有三张表,分别是客户基础信息表(包含客户姓名、性别、出生日期等)、客户联系方式表(包含手机号码、电子邮箱等)、客户消费等级信息表(包含普通客户、黄金客户、白金客户等等级标识)。
在进行垂直整合时,会将这三张表整合到一个客户维度模型中,比如新的客户维度表可能包含客户姓名、性别、出生日期、手机号码、电子邮箱、客户等级等属性。这样,当进行数据分析时,比如分析不同等级客户的消费行为,就可以从一个整合后的表中获取全面的客户信息,而不需要从多个表中进行关联查询,提高了查询效率和数据分析的便利性。
(这个感觉就是每一行记录是唯一的,然后把多个表联系起来补充维度字段)
第二种是水平整合,即不同的来源表包含不同的数据集,不同子集之间无交叉,亦可以存在部分交叉,比如针对蚂蚁金服数据仓库,其采集的会员数据有淘宝会员、1688会员、国际站会员、支付宝会员等,是否需要将所有的会员整合成一张会员表?如果进行整合,首先需要考虑各个会员体系是否有交叉,如果存在交叉,则需要去重;如果不存在交叉,不同子集的自然键是否存在冲突,如果不冲突,可以考虑将各子集的自然键作为整合后的表的自然键;另外一种方式是设置超自然键,来源+各子集的自然键加工成一个字段作为超自然键。在阿里巴巴,常用的方式是将来源各子集的自然键作为联合主键的方式,并且在物理实现时,将来源字段作为分区字段。
豆包:水平整合:
以一个综合性的金融服务平台为例,该平台有多个会员体系,包括银行会员、证券会员、基金会员等。这些不同来源的会员表包含不同的数据集,可能部分属性相同,但整体上无交叉或存在部分交叉。
如果不进行整合,当需要分析平台上所有会员的综合行为时,就需要分别查询各个会员表,然后进行复杂的合并操作。而进行水平整合后,如果各个会员体系没有交叉,且自然键不冲突,比如银行会员的自然键是银行卡号,证券会员的自然键是证券账号,基金会员的自然键是基金账户号,那么可以考虑将各子集的自然键作为整合后的表的自然键,新的会员表可能包含会员来源(银行、证券、基金等)、自然键(银行卡号、证券账号、基金账户号等)、会员其他属性等。
如果存在交叉,比如某个客户同时是银行会员和证券会员,就需要进行去重处理,确保整合后的表中每个会员是唯一的。或者设置超自然键,如 “来源 + 各子集的自然键” 加工成一个字段作为超自然键,比如 “银行_123456789” 表示银行会员的卡号为 123456789 的会员。
(之前那个dim应该就是属于水平整合,需要考虑去重)
有整合就有拆分,到底是做整合还是拆分,都有哪些因素决定。我们在下面两节讨论维度的水平拆分和垂直拆分。
水平拆分
维度通常可以按照类别或类型细分。比如淘系商品表,根据业务线或行业等可以对商品进行细分,比如淘宝的商品、天猫的商品、1688的商品、阿里去啊的商品、淘宝海外的商品、天猫国际的商品等。不同分类的商品,其维度属性可能相同,也可能不同。比如阿里去啊的商品和普通的淘系商品,都属于商品,都有商品价格、标题、类型、上架时间、类目等维度属性,但是阿里去啊商品除了有这些公共属性,还有酒店、景点、门票、旅行等自己独特的维度属性。
如何设计维度?针对此问题,主要解决方案有两种,方案1是将维度的不同分类实例化为不同的维度,同时在主维度中保存公共属性;方案2是维护单一维度,包含所有可能的属性。
(这里的方案1就是把淘系商品里面的阿里去啊给拆出来,因为其具有较多异于其他维度的属性,方案2就是不拆分,维护一个淘系商品维度)
选择哪种方案?数据模型设计过程中需要考虑的因素很多,基本不可能满足各个特性指标的最优化。在设计过程中需要重点考虑以下三点原则:
(1)扩展性:指在源系统、业务逻辑变化的时候,能通过少的成本快速扩展模型,保持核心模型的相对稳定性。软件工程中高内聚、低耦合的思想是重要的指导方针之一。
(2)效能:性能和成本方面的平衡。通过牺牲一定的存储成本,达到性能和逻辑上的优化。
(3)易用性:模型可理解性高、访问复杂度低。用户能够方便的从模型中找到对应的数据表,并能够方便查询和分析。
根据数据模型设计思想,在做维度的水平拆分时,主要考虑如下两个依据:
第一个依据是,维度的不同分类的属性差异情况。当维度属性随类型变化较大时,将所有可能的属性建立在一张表中是不切合实际和没有必要的,此时建议采用方案1。定义一个主维度存放公共属性;同时定义多个子维度,除包含公共属性外,还包含各自子维度的特殊属性。比如在阿里巴巴数据仓库维度体系中,依据此方法,构建了商品维度、阿里去啊商品维度等。公共属性一般比较稳定,通过核心的商品维度,保证了核心维度的稳定性;通过扩展子维度的方式,保证了模型的扩展性。
另外一个重要依据是,业务的关联程度。两个相关性较低的业务,耦合在一起弊大于利,对于模型的稳定性和易用性影响较大。比如阿里巴巴数据仓库维度体系中,我们将淘系商品和1688商品构建两个维度。虽然淘系和1688商品底层技术实现上是统一的,但属于不同的业务BU,业务各自发展;在数据仓库层面,淘系和1688属于不同的数据集市,一般不会相互调用,业务分析人员一般只针对本数据集市进行统计分析。如果设计成一个维度,由于不同BU业务各自发展,1688业务变更,此维度需要变更,淘宝业务变更亦然,稳定性很差;易用性方面,会给数据使用方造成困扰。
垂直拆分
在第一章维度设计中,我们提到维度是维度建模的基础和灵魂,维度属性的丰富程度直接决定了数据仓库的能力。在做维度设计时,依据维度设计的原则,尽可能丰富了维度属性,同时进行了反规范化处理。对于具体实现时可能存在问题,一方面是水平拆分中提到的,维度属性由于维度分类的不同而存在特殊属性,通过水平拆分的方式解决此问题。
另一方面的问题是某些维度属性的来源表产出时间较早,而某些属性的来源表产出时间较晚;或者某些维度属性的热度高、使用频繁,而某些维度属性热度低、较少使用;或者某些维度属性经常变化,而某些属性比较稳定。水平拆分中提到的模型设计的三个原则同样适用于解决此问题。
基于扩展性、产出时间、易用性等方面的考虑,设计主从维度。主维度表存放稳定、产出时间早、热度高的属性;从维度表存放变化较快、产出时间晚、热度低的属性。比如在阿里巴巴数据仓库中,我们设计了商品主维度和商品扩展维度。其中商品主维度在每日的01:30左右产出,而商品扩展维度由于冗余了产出时间较晚的商品品牌和标签信息,在每日的03:00左右产出;另外,商品扩展维度冗余了库存等变化较快的数据,对于主维度进行缓慢变化的处理较为重要。通过存储的冗余和计算成本的增加,实现了商品主模型的稳定和产出时间提前,对于整个数据仓库的稳定和下游应用的产出都有较大意义。
豆包:
水平拆分和垂直拆分主要有以下区别:
一、拆分方向不同
1. 水平拆分
- 是按照维度的不同分类进行拆分,主要依据是维度属性随类型变化情况以及业务的关联程度。例如将淘系商品和阿里去啊商品拆分为不同的维度,或者将淘系商品和 1688 商品构建为两个维度。这种拆分是在不同的维度类别之间进行的,类似于在同一平面上按照不同方向进行划分。
2. 垂直拆分
- 是基于维度属性自身的特点进行拆分,主要考虑某些维度属性的来源表产出时间、属性热度以及变化频率等因素。例如将商品主维度和商品扩展维度进行拆分,主维度表存放稳定、产出时间早、热度高的属性,从维度表存放变化较快、产出时间晚、热度低的属性。这种拆分是在同一维度的不同属性层次上进行的,类似于在垂直方向上对一个整体进行分层。
二、解决的问题重点不同
1. 水平拆分
- 重点解决不同类型维度之间的差异和业务关联度低的问题。当不同分类的商品维度属性差异较大时,避免将所有属性放在一张表中导致的不切实际和难以维护的情况。同时,对于相关性较低的业务,通过拆分保证模型的稳定性和易用性,避免一个业务的变更影响到其他不相关业务的维度。
2. 垂直拆分
- 重点解决同一维度中不同属性的产出时间、热度和变化频率差异带来的问题。通过主从维度的设计,保证主维度的稳定和提前产出,满足对变化较快、热度低或产出时间晚的属性的存储需求,同时也有利于对主维度进行缓慢变化的处理,提高整个数据仓库的稳定性和下游应用的产出效率。
三、对模型的影响不同
1. 水平拆分
- 会产生多个相对独立的维度表,每个维度表对应不同的维度分类,这些维度表之间通过公共属性进行一定程度的关联。例如商品维度和阿里去啊商品维度,它们都有一些公共属性,但也有各自独特的属性。这种拆分方式使得模型在处理不同类型的业务或商品时更加灵活,但也增加了数据查询和分析时的复杂性,需要在不同的维度表之间进行切换和关联。
2. 垂直拆分
- 产生主从维度表,主维度表和从维度表共同描述同一个实体,但侧重点不同。主维度表关注稳定、重要的属性,从维度表补充其他属性。这种拆分方式在一定程度上保持了数据的完整性,同时通过主从关系的设计,使得在查询和分析时可以根据需要选择不同的维度表,提高了模型的可扩展性和易用性。
历史归档
阿里巴巴历史截止当前的淘系商品约有几百亿的记录,在数据仓库占用约18T的存储。面对如此庞大的数据量,如何设计模型、如何降低存储、如何让下游方便获取数据,成为必须要解决的问题。是否存在历史数据,前台已经不再使用。答案是肯定的,如此庞大的数据,现有的技术架构也很难处理。前台有一套数据归档的策略,比如将商品状态为下架或删除的且最近31天未更新的商品归档至历史库;具体逻辑根据不同业务BU有不同的算法,且有特殊的规则。
数据仓库中,理所当然可以借用前台数据库的归档策略,定期将历史数据归档至历史维表。在实践中,阿里巴巴数据仓库设计商品维度表和历史商品维度表,每天将历史数据归档至历史商品维度表。关于归档策略的选择,可以有以下几种方式:
(1)同前台归档策略,将前台归档算法在数据仓库中实现,定期将历史数据进行归档。但存在一些问题,一方面是前台归档策略复杂,实现成本较高;另外一方面,前台归档策略可能会经常变化,会导致数据仓库归档算法也要随之变化,维护和沟通成本较高。此方法适用于同前台归档策略逻辑较为简单,且变更不频繁的情况。
(2)同前台归档策略,但采用数据库变更日志的方式。对于如此庞大的数据,阿里巴巴采用的数据抽取策略一般是通过数据库binlog解析获取每日增量,通过增量merge全量的方式获取最新全量数据。可以使用增量日志的删除标记,作为前台数据归档的标记。通过此标记对数据仓库的数据进行归档。此方法不需要关注前台归档策略,简单易行。但对前台应用的要求是数据库的物理删除只有在归档时才执行,应用中的删除只是逻辑删除。
(3)数据仓库自定义归档策略。可以将归档算法用简单直接的方式实现,但此方法的原则是尽量比前台应用晚归档、比前台应用少归档。避免数据仓库中已经归档的数据再次更新的情况出现。
如果技术条件允许,能够解析数据库binlog日志,建议使用归档策略2,规避前台算法。具体可以根据自身数据仓库的实际情况进行选择。
维度变化
数据仓库的重要特点之一是反应历史变化,所以如何处理维度的变化是维度设计的重要工作之一。
在Kimball的理论中,有三种缓慢变化的处理方式以及衍生出来的几种其他处理方式。阿里巴巴在处理缓慢变化维度方面,有自己独特的特点。比如对于数据量相对可控的表采用快照的方式,通过存储成本换取计算成本的降低;对于数据量巨大的表则引入极限存储的概念,在保证易用性的同时,通过计算成本换取存储成本的降低。
最后,简单介绍微型维度在避免维度快速增长方面的作用
缓慢变化维
数据仓库的重要特点之一是反应历史变化,所以如何处理维度的变化是维度设计的重要工作之一。缓慢变化维的提出是因为在现实世界中,维度的属性并不是静态的,它会随着时间的流失发生缓慢的变,这一现象称为缓慢变化的维度,简称缓慢变化维。与数据增长较为快速的事实表相比,维度变化相对缓慢。
某些情况下,保留历史数据没有什么分析价值;某些情况下,保留历史数据将会起到至关重要的作用。在Kimball的理论中,有三种缓慢变化的处理方式,下面通过简单的实例进行说明,具体细节请翻阅Kimball的相关书籍。
类型1:重写维度值。采用此种方式,不保留历史,始终取最新数据。比如,商品所属的类目于2015年11月16日由类目1变成类目2。采用类型1处理方式,变化前后的数据记录如下。
表1:变化前商品表和订单表
商品Key | 商品ID | 商品标题 | 所属类目 | 其它属性 |
---|---|---|---|---|
1000 | item1 | title1 | 类目1 | ... |
订单Key | 日期Key | 商品Key | 交易金额 | 其它事实 |
---|---|---|---|---|
9000 | 2015-11-11 | 1000 | 103.00 | ... |
表2:变化后商品表和订单表
商品Key | 商品ID | 商品标题 | 所属类目 | 其它属性 |
---|---|---|---|---|
1000 | item1 | title1 | 类目2 | ... |
订单Key | 日期Key | 商品Key | 交易金额 | 其它事实 |
---|---|---|---|---|
9000 | 2015-11-11 | 1000 | 103.00 | ... |
9001 | 2015-11-16 | 1000 | 89.00 | ... |
类型2:插入新的维度行。采用此种方式,保留历史,维度值变化前的事实和过去的维度值关联,维度值变化后的事实和当前的维度值关联。同上面的例子,采用类型2处理方式,变化前的数据同上,变化后的数据记录如下。
表3:变化后商品表和订单表
商品Key | 商品ID | 商品标题 | 所属类目 | 其它属性 |
---|---|---|---|---|
1000 | item1 | title1 | 类目1 | ... |
1001 | item1 | title1 | 类目2 | ... |
订单Key | 日期Key | 商品Key | 交易金额 | 其它事实 |
---|---|---|---|---|
9000 | 2015-11-11 | 1000 | 103.00 | ... |
9001 | 2015-11-16 | 1001 | 89.00 | ... |
类型3:添加维度列。类型2不能将变化前后记录的事实归一为变化前的维度或者归一为变化后的维度,比如根据业务需求,需要将11月份的交易金额全部统计到类目2上,类型2无法实现。针对此问题,采用类型3处理方式,保留历史,可以使用任何一个属性列。同上面的例子,采用类型3处理方式,变化前后的数据记录如下。通过变化后的商品表和订单表关联,可以将根据不同的业务需求,将11月份的交易金额全部统计到类目2或类目1上。
豆包:
在上述背景中,类型 2 的处理方式是插入新的维度行。当商品的维度值发生变化时,会新增一个商品维度行来记录变化后的状态,同时原有的事实表中的数据依然与变化前的商品维度行关联。
以例子来说,商品原本属于类目 1,后来变为类目 2。在类型 2 中,变化前的订单和变化前的商品维度行(类目 1)关联,变化后的订单和变化后的商品维度行(类目 2)关联。
但是如果业务需求是要将 11 月份的交易金额全部统计到类目 2 上,类型 2 无法实现这一需求。因为在类型 2 中,已经发生的事实(比如变化前属于类目 1 时的交易)是与变化前的维度行关联的,无法直接将这些交易金额统计到变化后的类目 2 上。也就是说,类型 2 只是忠实地记录了历史状态下的关联关系,不能根据新的业务需求将所有历史事实都归到变化后的维度上。
例如,订单表中有一个订单在变化前发生,其关联的商品维度行是类目 1。如果现在要按照新业务需求把这个订单的交易金额算到类目 2 中,在类型 2 的模式下无法直接做到,因为订单已经与特定的商品维度行(类目 1)关联,不能随意更改这种关联关系来满足将交易金额全部统计到类目 2 的业务需求。
表4:变化前商品表和订单表
商品Key | 商品ID | 商品标题 | 所属新类目 | 所属旧类目 | 其它属性 |
---|---|---|---|---|---|
1000 | item1 | title1 | 类目1 | 类目1 | ... |
订单Key | 日期Key | 商品Key | 交易金额 | 其它事实 |
---|---|---|---|---|
9000 | 2015-11-11 | 1000 | 103.00 | ... |
表5:变化后商品表和订单表
商品Key | 商品ID | 商品标题 | 所属新类目 | 所属旧类目 | 其它属性 |
---|---|---|---|---|---|
1000 | item1 | title1 | 类目2 | 类目1 | ... |
订单Key | 日期Key | 商品Key | 交易金额 | 其它事实 |
---|---|---|---|---|
9000 | 2015-11-11 | 1000 | 103.00 | ... |
9001 | 2015-11-16 | 1000 | 89.00 | ... |
例如,对于一个在变化前发生的订单,其关联的商品在商品表中有对应的记录,同时记录了 “所属旧类目” 为类目 1 和 “所属新类目” 为类目 2。当要统计到类目 2 时,可以通过筛选 “所属新类目” 列的值为类目 2 的商品,从而将这个原本在变化前发生的订单也纳入到类目 2 的统计中。所以类型 3 能够满足将交易金额根据业务需求统计到特定类目上的要求。
对于选择哪种方式处理缓慢变化维,根据业务的需求来选择适当的处理方式即可,并没有一个完全正确的答案。比如根据商品所属的类目统计淘宝2015年11月的成交额,商品所属的类目于2015年11月16日由类目1变成类目2,假设业务需求方不关心历史,将所有的成交额都统计到最新的类目2上,则不需要保存历史数据;假设类目1属于某个业务部门,类目2属于另一个业务部门,不同业务部门需要统计各自的业绩,则需要保留历史数据。
快照维表
第一章维度的基本概念中介绍了自然键和代理键的定义,在Kimball的维度建模中,必须使用代理键作为每个维度表的主键,用于处理缓慢变化维度。比如上面的例子,商品所属的类目于2015年11月16日由类目1变成类目2。采用Kimball的类型2的处理方式如下:
商品Key | 商品ID | 商品标题 | 所属类目 | 其它属性 |
---|---|---|---|---|
1000 | item1 | title1 | 类目1 | ... |
1001 | item1 | title1 | 类目2 | ... |
但在阿里巴巴数据仓库建设的实践过程中,虽然我们使用的是Kimball的维度建模的理论,但实际并未使用代理键。我们是如何处理缓慢变化维度,如何记录变化历史的呢?为什么不使用代理键呢?
首先看一下为什么不使用代理键。第一点原因是,由于阿里巴巴数据量庞大,使用的是阿里巴巴自主知识产权的分布式计算平台ODPS。对于分布式计算系统,不存在事务的概念,对于每个表的记录生成全局唯一的稳定的代理键难度很大,此处稳定指某条记录每次生成的代理键都相同。第二点原因是,使用代理键会大大增加ETL的复杂性,对ETL任务的开发和维护成本很高。
豆包
一、对于分布式计算系统生成全局唯一稳定代理键难度大的例子
假设在阿里巴巴的数据仓库中,有一个用户信息表,存储着数以亿计的用户数据。在传统的集中式数据库系统中,可以通过数据库自身的事务机制和自增主键等方式相对容易地为每条记录生成唯一的代理键。但在分布式计算系统中,由于数据分布在多个节点上,没有事务的概念来保证全局的一致性。
例如,多个节点同时处理不同批次的用户数据,节点 A 在处理一批数据时为某个用户分配了代理键 1001,但与此同时节点 B 在处理另一批数据时也可能为另一个不同的用户分配了代理键 1001,因为没有一个统一的事务机制来协调全局的代理键分配,导致很难生成全局唯一且稳定的代理键。
下面接着讨论不使用代理键如何处理缓慢变化维度。阿里巴巴数据仓库实践中处理缓慢变化维度的方法是快照方式。数据仓库的计算周期一般是每天一次,基于此周期,处理维度变化的方式就是每天一份全量快照。比如商品维度,每天保留一份全量商品快照数据。任意一天的事实均可以取到当天的商品信息,也可以取到最新的商品信息,通过限定日期,采用自然键进行关联即可。此方法既有优势亦有弊端。
优势主要有以下两点:
(1)处理缓慢变化维度的方式,简单而有效,开发和维护成本低。
(2)使用方便,理解性好。数据使用方只需要限定日期即可取到当天的快照数据。任意一天的事实快照和任意一天的维度快照通过维度的自然键进行关联即可。
弊端主要体现在存储的极大浪费。比如某维度,每天的变化量占总体数据量比例很低,极端情况下,每天无变化,此情况下存储浪费很严重。此方法主要就是实现了牺牲存储获取ETL效率的优化和逻辑上的简化。但是一定要杜绝过度使用这种方法,而且必须要有对应的数据生命周期制度,清除无用的历史数据。
豆包
一、优点举例
处理缓慢变化维度简单而有效,开发和维护成本低
- 假设一个电商企业的数据仓库,商品维度表需要记录商品的名称、价格、库存等信息。如果采用快照方式,开发人员只需要每天对商品维度进行一次全量备份即可。例如,在每天凌晨数据仓库进行数据抽取时,将所有商品的信息完整地复制一份存储下来。这样的处理方式不需要复杂的算法和逻辑来跟踪每个商品的变化历史,开发过程相对简单,减少了开发时间和人力成本。同时,在后续的维护中,也不需要花费大量精力去管理复杂的变化跟踪机制,降低了维护成本。
- 当商品的某个属性发生变化时,比如价格调整,只需要在新的快照中更新该商品的价格字段,而不需要对历史数据进行复杂的修改操作。
使用方便,理解性好
- 对于数据使用方来说,比如市场分析人员想要分析某一天的销售情况以及当天的商品信息。通过限定日期,使用自然键进行关联,就可以轻松地获取到当天的商品快照数据。例如,分析人员想要了解 2024 年 9 月 1 日的销售数据和商品情况,只需要在查询语句中指定日期为 2024 年 9 月 1 日,然后通过商品的自然键(如商品编号)与销售事实表进行关联,就可以获取到当天的商品信息,如商品名称、价格、库存等。这种方式直观易懂,不需要数据使用方了解复杂的维度变化处理逻辑,方便他们进行数据分析和决策。
二、弊端举例
存储浪费严重
- 以一个大型制造企业的产品维度为例,假设该企业有 100 万种产品,每天产品维度的变化量可能只有几十种甚至更少。在极端情况下,可能连续几天都没有产品信息的变化。但是采用快照方式,每天都要存储一份全量的产品快照数据,这就会导致大量的存储资源被浪费。例如,第一天存储了 100 万种产品的信息,占用了一定的存储空间。第二天如果没有任何产品信息变化,仍然需要存储一份完全相同的 100 万种产品信息,这样持续下去,存储浪费会越来越严重。
- 特别是对于那些数据量庞大、变化频率低的维度,存储浪费会更加突出。如果没有有效的数据生命周期制度来清除无用的历史数据,随着时间的推移,存储成本会不断增加,给企业带来沉重的负担。
综合来看,由于现在存储成本远低于CPU、内存等成本,此方法总体来说弊大于利。是否有方法既可以实现上面的优点,同时又可以很好地降低存储呢?答案是肯定的,那就是阿里巴巴的极限存储。
极限存储
首先来看历史拉链存储。历史拉链存储是指利用维度模型中缓慢变化维TYPE2的处理方式。这种处理方式是通过新增两个时间戳字段(start_dt和end_dt),将所有以天为粒度的变更数据都记录下来,通常分区字段也是这两个时间戳字段。
例如,1月1号,卖家A在淘宝网发布了B、C两个商品,前端商品表将生成两条记录t1、t2;1月2号,卖家A将B商品下架了,同时又发布了商品D,前端商品表将更新记录t1,又新生成记录t3;采用全量存储方式, 在1月1号这个分区中存储t1和t2两条记录;在1月2号这个分区中存储更新后的t1以及t2、t3记录。数据存储如下:
商品 | dt | 卖家 | 状态 | 其他字段 |
---|---|---|---|---|
B | 20160101 | A | 上架 | ... |
C | 20160101 | A | 上架 | ... |
B | 20160102 | A | 下架 | ... |
C | 20160102 | A | 上架 | ... |
D | 20160102 | A | 上架 | ... |
如果我们采用历史拉链存储,数据存储如下,对于不变的数据,不再重复存储。
商品 | start_dt | end_dt | 卖家 | 状态 | 其他字段 |
---|---|---|---|---|---|
B | 20160101 | 20160102 | A | 上架 | ... |
C | 20160101 | 30001231 | A | 上架 | ... |
B | 20160102 | 30001231 | A | 下架 | ... |
D | 20160102 | 30001231 | A | 上架 | ... |
这样下游应用可以通过限制 时间戳字段来获取历史数据,例如, 用户访问1月1号的数据,只需要限制: start_dt<=20160101 and end_dt>20160101
。
但是这种存储方式对于下游使用方的存在一定的理解障碍,特别是ODS的数据面向的下游用户包含数据分析师、前端开发等,这些人群不怎么理解维度模型的概念,因此会存在较高的解释成本。另一方面,这种存储方式用 start_dt和end_dt作分区,随着时间的推移,分区数量会极度膨胀,而现行的数据库系统都有分区数量限制。
为了解决上述的两个问题,我们提出极限存储的方式处理:
(1)透明化:底层的数据还是历史拉链存储,但是上层我们做一个视图操作或者在hive里做了一个hook,通过分析语句的语法树,把对极限存储前的表的查询转化成对极限存储表的查询。对于下游用户来说,极限存储表和全量存储方式是一样的:
Select * from A where ds =20160101 ;
等价于
Select * from A_EXST where start_dt <=20160101 and end_dt >20160101;
(2)分月做历史拉链:假设我们用start_dt和end_dt做分区,并且不做限制,那么可以计算下一年下来历史拉链表最多就可能产生分区数:365*364/2=66430个。如果我们在每个月月初重新开始做历史拉链,目录结构如下:
|-- 201410/ # 每月一个周期
|---- 20141001/ 201410_INFINITY # 历史截止自当前月仍然没有死亡的记录
|---- 20141001/20141002 # 历史截止自当前月到20141002死亡的记录
|---- 20141001/20141003 # 历史截止自当前月到20141003死亡的记录
…
|----- 20141001/20141031 # 历史截止自当前月到20141031死亡的记录
|----- 20141002/ 201410_INFINITY # 20141002新增或历史原有记录发生变更,当前月仍然没有死亡的记录
|---- -20141002/20141003 # 20141002新增或原有记录发生变更,到20141003死亡的记录
…
|---- 20141002 /20141031/ # 20141002新增或原有记录发生变更,到20141031死亡的记录
|-----20141003/ 201410_INFINITY # 20141003新增或历史原有记录发生变更,当前月仍然没有死亡的记录
…
|---- 20141031/ 201410_INFINITY # 20141031新增或历史原有记录发生变更,当前月仍然没有死亡的记录
|-- 201411/
…
再计算下,一年下来,假设按照每个月30天算,我们产生的分区数:12*(31*30)/2=5580个。
采用极限存储的处理方式,极大的压缩了全量存储的存储成本,又可以做到对于下游用户透明的效果,是一种比较理想的存储方式。但是其本身也有一定的局限性,首先其产出效率很低,大部分极限存储通常需要t-2;其次对于变化频率高的数据并不能做到节约成本的效果。因此,在实际生产中,做极限存储需要做一些额外的处理:
(1)极限存储前面有一个全量存储表,全量存储表仅保留最近一段时间的全量分区数据,历史数据通过映射的方式关联到极限存储表。即用户只访问全量存储表,所以对用户来说极限存储是不可见的。
(2)对于部分变化频率频繁的字段需要在过滤,例如用户表中存在用户积分字段,这种字段的值每天都在发生变化,如果不过滤的话,极限存储就相当于每个分区存储一份全量数据,起不到节约存储成本的效果。
微型维度
采用极限存储,需要避免维度的过度增长。比如对于商品维表,每天20多亿数据,如果设计商品维度时,将值变化频繁的属性加入到商品维度中,极限情况是每天所有商品数据都发生变化。此时,极限存储没有意义。反之,每天所有商品数据都不发生变化,此时只需要存储一天的数据即可。
通过将一些属性从维度表中移除,放置到全新的维度表中,可以解决维度的过度增长导致的极限存储效果大打折扣的问题。解决方法其中之一就是上一节提到的垂直拆分,保持主维度的稳定。另外一种方式是采用微型维度。
微型维度的创建是通过将一部分不稳定的属性从主维度中移出,并将它们放置到拥有自己代理键的新表中来实现的。这些属性相互之间没有直接关联,不存在自然键。通过为每个组合创建新行的一次性过程来加载数据。比如在淘宝用户维度,用户的注册日期、年龄、性别、身份信息等基本不会发生变化,但用户VIP等级、用户信用评价等级会随着用户的行为不断发生变化。其中VIP等级共有8个值,-1~6;用户信用评价等级共有18个值。假设基于VIP等级和用户信用评价等级构建微型维度,则在此微型维度中共有8*18个组合,即144条记录,代理键可能是1~144。
以淘宝交易事实表为例,其它维度忽略,星型模式可能表示如下:
但在阿里巴巴数据仓库实践中,并未使用此技术,主要有以下几点原因:
(1)微型维度的局限性。微型维度是事先用所有可能值的组合加载的,需要考虑每个属性的基数,且必须是枚举值。很多属性可能是非枚举型,比如数值类型,如VIP分数、信用分数;比如时间类型,上架时间、下架时间、变更时间等等。
(2)ETL逻辑复杂。对于分布式系统中生成代理键和使用代理键进行ETL加工都非常复杂,前面章节已经有介绍,ETL开发和维护成本过高。
(3)破坏了维度的可浏览性。买家维度和微型维度通过事实表建立联系,无法基于VIP等级和信用等级进行浏览和统计。可以通过在买家维度中添加引用微型维度的外键部分解决此问题,但带来的问题是微型维度未维护历史信息。
特殊维度
本章主要介绍几种特殊的维度,递归层次维度及处理方法;行为维度的概念及几种常见的表现形式;多值维度和多值属性的概念及几种处理方式;杂项维度的概念等。
递归层次
我们在第一章学习了维度的层次结构,即维度属性以层次方式或一对多方式相互关联;或者描述为不同的维度之间的主从关系。比如商品和类目的关系,商品和品牌的关系等。本节的递归层次指的是某维度的实例值的层次关系,比如淘宝类目体系,示例如下:
类目ID | 类目名称 | 父类目ID | 是否叶子类目 |
---|---|---|---|
50026579 | 圣诞服饰 | 50026576 | 是 |
50026576 | 节日\庆典用品 | 21 | 否 |
21 | 家居日用 | 0 | 否 |
121456022 | 台历 | 21 | 是 |
维度的递归层次,按照层级是否固定分为均衡层次结构和非均衡层次结构。比如类目,有固定数量的级别,分别是叶子类目、五级类目、四级类目、三级类目、二级类目、一级类目;比如地区,分别是乡镇/街道、区县、城市、省份、国家。对于这种具有固定数量级别的递归层次,我们称为均衡层次结构。比如,公司之间的关系,每个公司可能存在一个母公司,但可能没有固定的一级、二级等的层级关系。对于这种数量级别不固定的递归层次,我们称为非均衡层次结构。
淘宝交易事实表通过叶子类目和类目维表关联,假设统计类目ID为21的最近一天的GMV,如何进行统计。第一步,取父类目ID等于20的所有类目,称为子类目。第二步,对于每个子类目,如果为叶子类目,则终止;如果非叶子类目,则此类目ID作为父类目ID执行第一步,直到找到所有叶子类目,如圣诞服饰(50026579)、台历(121456022)等。将所有叶子类目和交易事实表关联进行统计汇总即可得到类目ID等于21的最近一天GMV,即家具日用类目的最近一天GMV。在物理实现时,可以使用递归SQL实现,如Oracle中connect by语法。
通过数据探查得到ID等于21的类目属于一级类目(父类目ID等于0),统计其最近一天GMV的过程,称为上钻;在递归层次中进行上钻和下钻是很常见的。由于很多数据仓库系统和商业智能工具不支持递归SQL,且用户使用递归SQL的成本较高,所以在维度模型中,需要对此层次结构进行处理。
层次结构扁平化
降低递归层次使用复杂度的最简单和有效的方法是层次结构的扁平化,通过建立维度的固定数量级别的属性来实现,可以在一定程度上解决上钻和下钻的问题。对于均衡层次结构,采用扁平化最为有效。
对于淘宝商品类目,通过层次扁平化之后,类目维度表示如下,每个类目保存一条记录,并将其所属的各类目层级属性化。其中,对于高层级类目,由于其无低级类目,则低级类目置为空值。
具体数据存储示例如下,其中四级和五级类目省略:
类目ID | 类目级别 | 是否叶子类目 | 一级类目 | 二级类目 | 三级类目 |
---|---|---|---|---|---|
21 | 1 | N | 21 | --- | --- |
50026576 | 2 | N | 21 | 50026576 | --- |
50026579 | 3 | Y | 21 | 50026576 | 50026579 |
121456022 | 2 | Y | 21 | 121456022 | --- |
假设统计类目ID为21的最近一天的GMV,如何进行统计。将淘宝交易事实表通过叶子类目和类目维表的类目ID关联之后,限制一级类目ID等于21之后进行汇总统计,即可以得到类目ID等于21的最近一天的GMV,使用方便性大大提高。但存在如下三个方面的问题:
(1)针对某类目上钻或下钻之前,必须知道其所属的类目层级,然后才能决定限制哪一级类目,如上述示例,限制一级类目ID等于21。
(2)假设分三级类目统计最近一天交易GMV,由于某些叶子类目直接是一级类目或二级类目。比如类目ID等于121456022的类目,其是叶子类目。和交易事实表关联之后,由于其对应的三级类目为空,导致根据三级类目统计最近一天交易GMV时,类目ID等于121456022的交易无法被统计到。下游数据统计时,为了规避此问题,如此类目对应的三级类目为空,则取二级类目,如二级类目仍为空,则取一级类目。
所以针对次问题,解决下游数据统计问题,类目层次扁平化的另一种方式是回填,将类目向下虚拟,具体数据示例如下,其中标红部分为回填内容。其中阿里巴巴中文站的类目体系使用此种方式。
类目ID | 类目级别 | 是否叶子类目 | 一级类目 | 二级类目 | 三级类目 |
---|---|---|---|---|---|
21 | 1 | N | 21 | 21 | 21 |
50026576 | 2 | N | 21 | 50026576 | 50026576 |
50026579 | 3 | Y | 21 | 50026576 | 50026579 |
121456022 | 2 | Y | 21 | 121456022 | 121456022 |
(3)第三个问题就是扁平化仅包含固定数量的级别,对于非平衡层次结构,可以通过预留级别的方式解决,但扩展性会较差。
层次桥接表
针对层次结构扁平化存在的问题,可以采用桥接表的方式解决,不需要预先知道所属层级,也不需要回填,亦可解决非均衡层次的问题。与扁平化方法相比,该方法适合解决更宽泛的分析问题,灵活性更好;但复杂性高、使用成本高。
仍然以类目为例,模型设计如下:
上一节提到的类目,使用树型结构表示如下:
针对此类目树,类目桥接表的内如如下:
父类目ID | 子类目ID | 类目层级间隔 |
---|---|---|
21 | 21 | 0 |
21 | 50026576 | 1 |
21 | 121456022 | 1 |
21 | 50026579 | 2 |
50026576 | 50026576 | 0 |
50026576 | 50026579 | 1 |
121456022 | 121456022 | 0 |
假设针对类目21进行下钻操作,步骤如下:限制类目表类目ID等于21,通过类目ID和类目桥接表的父类目ID关联,将两表建立连接;通过类目桥接表的子类目ID和交易事实表的类目ID关联,将此两表建立连接;即可针对订单事实进行下钻操作。涉及类目桥接表的数据见表格中父类目ID字段的黑色部分(21)。
假设针对类目50026579进行上钻操作,步骤如下:限制类目表类目ID等于50026579,通过类目ID和类目桥接表的子类目ID关联,将两表建立连接;通过类目桥接表的父类目ID和交易事实表的类目ID关联,将此两表建立连接;即可针对订单事实进行上钻操作。涉及类目桥接表的数据见表格中子类目ID字段的黑色部分(50026579)。
可以看到,层次桥接表解决了层次结构扁平化带来的一些问题;但由于其加工逻辑复杂、使用逻辑复杂,而且由于事实表和桥接表的多对多关系而带来了双重计算的隐患。实际应用中可以根据具体的业务需求来选择技术方案,如果在统计分析或报表中,一般都是按照固定级别进行,如按照一级类目统计GMV、按照省份统计GMV等,现在的扁平化设计完全可以满足需求,而不需要引入复杂的桥接表。很多时候,简单、直接的技术方案却是最好的解决方案。
行为维度
在阿里巴巴的数据仓库中,存在很多如下的维表。比如卖家的主营类目、卖家主营品牌、用户常用地址等。其中卖家主营类目和主营品牌使用卖家的商品分布情况和卖家的交易分布情况,通过算法计算得到卖家的主营类目和主营品牌;其中卖家常用地址,使用最近一段时间内物流中卖家的发货地址和买家的收货地址进行统计得到。类似维度,都和事实相关,如交易、物流等,称之为行为维度,或事实衍生的维度。
按照行为维度的加工方式,可以划分为以下几种:
(1)另一个维度的过去行为,如买家最近一次访问淘宝的时间、买家最近一次发生淘宝交易的时间等;
(2)快照事实行为维度,如买家年初截止当前的淘宝交易金额、买家信用分值、卖家信用分值等;
(3)分组事实行为维度,将数值型事实转换为枚举值。如买家年初截止当前的淘宝交易金额按照金额划分的等级、买家信用分值按照分数划分得到买家信用等级等;
(4)复杂逻辑事实行为维度,通过复杂算法加工或多个事实综合加工得到。如前面提到的卖家主营类目、如商品热度,根据访问、收藏、加入购物车、交易等情况综合计算得到。
对于行为维度,有两种处理方式。一种是将其冗余至现有维度表中,如将卖家信用等级冗余至卖家维度表中。另外一种方式是加工单独的行为维度表,如卖家主营类目。具体采用哪种方式主要参考如下两个原则:
第一,避免导致维度的过快增长。比如我们对商品表进行了极限存储,如果将商品热度加入现有商品维度表,可能会导致每日商品变更占比过高,而导致极限存储效果较差。
第二,避免耦合度过高。比如卖家的主营类目,加工逻辑异常复杂,如果融合进现有的卖家维度表,过多业务耦合会导致卖家维度表刷新逻辑复杂、维护性差、产出延迟等。
多值维度
对于多值维度,其中一种情况是,事实表的一条记录在某维度表中有多条记录对应。比如,对于淘宝交易订单,买家一次购买了多种商品,比如一件毛衣和两双袜子,称此次交易为父订单;对于每种商品的交易,称为子订单,对于此交易父订单,则有两个子订单与之对应。假设设计交易父订单事实表,则对于此事实表的每一条记录,在商品表中有一到多条记录与之对应。
针对多值维度,常见的处理方式有三种,可以根据业务的表现形式和业务的统计分析需求进行选择。
第一种处理方式是降低事实表的粒度。在淘宝交易中,前台业务和商业智能关注于交易子订单,所以在数据仓库模型设计中,将交易订单设计为子订单粒度,对于每个子订单,都只有一个商品相对应。对于其中的事实,则采用分摊到子订单的方式解决。但很多时候,事实表的粒度是不能降低的,多值维度的出现是无法避免的。
第二种处理方式是多字段的方式。比如房地产销售中,每次合同签订可能存在多名买受人的情况,如夫妻合买等。对于合同签订事实表,每条记录可能对应多个买受人,而合同已经是此事实中的最细粒度,无法通过降低粒度的方式解决。由于合同签订的买受人一般不会太多,所以多字段的方式一般即可解决,考虑到扩展性,可以通过预留字段的方式解决,如超过三个买受方时,其余买受方填写至“其他买受方”字段。模型设计如下:
第三种处理方式是较为通用的桥接表方式。桥接表方式更加灵活、扩展性更好,但逻辑复杂、开发和维护成本较高,可能带来双重计算的风险,选择此方案需慎重。通过在事实表和维度表之间开发一张分组表,通过此分组表建立连接。模型设计如下,其中桥接表包含和事实表关联的分组KEY,以及作为买受方维表外键的买受方ID。如果事实表的一条记录对应两个买受方,则桥接表针对这两个买受方建立两条记录,分组KEY相同。
假设根据买受方籍贯统计2015年的合同总金额,如果某合同有两个买受人,籍贯分别是浙江和山东,此合同金额将会分别统计至浙江和山东,造成双重计算。双重计算不一定是错误,对于某些业务需求确是合理的;但对某些业务需求,需要规避。
多值属性
维度表中的某个属性字段同时有多个值,此种情况称为多值属性,也是多值维度的另一种表现形式。在阿里巴巴的数据仓库中,存在很多如下的维表。比如商品SKU维表、商品属性维表、商品标签维表等。每个商品均有一到多个SKU、一到多个属性和一到多个标签,所以商品和SKU、属性、标签都是多对多的关系。淘宝商品SKU和属性信息示例如下:
对于多值属性,有三种常见的处理方式,和多值维度的第一种表现形式(上一节)相比既有相同点也有不同点,可以根据具体情况进行选择。
第一种方式是保持维度主键不变,将多值属性放在维度的一个属性字段中。比如,对于商品属性(注:此属性是业务上含义,和维度建模中的维度属性含义不同),可以通过KV对的形式放在property字段中,数据示例如下:10281239:156426871; 137396765:29229; 137400766:3226633。此种处理方式扩展性好,但数据使用较为麻烦。
第二种处理方式也是保持维度主键不变,但将多值属性放在维度的多个属性字段中。比如卖家的主营类目,由于卖家店铺中可能同时会销售男装、女装、内衣等,所以卖家的主营类目可能有多个,但业务需求只取根据算法计算得到的TOP3。针对此种情况,维度的多值属性字段具体值的数量固定,可以采用多个属性字段进行存储,方便数据的统计分析和报表展示。如果多值属性字段具体值的数量不固定,也可以采用预留字段的方式,但扩展性较差。卖家的主营类目维度设计如下:
第三种处理方式是维度主键变化,一个维度值存放多条记录。比如商品SKU维表,对于每个商品,有多少SKU,即有多少记录,主键是商品的ID和SKU的ID。此种处理方式扩展性好,使用方便,但需要考虑数据的急剧膨胀。比如淘宝商品属性表采用了此种处理方式,数据记录达到几百亿的级别。
杂项维度
在维度建模中,有一种维度叫Junk Dimension,中文一般翻译为“杂项维度”。杂项维度是由操作型系统中的指示符或者标志字段组合而成,一般不在一致性维度之列。比如淘宝交易订单的交易类型字段,包括话费充值、司法拍卖、航旅等等类型;支付状态、物流状态等,它们在源系统中直接保存在交易表中。
一张事实表中可能会存在好几个类似的字段,如果作为事实存放在事实表中,会导致事实表占用空间过大;如果单独建立维度表,外键关联到事实表,会出现维度过多的情况;如果将这些字段删除,会有人不同意。
这时,我们通常的解决方案就是建立杂项维度,将这些字段建立到一个维度表中,在事实表中只需保存一个外键。几个字段的不同取值组成一条记录,生成代理键,存入维度表,并将该代理键保存入相应的事实表字段。建议不要直接使用所有的组合生成完整的杂项维度表,在抽取时遇到新的组合时生成相应记录即可。杂项维度的ETL过程比一般的维度略为复杂。
但在阿里巴巴实践中,杂项维度不仅包含上述所述的指示符、状态或分类等可枚举字段,还包括很多非可枚举字段,如交易留言、交易属性(若干KV对组成)、交易标签(二进制位表示)。针对这些字段,不可能生成所有的组合;同时,由于分布式计算系统中生成代理键的复杂度,一般在逻辑建模中,会使用实体的主键作为杂项维度的主键。只考虑杂项维度,忽略其他维度,如下所示:
但子订单维度一般是逻辑模型,物理实现时不进行物理化,订单杂项维度和其它维度一起,会将维度属性退化至事实表,详情在事实表中描述。
事实表基础
事实表作为数据仓库维度建模的核心,紧紧围绕着业务过程来设计,通过获取描述业务过程的度量来表达业务过程,包含了引用的维度和与业务过程有关的度量。
事实表的设计需要遵循一些原则和方法。在Kimball所著的《数据仓库工具箱》一书中,对于维度模型设计采用的4步设计方法:1.选择业务过程 2.声明粒度 3.确定维度 4.确定事实。
在当前的互联网大数据环境下,面对复杂的业务场景,为了更有效准确地进行维度模型建设,基于Kimball的4步维度建模方法,我们进行了更进一步的改进。
事实表特性
事实表作为数据仓库维度建模的核心,紧紧围绕着业务过程来设计,通过获取描述业务过程的度量来表达业务过程,包含了引用的维度和与业务过程有关的度量。
事实表中一条记录所表达的业务细节程度被称为粒度。粒度通常可以通过两种方式来表述:一种是维度属性组合所表示的细节程度,一种是所表示的具体业务含义。
作为度量业务过程的事实,一般为整型或浮点型的十进制数值,有可加性、半可加性和不可加性三种类型。可加性事实是指可以按照与事实表关联的任意维度进行汇总。半可加性事实只能按照特定维度汇总,不能对所有维度汇总,比如库存可以按照地点和商品进行汇总,而按时间维度,把一年中每个月的库存累加起来则毫无意义。另一种度量完全不具备可加性,比如比率型事实。对于不可加的事实可分解为可加的组件来实现聚集。
相对维度表来说,通常事实表要细长的多,行的增加速度也比维度表快很多。 维度属性也可以存储到事实表中,这种存储到事实表中的维度列被称为退化维度。与其他存储在维度表中的维度一样,退化维度也可以用来作为事实表的过滤查询、实现聚合操作等。
事实表有三种类型:事务事实表、周期快照事实表、累积快照事实表,具体后面章节会详细介绍。事务事实表用来描述业务过程,跟踪空间或时间上某点的度量事件,保存的是最原子的数据,也称为“原子事实表”。周期快照事实表以具有规律性的、可预见的时间间隔记录事实,时间间隔比如每天、每月、每年等。累积快照事实表用来表述过程开始和结束之间的关键步骤事件,覆盖了过程的整个生命周期,通常具有多个日期字段来记录关键时间点,当过程随着生命周期不断变化时,记录也会随着过程的变化而被修改。
事实表设计原则
- 原则1:尽可能包含所有与业务过程相关的事实
事实表设计的目的,是为了度量业务过程。所以分析哪些事实与业务过程有关是设计中非常重要的关注点。事实表中应该尽量包含所有与业务过程相关的事实,即使存在冗余,因事实通常为数字型,带来的存储开销也不会很大。
- 原则2:只选取与业务过程相关的事实
在事实的选择时需要注意应该只选取与业务过程有关的事实。比如在订单的下单这个业务过程的事实表设计中,不应该存在支付金额这个表示支付业务过程的事实。
- 原则3:分解不可加事实为可加的组件
对于不具备可加性条件的事实,需要分解为可加的组件。比如订单的优惠率,应该分解为订单原价金额与订单优惠金额两个事实存储在事实表中。
- 原则4:在选择维度和事实之前必须先声明粒度
粒度的声明是事实表设计中不可忽视的重要一步,粒度用于确定事实表中一行所表示业务的细节层次,决定了维度模型的扩展性,在选择维度和事实之前必须先声明粒度。且每个维度与事实必须与定义的粒度保持一致。在设计事实表的过程中,粒度定义越细越好,建议从最低级别的原子粒度开始,因为原子粒度提供了最大限度的灵活性,可以支持无法预期的各种细节层次的用户需求。在事实表中,通常通过业务描述来表述粒度,但对于聚集性事实表的粒度描述,可采用维度或维度属性组合的方式。
- 原则5:在同一个事实表中不能有多种不同粒度的事实
事实表中的所有事实需要与表定义的粒度保持一致,在同一事实表中不能有多种不同粒度的事实。
如下表所示的机票支付成功事务事实表,粒度为票一级的,而在实际业务中,一条订单可以同时支付多张票,如ID为100901的订单包含三张机票,ID为100902的订单包含两张机票,ID为100903的订单包含一张机票。在该事实表的设计中,票支付金额与票折扣金额两个事实,与表的粒度一致,并且支持按表的任意维度汇总,可以添加进该事实表中。而订单支付金额与订单票数,作为更上一层粒度的订单级事实,与该票级事实表的粒度不一致,且不能进行汇总,比如订单ID为100901,订单支付金额为3700元,订单票数为3张,如果这两个度量在该表进行汇总计算总订单金额和总票数,则会造成重复计算的问题,所以不能作为该表的度量选入。
机票ID | 订单ID | 票支付金额 | 票折扣金额 | 订单支付金额 | 订单票数 |
---|---|---|---|---|---|
23459801 | 100901 | 1000.00 | 100.00 | 3700.00 | 3 |
23459802 | 100901 | 1200.00 | 120.00 | 3700.00 | 3 |
23459803 | 100901 | 1500.00 | 150.00 | 3700.00 | 3 |
23459804 | 100902 | 1600.00 | 160.00 | 2600.00 | 2 |
23459805 | 100902 | 1000.00 | 100.00 | 2600.00 | 2 |
23459806 | 100903 | 1500.00 | 150.00 | 1500.00 | 1 |
- 原则6:事实的单位要保持一致
对于同一个事实表中事实的单位,应该保持一致。比如原订单金额、订单优惠金额、订单运费金额这三个事实,应该采用一致的计量单位,统一为元或分,以方便使用。
- 原则7:对事实的null值要处理
关于事实表中事实度量为null值的处理,因在数据库中null值对常用数字型字段的SQL过滤条件都不生效,比如:大于、小于、等于、大于等于、小于等于,建议用零值填充。
- 原则8:使用退化维度提高事实表的易用性
在Kimball的维度建模中,通常按照星型模型的方式来设计,对于维度的获取采用的是通过事实表的外键关联专门的维度表的方式,谨慎使用退化维度。而在大数据领域的事实表设计中,则大量采用退化维度的方式,在事实表中存储各种类型的常用维度信息。这样设计的目的主要是为了减少下游用户使用时候关联多个表的操作,直接通过退化维度实现对事实表的过滤查询、控制聚合层次、排序数据以及定义主从关系等。以增加冗余存储的方式减少计算开销、提高使用效率。 -
事实表设计方法
-
在Kimball所著的《数据仓库工具箱》一书中,对于维度模型设计采用的4步设计方法:1.选择业务过程 2.声明粒度 3.确定维度 4.确定事实。
在当前的互联网大数据环境下,面对复杂的业务场景,为了更有效准确地进行维度模型建设,基于Kimball的4步维度建模方法,我们进行了更进一步的改进。
- 第一步:选择业务过程及确定事实表类型
在明确了业务需求以后,接下来需要进行详细的需求分析,对业务的整个生命周期进行分析,明确关键的业务步骤,从而选择与需求有关的业务过程。
以淘宝的正向订单流转为例:
业务过程通常用行为动词表示,表示业务执行的活动。比如图中的淘宝订单流转的业务过程有四个:创建订单、买家付款、卖家发货、买家确认收货。在明确了流程所包含的业务过程后,需要根据具体的业务需求来选择与维度建模有关的业务过程。比如选择买家付款这个业务过程;还是选择创建订单和买家付款这两个业务过程,具体根据业务情况来确定。
在选择了业务过程以后,相应的事实表类型也随之确定了。比如选择买家付款单个业务过程,那么事实表应为只包含买家付款这一个业务过程的单事务型事实表;如果选择的是所有这四个业务过程,并且需要分析各个业务过程之间的时间间隔,那么所建的事实表应为包含了所有四个业务过程的累积快照型事实表。 - 第二步:声明粒度
粒度的声明是事实表建模非常重要的一步,意味着精确定义事实表的每一行表示什么业务含义,粒度传递的是与事实表度量有关的细节层次。明确的粒度能确保对事实表中行的意思的理解不会产生混淆,保证所有的事实按照同样的细节层次记录。
应该尽量选取最细级别的原子粒度,以确保事实表的应用具有最大的灵活性。同时对于订单过程而言,粒度可以被定义为最细的订单级别。比如淘宝订单中有父子订单的概念,既一条子订单对应一类商品,如果拍下了多种商品,则每种商品对应一条子订单;这些子订单一同结算的话,则会生成一条父订单。那么在这个例子中,事实表的粒度应该选择为子订单级别。
- 第三步:确定维度
完成粒度声明以后,也就意味着确定了主键,对应的维度组合以及相关的维度字段就可以确定了,应该选择能够描述清楚业务过程所处的环境的维度信息。比如淘宝的订单付款事务事实表中,粒度为子订单,相关的维度信息有买家维度、卖家维度、商品维度、收货人信息、业务类型、订单时间等。
- 第四步:确定事实
事实可以通过回答“过程的度量是什么”来确定。应该选择与业务过程有关所有事实,且事实的粒度要与所声明的事实表粒度一致。事实有可加性、半可加性、非可加性事实,需要将不可加事实分解为可加的组件。
在淘宝的订单付款事务事实表中,同粒度的事实有子订单分摊的支付金额、邮费、优惠金额等。
- 第五步:冗余维度
在传统的维度建模星型模型中,对于维度的处理是需要单独存放在专门的维度表中,通过事实表中外键的方式获取维度。这样做的目的是为了减少事实表的维度冗余,从而减少存储消耗。而在大数据的事实表模型设计中,更多的考虑是提高下游用户的使用效率,降低数据获取的复杂性,减少关联的表数量。所以通常的事实表中会冗余方便下游用户使用的常用维度,以实现对事实表的过滤查询、控制聚合层次、排序数据以及定义主从关系等操作。
比如在淘宝订单付款事务事实表中,通常会冗余大量的常用维度字段,以及商品的类目、卖家店铺等维度信息。
事务事实表
订单作为交易行为的核心载体,直观反映了交易的状况。订单的流转会产生很多的业务过程,而下单、支付和成功完结三个业务过程是整个订单的关键节点。获取这三个业务过程的笔数、金额以及转化率是日常数据统计分析的重点,事务事实表设计可以很好的满足这个需求。前面章节讨论了事实表的基础,您已经了解了事实表设计的原则和方法,本章将以此为基础介绍三种不同事务事实表的设计方式,以及淘宝交易订单中关于邮费和折扣在子订单的分摊算法。
事务事实表设计过程
事务,任何类型的事件都可以被理解为一种事务,比如交易过程中的创建订单、买家付款,物流过程中的揽货、发货、签收,退款中的申请退款、申请小二介入等等都可以理解为一种事务,事务事实表,即针对这些过程构建的一类事实表,用以跟踪定义业务过程的个体行为,提供丰富的分析型能力,作为数据仓库原子的明细数据。下面以淘宝交易事务事实表为例阐述事务事实表的一般设计过程。
第一步,选择业务过程。
上一章《事实表基础》给出了淘宝交易订单的流转过程,其中介绍了四个重要过程:创建订单、买家付款、卖家发货、买家确认收货,即下单、支付、发货和成功完结四个业务过程。这四个业务过程不仅是交易过程中的重要时间节点,同时也是下游统计分析的重点,因此淘宝交易事务事实表着重从这个四个业务过程进行展开。
Kimball维度建模理论认为,为了便于独立的分析研究,应该为每一个业务过程建立一个事实表,对于不同业务过程是否放到同一个事实表中,将在下一节详细介绍。
第二步,确定粒度。
业务过程选定以后,就要针对每个业务过程确定一个粒度,即确定事务事实表每一行所表达的细节层次,这里先介绍下淘宝订单过程。
淘宝出售商品主要分两类卖家,一类是个人性质的闲置卖家,主要出售闲置或者二手商品,另外一类是拥有店铺类的卖家,以出售新商品为主。接下来主要以店铺类的交易订单为主进行介绍。在淘宝下单交易时,有两种方式,一种是选定商品然后直接购买,这样会产生一条交易订单;另外一种是将多件商品加入到购物车,然后从购物车一起结算,此时对于每一件商品都会产生一个订单,同时对于同一个店铺会额外产生一个订单,即父订单,由于是同一个店铺购买,所以此时父订单会承载订单物流、店铺优惠等信息。而对于每一个商品产生的订单就称为子订单,子订单记录了父订单的订单号,并且有子订单标记。如果在同一个店铺只购买了一件商品则会将父子订单进行合并,只保留一条订单记录。如下所示,分别是父子订单合并一条和父子订单合并多条。
订单ID | 父订单ID | 创建时间 | 买家ID | 卖家ID | 商品ID | 金额 | 数量 | 是否子订单 | 是否父订单 |
1 | 1 | 2016-01-01 00:00:00 | 111 | 222 | 11111 | 100.0 | 1 | Y | Y |
9 | 9 | 2016-01-03 00:00:00 | 444 | 555 | 66666 | 500.0 | 2 | Y | Y |
订单ID | 父订单ID | 创建时间 | 买家ID | 卖家ID | 商品ID | 金额 | 数量 | 是否子订单 | 是否父订单 |
2 | 0 | 2016-01-01 00:00:00 | 111 | 222 | 0 | 0.0 | 3 | N | Y |
4 | 2 | 2016-01-01 00:00:00 | 111 | 222 | 11111 | 100.0 | 1 | Y | N |
5 | 2 | 2016-01-01 00:00:00 | 111 | 222 | 22222 | 50.0 | 2 | Y | N |
3 | 0 | 2016-01-02 00:00:00 | 333 | 777 | 0 | 0.0 | 6 | N | Y |
6 | 3 | 2016-01-02 00:00:00 | 333 | 777 | 33333 | 70.0 | 1 | Y | N |
7 | 3 | 2016-01-02 00:00:00 | 333 | 777 | 44444 | 80.0 | 2 | Y | N |
8 | 3 | 2016-01-02 00:00:00 | 333 | 777 | 55555 | 90.0 | 3 | Y | N |
了解了淘宝交易订单的产生过程后,接下来将为我们的淘宝交易事务事实表确定粒度。如第一步所述,淘宝交易过程中有四个重要业务过程,需要为每一个业务过程确定一个粒度。其中下单、支付和成功完结三个业务过程选择交易子订单粒度,即每一笔子订单为事务事实表的一行,每一笔子订单所表达的细节信息:交易时间、卖家、买家、商品,即选择图中订单ID为1、4、5、6、7、8的子订单作为事务事实表的每一行。卖家发货这个业务过程可以选择子订单粒度,即将每一笔子订单作为卖家发货事实表的一个细节,然而在实际操作中发现,卖家发货更多是物流单粒度而非子订单粒度,同一笔子订单可以拆开多笔物流进行发货,在事务事实表设计过程中,秉承确定粒度为最细粒度,因此对于卖家发货的粒度确定为物流单粒度,和其他三个业务过程不同,这样可以更好的给下游统计分析带来灵活性。
第三步,确定维度。
对于选定的业务过程并且确定粒度后,就可以确定维度信息。淘宝交易事务事实表在设计过程中,按照经常用于统计分析的场景,确定维度包含:买家维度、卖家维度、商品维度、商品类目维度、发货地区维度、收货地区维度、父订单维度以及杂项维度。由于订单的属性较多,比如订单的业务类型、是否无线交易、订单属性attributes等等,这些使用较多却又无法归属到上述买卖家或商品维度中则新建一个杂项维度进行存放。如图所示。
第四步,确定事实。
作为过程度量的核心,事实表应该包含与其描述过程有关的所有事实。以淘宝交易事务事实表为例,选定三个业务过程——下单、支付和成功完结,不同的业务过程拥有不同的事实。比如下单业务过程中,需要包含下单金额、下单数量、下单分摊金额,支付业务过程中,包含支付金额、分摊邮费、折扣金额、红包金额、积分金额,完结业务过程中包含确认收货金额等,由于粒度是子订单的,所以对于一些父订单上的金额需要平摊到子订单上,比如父订单邮费、父订单折扣等,具体分摊算法将在父子事实处理方式一节介绍。
根据Kimball维度建模理论,经过以上四步,淘宝交易事务事实表已初见成型,可以满足下游分析统计需要,然而阿里巴巴数据仓库在建模时,基于以上四个过程增加了一步——退化维度,这个过程在Kimball维度建模中也有所提及,但阿里数仓基于效率和资源考虑将常用维度全部退化到事实表中,使下游分析使用模型更加方便。
第五步,冗余维度。
第三步确定维度时,包含了买卖家维度、商品维度、类目维度、收发货维度等等,Kimball维度建模建议在事实表中只保存这些维度表的外键,而淘宝交易事务事实表在Kimball维度建模基础之上做了进一步的优化,将买卖家的星级、标签、店铺名称、商品类型、商品特征、商品属性、类目层级等等维度属性都冗余到事实表中,提高对事实表过滤查询、统计聚合的效率,如图所示。
经过以上五个步骤,完成了淘宝交易事务事实表的设计。
但设计过程中遗留的一个问题,即单一事实表中是否包含多个业务过程,还没有给出定论,接下来将通过淘宝和1688交易过程中不同的设计方案来阐述两种设计方法。
单事务事实表
单事务事实表,顾名思义,即针对每一个业务过程设计一个事实表,这样设计的优点不言而喻,可以方便的对每一个业务过程进行独立的分析研究。1688交易流程则采用了这种模式构建了事务事实表。
1688交易和淘宝交易相似,主要流程也是下单、支付、发货和完结,而在这四个关键流程中1688交易选择下单和支付两个业务过程设计事务事实表,分别是1688交易订单创建事务事实表和1688交易订单支付事务事实表。
选定业务过程后,将对每一个业务过程确定粒度、维度和事实。对于1688交易订单创建事务事实表,确定子订单粒度,选择买家、卖家、商品、父订单、收货地区维度,事实包含下单分摊金额和折扣金额,如图所示 ;而对于1688交易订单支付事务事实表,粒度和维度和交易订单创建事务事实表相同,所表达的事实则不一样,包含支付金额、支付调整金额和支付优惠等,如图所示 。
1688交易针对下单和支付分别建立单事务事实表后,每天的下单记录则进入当天的下单事务事实表中,每天的支付记录进入当天的支付事务事实表中,由于事实表拥有稀疏性质,因此只有当天有数据才会进入当天的事实表中。下面以一条交易订单为例,展示单事务事实表的数据流转情况。order1在2016-01-01下单并且在当天完成支付;order2和order3在2016-01-01下单并且在2016-01-02完成支付。
表:交易订单详情实例
订单ID | 父订单ID | 下单时间 | 支付时间 | 完结时间 |
order1 | mord1 | 2016-01-01 08:00:00 | 2016-01-01 10:00:00 | 2016-01-02 10:00:00 |
order2 | mord2 | 2016-01-01 09:00:00 | 2016-01-02 09:00:00 | 2016-01-04 09:00:00 |
order3 | mord2 | 2016-01-01 09:00:00 | 2016-01-02 09:00:00 | 2016-01-04 09:00:00 |
order4 | mord2 | 2016-01-01 09:00:00 | 2016-01-02 09:00:00 | 2016-01-04 09:00:00 |
订单ID | 父订单ID | 买家ID | 卖家ID | 商品ID | 下单度量 | 支付度量 | 是否子订单 | 是否父订单 |
order1 | mord1 | 111 | 222 | aa | … | … | Y | Y |
order2 | mord2 | 333 | 444 | 0 | … | … | N | Y |
order3 | mord2 | 333 | 444 | bb | … | … | Y | N |
order4 | mord2 | 333 | 444 | cc | … | … | Y | N |
表:1688交易订单创建事务事实表数据实例
业务日期 | 订单ID | 父订单ID | 下单时间 | 买家ID | 卖家ID | 商品ID | 下单度量 |
20160101 | order1 | mord1 | 2016-01-01 08:00:00 | 111 | 222 | aa | … |
20160101 | order3 | mord2 | 2016-01-01 09:00:00 | 333 | 444 | bb | … |
20160101 | order4 | mord2 | 2016-01-01 09:00:00 | 333 | 444 | cc | … |
表:1688交易订单支付事务事实表数据实例
业务日期 | 订单ID | 父订单ID | 支付时间 | 买家ID | 卖家ID | 商品ID | 支付度量 |
20160101 | order1 | mord1 | 2016-01-01 08:00:00 | 111 | 222 | aa | … |
20160102 | order3 | mord2 | 2016-01-02 09:00:00 | 333 | 444 | bb | … |
20160102 | order4 | mord2 | 2016-01-02 09:00:00 | 333 | 444 | cc | … |
多事务事实表
多事务事实表,将不同的事实放到同一个事实表中,即同一个事实表包含不同的业务过程。多事务事实表在设计时有两种方法进行事实的处理:1)不同业务过程的事实使用不同的事实字段进行存放;2)不同业务过程的事实使用同一个事实字段进行存放,但增加一个业务过程标签。接下来将分别通过淘宝交易事务事实表和淘宝收藏事务事实表分别阐述设计方法。
- 淘宝交易事务事实表
淘宝交易事务事实表采取了将不同业务过程的事实使用不同事实字段进行存放的设计模式。淘宝交易事务事实表中同时包含了下单、支付和成功完结三个业务过程,这三个业务过程拥有相同的粒度,都是到子订单粒度,也比较适合放到同一个事实表中。选择业务过程时没有把发货也加到此事务事实表,原因如上一节确定粒度所述,由于发货的粒度比子订单更细,属于不同粒度上的业务过程,因此没有放到同一个事实表中。
在确定好业务过程和粒度后,下一步就是确定维度和事实。对于不同的业务过程和粒度,一般而言,维度也不完全一致,但在设计淘宝交易事务事实表时,根据分析统计,常用维度比较一致,因此在维度层面可以保证这三个业务过程放到同一个事务事实表中,这里的维度也是交易过程中比较常见的,如前文所述包括买家、卖家、商品、类目、店铺、收发货地区等,无论在哪一个业务过程中,都需要按照这些维度进行统计分析。
将多个业务过程放到同一个事实表中,将要面对的是如何处理多个事实。淘宝交易事务事实表包含下单、支付和成功完结三个业务过程,则需要包含下单的度量、支付的度量和成功完结时的度量信息,这里的解决方案是针对每一个度量都使用一个字段进行保存,即不同的事实使用不同的字段进行存放,如果不是当前业务过程的度量则采取零值处理。比如下单业务过程中,对于支付度量和成功完结度量全部置为0,其他业务过程类似处理。
同一个事实表中包含了多个业务过程,在表中如何进行标记呢?淘宝交易事务事实表采取了这样的解决方案,即针对每一个业务过程打一个标签,标记当天是否是这个业务过程,比如针对下单,则打一个是否当天下单的标签,针对支付,打一个是否当天支付的标签,针对完结,打一个是否当天完结的标签,标签之间互不相干。淘宝交易事务事实表如图所示。
表:交易订单详情实例订单ID 父订单ID 下单时间 支付时间 完结时间 order1 mord1 2016-01-01 08:00:00 2016-01-01 10:00:00 2016-01-02 10:00:00 order2 mord2 2016-01-01 09:00:00 2016-01-02 09:00:00 2016-01-04 09:00:00 order3 mord2 2016-01-01 09:00:00 2016-01-02 09:00:00 2016-01-04 09:00:00 order4 mord2 2016-01-01 09:00:00 2016-01-02 09:00:00 2016-01-04 09:00:00
同样以一条交易订单为例,展示多事务事实表的数据流转情况。order1在2016-01-01下单并且在当天完成支付;order2和order3在2016-01-01下单并且在2016-01-02完成支付,在2016-01-04成功完结。如下面表格所示,同一个事实表中,包含有多个业务过程数据。订单ID 父订单ID 买家ID 卖家ID 商品ID 下单度量 支付度量 是否子订单 是否父订单 order1 mord1 111 222 aa … … Y Y order2 mord2 333 444 0 … … N Y order3 mord2 333 444 bb … … Y N order4 mord2 333 444 cc … … Y N 业务日期 订单ID 父订单ID 是否当天下单 是否当天支付 是否当天完结 下单时间 支付时间 完结时间 20160101 order1 mord1 Y Y N 2016-01-01 08:00:00 2016-01-01 10:00:00 NULL 20160101 order3 mord2 Y N N 2016-01-01 09:00:00 NULL NULL 20160101 order4 mord2 Y N N 2016-01-01 09:00:00 NULL NULL 20160102 order1 mord1 N N Y 2016-01-01 08:00:00 2016-01-01 10:00:00 2016-01-02 10:00:00 20160102 order3 mord2 N Y N 2016-01-01 09:00:00 2016-01-02 09:00:00 NULL 20160102 order4 mord2 N Y N 2016-01-01 09:00:00 2016-01-02 09:00:00 NULL 20160104 order3 mord2 N N Y 2016-01-01 09:00:00 2016-01-02 09:00:00 2016-01-04 09:00:00 20160104 order4 mord2 N N Y 2016-01-01 09:00:00 2016-01-02 09:00:00 2016-01-04 09:00:00 业务日期 订单ID 父订单ID 买家ID 卖家ID 商品ID 下单度量 支付度量 完结度量 20160101 order1 mord1 111 222 aa … … 0.0 20160101 order3 mord2 333 444 bb … 0.0 0.0 20160101 order4 mord2 333 444 cc … 0.0 0.0 20160102 order1 mord1 111 222 aa … … … 20160102 order3 mord2 333 444 bb … … 0.0 20160102 order4 mord2 333 444 cc … … 0.0 20160104 order3 mord2 333 444 bb … … … 20160104 order4 mord2 333 444 cc … … … -
淘宝收藏商品事务事实表
收藏和加购物车是淘宝购物过程中比较常见的两个行为,当用户遇到喜欢的商品或者店铺时可以选择收藏然后下次继续浏览购买。这里以收藏事务事实表阐述多事务事实表在处理不同业务过程使用同一个字段保存事实的设计方式。
收藏业务较为简单,商品和店铺的收藏业务相似,这里仅以收藏商品为例进行阐述。用户可以直接收藏一个商品,收藏后可以删除收藏的这个商品,所以在这个过程中包含了两个业务过程:收藏商品和删除商品。因此收藏商品事务事实表的第一步选择业务过程,就选定收藏商品和删除商品两个业务过程。业务过程确定后,就是确定粒度。无论是收藏商品还是删除收藏的商品,都是用户对商品的一个操作,因此这里确定用户加上商品的粒度。
业务过程和粒度确定好后,接下来是确定维度和事实。由于粒度是用户加上商品,所以维度主要是用户维度和商品维度,为了使事实表信息更丰富,冗余了商品类目维度和商品所属卖家维度,收藏商品和删除商品业务过程所属维度都是一致的。
收藏和删除两个不同的业务过程,但是确定了相同的粒度和维度,所以考虑设计多事务事实表,将这两个业务过程放到同一个事实表中,只是在不同业务过程的事实上进行区分,前一节是通过不同字段存放不同业务过程的事实,这里的解决方案是通过同一个字段存放不同业务过程的事实,使用标签字段区分不同业务过程,比如收藏事务事实表使用一个“收藏事件类型”字段来区分是收藏商品还是删除商品。收藏商品和删除商品的事实主要是商品价格,不过收藏事务事实表更多是无事实的事实表,一般用于统计收藏或者删除的次数。
下面通过一组实例来说明收藏事务事实表的设计过程。
表:收藏商品明细
表:收藏商品事务事实表收藏时间 删除时间 商品ID 会员ID 商品度量 2016-01-01 08:00:00 null aa 111 … 2016-01-01 12:00:00 2016-01-01 18:00:00 aa 222 … 2016-01-01 08:00:00 2016-01-01 08:00:00 bb 333 … 业务日期 收藏时间 删除时间 商品ID 会员ID 商品度量 收藏删除类型 20160101 2016-01-01 08:00:00 null aa 111 … collect 20160101 2016-01-01 12:00:00 2016-01-01 18:00:00 aa 222 … delete 20160101 2016-01-01 08:00:00 null bb 333 … collect 20160102 2016-01-01 08:00:00 2016-01-02 08:00:00 bb 333 … delete -
多事务事实表的选择
上面介绍了两种多事务事实表的设计方式,在实际应用中选择哪一类需要因业务过程不同而不同。由于是多事务事实表,因此在事实表中包含多个业务过程:
当不同业务过程的度量比较相似、差异不大时则采用第二种多事务事实表的设计方式,使用同一个字段来表示度量数据,但这种方式面对一个问题——同一个周期内会存在多条记录;
当不同业务过程的度量差异较大时,可以选择第一种多事务事实表的设计方式,将不同业务过程的度量使用不同字段冗余到表中,非当前业务过程则置零表示,问题是度量字段零值较多。
两类事实表对比
前面介绍了单事务事实表和多事务事实表的设计过程,同时给出了1688和淘宝关于不同事务事实表的实例。目前两类事实表都有实际的应用,但具体哪一种设计方式更优,我们接下来做一个分析。
- 业务过程
单事务事实表对一个业务过程建立一张事实表,只反映一个业务过程的事实;多事务事实表在同一张事实表中反映多个业务过程。多个业务过程是否放到同一个事实表中,首先需要分析不同业务过程之间的相似性和业务源系统,比如淘宝交易的下单、支付和成功完结这三个业务过程是存在相似性的,都是属于订单处理中的一环,并且都是来自于交易系统,因此适合放到同一个事务事实表中。
- 粒度和维度
在考虑单事务事实表还是多事务事实表,另一个关键点就是粒度和维度,在确定好几个业务过程后,需要基于不同业务过程确定粒度和维度,当不同业务过程的粒度相同,同时拥有相似的维度,此时就可以考虑多事务事实表方式。如果粒度不同,则必定是不同的事实表,比如交易中支付和发货则不属于同一粒度,无法将发货业务过程放到淘宝交易事务事实表中。
- 事实
对于不同的业务过程,事实往往是不同的,单事务事实表在处理事实上比较方便和灵活,仅仅体现同一个业务过程的事实即可。而多事务事实表由于有多个业务过程,所以有更多的事实需要处理,如果单一业务过程的事实较多,同时不同业务过程的事实又不相同,则可以考虑使用单事务事实表,处理更加清晰,若使用多事务事实表则会导致事实表零值或空值字段较多。
- 下游业务使用
单事务事实表于下游用户而言更容易理解,关注哪个业务过程则使用相应的事务事实表;而多事务事实表包含多个业务过程,用户使用时往往较为困惑。1688和淘宝交易分别采用了这两种方式,从日常使用来看,淘宝交易事务事实表下游确实有一定的学习成本。
- 计算存储成本
针对多个业务过程设计事务事实表,采用单事务还是多事务,对于数据仓库的计算存储成本也是参考点之一,当业务过程数据来源于同一个业务系统,具有相同的粒度和维度,另外维度较多而事实相对不多,此时可以考虑使用多事务事实表,不仅加工计算成本较低,同时存储上也相对节省,此时多事务事实表是一个较优的处理方式。
通过一个表格描述下两类事务事实表的比较,如下所示。
单事务事实表 | 多事务事实表 | |
---|---|---|
业务过程 | 1个 | 多个 |
粒度 | 相互间不相关 | 相同粒度 |
维度 | 相互间不相关 | 一致 |
事实 | 只取当前业务过程中的事实 | 保留多个业务过程中的事实,非当前业务过程的事实需要置零处理 |
冗余维度 | 多个业务过程,则需要冗余多次 | 不同的业务过程只需要冗余一次 |
理解程度 | 易于理解,不会混淆 | 难以理解,需要通过标签来限定 |
计算存储成本 | 较多,每一个业务过程需要计算存储一次 | 较少,不同业务过程融合到一起,降低存储计算量, 但是非当前业务过程的度量存在大量零值 |
父子事实的处理方式
淘宝交易父子订单的含义在前文确定粒度时有所说明,同一家店铺同时下单的多件商品,不仅每件商品有一个子订单,这几个子订单会再单独产生一个父订单。下单和支付都是在父订单粒度上完成的,比如拍下时的订单总额、支付的总额、支付的邮费,淘宝交易事务事实表在粒度选取上,按照粒度最细原则,确定为子订单,因此需要将下单总额或者支付总额分摊到每个子订单上,当然只有一个子订单时是不需要进行分摊的。这里以子订单分摊的有效下单金额和子订单分摊的支付金额加以说明。
子订单下单金额=下单商品数量*商品价格
子订单分摊有效下单金额=下单商品数量*商品价格+父订单邮费*下单分摊比例-子订单折扣-父订单折扣*下单分摊比例
下单分摊比例=(下单商品数量*原价-子订单折扣)/sum(下单商品数量*原价-子订单折扣)
子订单分摊的支付金额=父订单支付金额*支付分摊比例
支付分摊比例=(下单商品数量*原价-子订单折扣+调价)/sum(下单商品数量*原价-子订单折扣+调价)
通过分摊父订单的金额将所有业务过程的度量全部带进淘宝交易事务事实表中,包括了下单数量、商品价格、子订单折扣、下单分摊比例、父订单支付金额、父订单支付邮费、父订单折扣、子订单下单金额、子订单下单有效金额、支付分摊比例、子订单支付金额等等,将父子事实同时冗余到事务表中。
事实的设计准则
- 事实完整性
事实表包含与其描述的过程有关的所有事实,即尽可能多的获取所有的度量。在淘宝交易事务事实表中,比如支付业务过程,子订单粒度上的支付金额、支付邮费、支付红包、支付积分、支付折扣,都有所包含,覆盖全面。
- 事实一致性
在确定事务事实表的事实时,明确的存储每一个事实以确保度量的一致性。以淘宝交易事务事实表为例,下单业务过程中,有下单商品数量和商品价格两个事实,但在事实表中计算了下单金额和下单有效金额,这些可以通过商品数量乘以商品价格进行计算,虽然下游在取数时也可以通过这种方式完成计算,但是在事实表中统一计算可以保证这个度量的一致性,其他如支付过程中的分摊金额等也是类似的。
- 事实可加性
事实表确定事实时,往往会遇到非可加性的度量,比如分摊比例、利润率等,这些虽然也是下游分析的关键点,但往往在事务事实表中更多是关注可加性的事实,下游用户在聚合统计时更加方便。在淘宝交易事务事实表中,存储了分摊比例这样的度量,但更多的是存储各类金额的度量。
周期快照事实表
前面章节对于事务事实表进行了详细的阐述,同时给出了淘宝交易事务事实表的设计过程,事务事实表可以很好的跟踪一个事件,并对其进行度量,以提供丰富的分析型能力。然而,当需要一些状态度量时,比如账户余额、买卖家星级、商品库存、卖家累积交易额等状态量,需要聚集与之相关的事务才能进行识别计算;或者聚集事务无法识别,比如温度等。对于这些状态度量,事务事实表是无效率的,而这些度量也是和度量事务本身一样是有用的,因此,维度建模理论给出了第二种常见的事实表——周期性快照事实表,或者简称为快照事实表。快照事实表在确定的间隔内对实体的度量进行抽样,这样可以很容易的研究实体的度量值,而不需要聚集长期的事务历史。接下来将以淘宝交易结束后的评价数据、卖家的累积支付金额、买卖家星级等事实表的设计来介绍快照事实表在阿里巴巴数据仓库中的设计与应用。
快照事实表特性
首先简要介绍下快照事实表的一些特性,快照事实表的设计有一些区别于事务事实表设计的性质。事务事实表的粒度能以多种方式表达,但快照事实表的粒度通常以维度形式声明。事务事实表是稀疏的,但快照事实表是稠密的。事务事实表中的事实是完全可加的,但快照模型将包含至少一个用来展示半可加性质的事实。
- 用快照采样状态
快照事实表以预定的间隔采样状态度量。这种间隔,联合一个或多个维度,将被用来定义快照事实表的粒度,每行将包含记录所涉及状态的事实。
这里以淘宝交易卖家自然年汇总事实表为例进行介绍,对于淘宝活动运营小二或者卖家自身经常都需要看一些交易状态数据,比如自然年至今或者历史至今的下单金额、支付金额、支付买家数、支付商品件数等等状态度量,于卖家而言,可能是每天早上都想看一下截至昨天的一个成交情况,于小二而言,可能频繁的活动周期就需要查看一次成交情况,这些状态度量可以每天通过事务事实表进行聚集,但随着时间跨度变大,聚集效率会越来越低,因此设计事实表快照进行状态的度量,这里用于采样的周期间隔就是每天,如图所示的快照事实表记录了每个卖家的下单和支付情况。
豆包:
以下是一个例子来说明这句话(通过事务事实表进行聚集,但随着时间跨度变大,聚集效率会越来越低):
假设一个淘宝卖家的店铺,在最初的几天,事务事实表记录的交易数据量相对较少。
比如在第一天,只有 10 笔订单交易,通过事务事实表进行聚集来计算当天截至的下单金额、支付金额、支付买家数、支付商品件数等状态度量非常快速。假设这 10 笔订单的下单金额总共为 5000 元,支付金额为 4800 元,涉及 8 个买家,支付商品件数为 15 件。这个计算过程可能只需要几秒钟就能完成。
随着时间推移,到了第十天,店铺的交易变得非常频繁,事务事实表已经积累了 1000 笔订单交易数据。这时候如果要通过事务事实表进行聚集来计算从第一天到第十天这个时间跨度的状态度量,就会变得复杂得多。需要对这 1000 笔订单逐一进行统计和汇总,包括查找每笔订单的金额、买家信息、商品件数等,然后进行累加计算。这个过程可能需要几分钟甚至更长时间,相比第一天只计算当天的状态度量,效率明显降低。
如果再继续扩大时间跨度,比如计算一个自然年至今的状态度量,事务事实表中可能积累了数万甚至更多的订单交易数据。此时进行聚集计算的时间会更长,可能需要几十分钟甚至几个小时,聚集效率会越来越低。
对于淘宝活动运营小二也是类似的情况,在活动周期较短时,通过事务事实表进行聚集计算活动期间的状态度量还比较高效。但如果活动周期较长,涉及的数据量巨大,聚集计算就会变得非常耗时,效率低下。
- 快照粒度
事务事实表的粒度可以通过业务过程中涉及到的细节程度来描述,但快照事实表的粒度通常总是被多维声明,可以简单理解为快照需要采样的周期以及什么将被采样。淘宝交易卖家事实表快照中,粒度可以理解为每天针对卖家的历史截至当日下单支付金额进行快照。
当然快照周期并不一定都是按天来进行的。可以按照月或者季度来统计。比如淘宝交易有针对卖家加类目的每月汇总事实表,每月统计一次,同时维度也不仅一个,包含卖家和类目。
- 密度与稀疏性
快照和事务事实表的一个关键区别也在于密度上。事务事实表是稀疏的,只有当天发生了相应的业务过程,事实表才会记录该业务过程的事实,如下单、支付等等;而快照是稠密的,无论当天是否有业务过程发生,都会记录一行,比如针对卖家的历史至今的下单和支付金额,无论当天卖家是否有下单支付事实,都会给该卖家记录一行。稠密性是快照事实表的重要特征,如果在每个快照周期内不记录行,比如和事务事实表一样,确定状态将变得非常困难。
- 半可加性
快照事实表中收集到的状态度量都是半可加的。与事务事实表的可加事实不同,半可加事实不能根据时间维度获得有意义的汇总结果。比如对于淘宝交易事务事实表,可以对一段周期内的下单金额或者支付金额进行汇总,得到一段周期内的下单支付总额,但快照事实表在每个采样周期内是不能对状态度量进行汇总的,比如淘宝交易卖家快照事实表,无法对每天的历史至今的下单金额进行汇总,也没有汇总意义。虽然不能汇总,但可以计算一些平均值,比如计算每天的一个下单均值。
快照事实表实例
快照事实表注意项
- 事务与快照成对设计
数据仓库进行维度建模时,往往对于事务事实表和快照事实表都是成对的设计,进行互相补充,以满足更多的下游统计分析,特别在于,在事务事实表的基础上可以加工快照类事实表,如第二节所述的淘宝卖家历史至今的快照事实表,就是在事务事实表的基础上加工得到,既丰富了星型模型,又减轻了下游分析的成本。
- 附加事实
快照事实表在确定状态度量时,一般都是保存采样周期结束时的状态的度量,但是往往也有分析需求需要关注前一个采样周期结束时的状态度量,而又不愿意多次使用快照事实表,因此一般在设计周期快照事实表时,会附加一些上一采样周期的状态度量。
- 周期到日期度量
第二节介绍淘宝卖家历史至今快照事实表时,指定了统计周期是卖家历史至今的一些状态度量,比如历史截至当日的下单金额、成交金额等等,然后在实际应用中,也有需要关注自然年至今、季度至今、财年至今的一些状态度量,因此在周期快照事实表的度量确定时,也要考虑类似的度量值,以满足更多的统计分析需求。阿里巴巴数据仓库在设计周期快照事实表时,就针对多种周期到日期的度量设计了不同的快照事实表,比如淘宝卖家财年至今的下单金额、淘宝商品自然年至今的收藏次数等等。
累计快照事实表
针对淘宝交易,设计了淘宝交易下单/支付/确认收货事务型事实表,用于统计下单OR支付OR确认收货的子订单数、GMV等。但仍然有很多需求,此事务型事实表很难满足。比如统计买家下单到支付的时长、买家支付到卖家发货的时长、买家从下单到确认收货的时长等。如果使用事务型事实表统计,逻辑复杂且性能很差。对于类似研究事件之间时间间隔的需求,采用累积快照事实表可以很好的解决。
累积快照事实表设计过程
对于累积快照事实表,建模过程和事务型事实表相同,适用维度建模的步骤。下面详述一下淘宝交易累积快照事实表的设计过程,并讨论和事务型事实表的设计差异。
第一步,选择业务过程。
事实表基础章节讲解了淘宝交易订单的流转过程,主要有如下四个事件,即业务过程:买家下单、买家支付、卖家发货、买家确认收货。对于此4个业务过程,在事务型统计中只关注下单、支付和确认收货三个业务过程。而在统计事件时间间隔的需求中,卖家发货也是关键环节。所以针对淘宝交易累积快照事实表,我们选择此四个业务过程。
第二步,确定粒度。
在事务型事实表中提到,对于淘宝交易,业务需求一般是从子订单粒度进行统计分析,所以选择子订单粒度。淘宝交易事务型事实表的粒度也是子订单,但通常对于子订单的每个事件都会记录一行,对于多事件事实表如果子订单同一周期发生多次事件则记录一行;而对于累积快照表,用于考察实体的唯一实例,所以子订单在此表中只有一行记录,事件发生时,对此实例进行更新。
第三步,确定维度。
同事务型事实表相同,维度主要有买家、卖家、店铺、商品、类目、发货地区、收货地区等。四个业务过程对应的时间字段,格式为日期+时间,分别为下单时间、支付时间、发货时间、确认收货时间,会对应日期维度表,下图中未标示。实际使用时会使用视图或SQL别名的方式表示四个日期角色维度。类似于发货地区维度和收货地区维度。
交易订单表中,存在很多订单相关的属性,如订单的类型、子类型、支付状态、物流状态、attributes、options等。对于类似的属性字段,无法归属到已有的商品等维度中,所以新建杂项维度存放。在数据仓库建模理论中,杂项维度一般无自然键,一般是可枚举值的组合,对于每个组合生成一个代理键。但实际建模中,存在很多不可枚举值,且对于每个订单都不相同,如订单的attributes和options属性。所以实际中杂项维度设计时,也可以直接使用自然键标示具体的维度值,如下图中所示的子订单维度和父订单维度。
第四步,确定事实。
对于累积快照事实表,需要将各业务过程对应的事实均放入事实表。比如,对于淘宝交易累积快照事实表,包含各业务过程对应的事实,如下单对应的下单金额,支付对应的折扣、邮费和支付金额,确认收货对应的金额等。累积快照解决的最重要问题是统计不同业务过程之间的时间间隔,建议将每个过程的时间间隔作为事实放在事实表中。在淘宝交易累积快照事实表建模中,由于每个过程的时间间隔计算逻辑简单,并未加入事实表中。
第五步,退化维度。
大数据的事实表模型设计中,更多的考虑是提高下游用户的使用效率,降低数据获取的复杂性,减少关联的表数量。一方面,存储成本的降低,而CPU成本相比之下仍然较高;另一方面,大数据时代,很多维度比事实表还大,如淘宝几十亿的商品、几个亿的买家等,在分布式数据仓库系统中,事实表和维度表关联的成本很高。所以在传统的维度模型设计完成之后,在物理实现中将各维度的常用属性退化到事实表中,以大大提高对事实表的过滤查询、统计聚合等操作的效率,具体详情不再赘述。
累积快照事实表特点
(1)数据不断更新
事务型事实表记录事务发生时的状态,对于实体的某一实例不再更新;而累积快照事实表则对实体的某一实例定期更新。以淘宝交易为例,下表通过实例展示了事务型事实表的情况,假设采用多事物事实表:对于order1订单,2015-11-12支付之后,产生新的支付记录,2015-11-11的数据不会更新。截止2015-11-13日,买家确认收货之后,共产生3条记录。
2015-11-11下单:
日期 | 子订单ID | 下单时间 | 支付时间 | 确认收货时间 | 相关事实 |
---|---|---|---|---|---|
20151111 | order1 | 2015-11-11 19:23:58 | NULL | NULL | … |
2015-11-12支付:
日期 | 子订单ID | 下单时间 | 支付时间 | 确认收货时间 | 相关事实 |
---|---|---|---|---|---|
20151112 | order1 | 2015-11-11 19:23:58 | 2015-11-12 10:00:58 | NULL | … |
2015-11-13确认收货:
日期 | 子订单ID | 下单时间 | 支付时间 | 确认收货时间 | 相关事实 |
---|---|---|---|---|---|
20151113 | order1 | 2015-11-11 19:23:58 | 2015-11-12 10:00:58 | 2015-11-13 09:00:00 | … |
对于累积快照事实表,则只有一条记录,针对此记录不断更新。
日期 | 子订单ID | 下单时间 | 支付时间 | 确认收货时间 | 相关事实 |
---|---|---|---|---|---|
20151113 | order1 | 2015-11-11 19:23:58 | 2015-11-12 10:00:58 | 2015-11-13 09:00:00 | … |
(2)多业务过程日期
通过上面的实例,可以看到累积快照事实表典型特征:多个业务过程日期。累积快照适用于具有较明确起止时间的短生命周期的实体。比如交易订单、物流订单等,对于实体的每一个实例,都会经历从诞生到消亡等一系列步骤。对于商品、用户等具有长生命周期的实体,一般采用周期性快照事实表更合适。
累积快照事实表典型特征是多个业务过程日期,用于计算业务过程之间的时间间隔。但结合阿里巴巴数据仓库模型建设的经验,对于此表,还有一个重要作用是保存全量数据。对于淘宝交易,需要保留历史截止当前的所有交易数据,其中一种方式是在ODS层保留和源系统结构完全相同的数据;但由于使用时需要关联维度,较为麻烦。所以在公共明细层,需要保留一份全量数据,淘宝交易累积快照事实表就承担了这样的作用。存放了加工后的事实,并将各维度常用属性和订单杂项维度退化到此表。常用于数据探查、统计分析、数据挖掘等。
累积快照事实表特殊处理
- 非线性过程
如前面章节提到,淘宝交易流程一般经过如下四个业务过程:
下单->支付->发货->确认收货。
但并不是所有的交易都会走此流程。比如买家下单之后不支付,可以自己关闭订单或者经过一段时间后系统自动关闭订单,此时交易流程如下:
下单->关闭订单。
买家下单并支付之后,可以申请退款,卖家同意之后,交易关闭,此时交易流程如下:
下单->支付->关闭订单。
特殊情况下,流程可能会回转。比如在退款过程中,正常流程可能是:
买家申请退款->卖家同意退款->退款达成。
或者是:
买家申请退款->卖家不同意退款->退款关闭。
但由于买家和卖家之间未达成协议,卖家不同意买家的退款,此时流程可能是:
买家申请退款->卖家不同意退款->买家申请退款->卖家不同意退款…,一直到退款达成或关闭。
针对非线性过程,处理情况主要有以下几种:
(1)业务过程的统一。比如流程结束的标记的统一,最开始设计交易累积快照事实表时,以交易完成作为结束标记;进一步了解业务之后,发现交易关闭也是交易结束的一个分支。所以将交易结束作为流程结束、实体消亡的标记,包括交易完成和交易结束两种情况。
(2)针对业务关键里程碑构建全面的流程。比如淘宝交易,全流程可能是下单->支付->发货->确认收货。对于没有支付或没有发货的交易订单,全流程仍然可以覆盖,相关业务过程的时间字段和事实置空。
(3)循环流程的处理。主要问题是解决一个业务过程存在多个里程碑日期的问题。使用业务过程第一次发生的日期还是最后一次发生的日期?决定权在于商业用户,而不是设计或开发人员。
- 多源过程:
针对多业务过程建模时,业务过程可能来自于不同的系统或者来源于不同表,其对于累积快照的模型设计没有影响,会影响ETL开发的复杂程度。对于淘宝交易累积快照事实表,除了上述提到的下单-支付-发货-确认收货流程,假设需要关注交易子订单退款业务或者物流业务。此时会涉及交易、售后、物流三个业务源系统。
退款部分业务流程如下:
下单-支付-买家申请退款-卖家同意退款-退款达成-交易关闭
或者:
下单-支付-发货-买家申请退款-卖家同意退款-退款达成-交易关闭
或者
下单-支付-发货-买家申请退款-卖家不同意退款-退款取消-交易成功
针对多源业务建模,主要考虑事实表的粒度问题。对于淘宝交易累积快照事实表,其粒度是交易子订单。对于退款,由于每笔子订单可能存在多次退款,此时如果需要将退款相关的业务过程加入模型中,需要和商业用户确定存在多次退款时,如何取舍,确保模型粒度不变。
- 业务过程取舍:
上一节提到的退款业务是简化的流程,比较完整的业务流程如下:
申请退款-申请小二介入-小二实际介入-卖家同意退款-退款完结。
将退款相关的业务流程设计进入交易累积快照事实表时,是否需要所有的业务过程。答案是否定的。当拥有大量的业务过程时,模型的实现复杂度会增加,特别是对于多源业务过程,模型的耦合度过高。此时需要根据商业用户需求,选取关键的里程碑。
累积快照事实表物理实现
逻辑模型和物理模型密不可分,针对累积快照事实表模型设计,其有不同的实现方式。
- 第一种方式是全量表的形式。
此全量表一般为日期分区表,每天的分区存储昨天的全量数据和当天的增量数据合并的结果,保证每条记录的状态最新。此种方式适用于全量数据较少的情况。如果数据量很大,此全量表数据量不断膨胀,存储了大量永远不再更新的历史数据。对ETL性能和分析统计性能影响较大。
- 第二种方式是全量表的变化形式。
主要针对事实表数据量很大的情况。较短生命周期的业务实体一般从产生至消亡都有一定的时间间隔。可以测算此时间间隔,或者根据商业用户的需求确定一个相对较大的时间间隔。比如,针对交易订单,我们以200天作为订单从产生到消亡的最大间隔。设计最近200天的交易订单累积快照事实表,每天的分区存储最近200天的交易订单;而200天之前的订单则按照gmt_create创建分区存储在归档表中。此方式存在的问题之一是200天的全量表根据商业需求需要保留多天的分区数据,而由于数据量较大,存储消耗较大。
- 第三种方式是以业务实体的结束时间分区。
每天的分区数据存放当天结束的数据,设计一个时间非常大的分区,比如3000-12-31,存放截止当前未结束的数据。由于每天将当天结束的数据归档至当天分区,时间非常大的分区数据量不会很大,ETL性能较好;并且无存储的浪费,对于业务实体的某具体实例,在这张表的全量数据中唯一。比如,对于交易订单,在交易累积快照事实表中唯一。
针对第三种方式,可能存在极特殊情况下,业务系统无法标示业务实体的结束时间的情况。比如业务系统调用接口很多、依赖的系统复杂,最终无法判断业务实体是否已经消亡。比如菜鸟的物流订单,由于其依赖物流公司的数据,和大量的物流公司存在接口。按照约定,物流公司会向菜鸟回传运单的流转信息,但无法保证100%准确;且一般为批量回传,菜鸟订单系统根据批量数据更新物流订单的结束标示几乎无法实现。前台业务系统没有物流订单的结束时间,如果设计物流订单累积快照事实表?针对此问题,可以两种处理方式。
第一,使用相关业务系统的业务实体的结束标示作为此业务系统的结束标示。比如针对物流订单,可以使用交易订单。理论上交易订单完结,物流订单则已经完结。
第二,和前端业务系统确定口径或使用前端归档策略。累积快照事实表针对业务实体一般是具有较短生命周期的,和前端业务系统确定口径,确定从业务实体的产生至消亡的最大间隔。另外,针对大量事实数据,前端系统定期会对历史数据进行归档,避免业务库性能的降低,对于这种情况,可以使用前端系统的归档时间作为业务实体的结束日期。
三种事实表的比较
通过前面的章节,对数据仓库三种事实表有了详细的理解。一些业务过程可能只需要一种事实表,但另外一些过程可能需要两种或三种事实表。三种事实表相互补充,给出业务的完整描述。下表对三种事实表进行了比较:
事务型 | 周期快照型 | 累积快照型 | |
---|---|---|---|
时期/时间 | 离散事务时间点 | 以有规律的、可预测的间隔产生快照 | 用于时间跨度不确定的不断变化的工作流 |
日期维度 | 事务日期 | 快照日期 | 相关业务过程涉及的多个日期 |
粒度 | 每行代表实体的一个事务 | 每行代表某时间周期的一个实体 | 每行代表一个实体的生命周期 |
事实 | 事务事实 | 累积事实 | 相关业务过程事实和时间间隔事实 |
事实表加载 | 插入 | 插入 | 插入与更新 |
事实表更新 | 不更新 | 不更新 | 业务过程变更时更新 |
事务事实表记录的事务层面的事实,用于跟踪业务过程的行为,并支持几种描述行为的事实,保存的是最原子的数据,也称“原子事实表”。事务事实表中的数据在事务事件发生后产生,数据的粒度通常是每个事务一条记录。一旦事务被提交,事实表数据被插入,数据就不再进行更改,其更新方式为增量更新。
周期快照事实表以具有规律性的、可预见的时间间隔来记录事实,如余额、库存、层级、温度等,时间间隔如每天、每月、每年等等。典型的例子如库存日快照表等。周期快照事实表的日期维度通常是记录时间段的终止日,记录的事实是这个时间段内一些聚集事实值或状态度量。事实表的数据一旦插入即不能更改,其更新方式为增量更新。
累计快照被用来跟踪实体的一系列业务过程的进展情况,它通常具有多个日期字段,用于研究业务过程中的里程碑过程的时间间隔。另外,它还会有一个用于指示最后更新日期的附加日期字段。由于事实表中许多日期在首次加载时是不知道的,而且这类事实表在数据加载完后,是可以对它进行更新的,来补充业务状态变更时的日期信息和事实。
无事实的事实表
维度模型中,事实表用事实来度量业务过程,不包含事实或度量的事实表称为无事实的事实表。虽然没有明确的事实,却可以用来支持业务过程的度量。
常见的无事实的事实表主要有如下两种:
第一种,事件类,记录事件的发生。在阿里巴巴数据仓库中,最常见的是日志类事实表。比如用户的浏览日志,某会员某时间点浏览了淘宝首页、某会员某时间点浏览了某卖家的店铺中的某商品详情页等。对于每次点击,其事实为1,但一般不会保存此事实。
第二种,条件、范围或资格类,记录维度与维度多对多之间的关系。比如客户和销售人员的分配情况、产品的促销范围等。