迟到的事实概述
- 数据仓库通常建立于一种理想的假设情况下, 这就是数据仓库的度量(事实记录) 与度量的环境(维度记录) 同时出现在数据仓库中。 当同时拥有事实记录和正确的当前维度行时, 就能够从容地首先维护维度键, 然后在对应的事实表行中使用这些最新的键。 然而, 各种各样的原因会导致需要ETL系统处理迟到的事实数据。 例如, 某些线下的业务, 数据进入操作型系统的时间会滞后于事务发生的时间。 再或者出现某些极端情况, 如源数据库系统出现故障, 直到恢复后才能补上故障期间产生的数据。
- 在销售订单示例中, 晚于订单日期进入源数据的销售订单可以看作是一个迟到事实的例子。 销售订单数据被装载进其对应的事实表时, 装载日期晚于销售订单产生的日期, 因此是一个迟到的事实。 本例中因为定期装载的是前一天的数据, 所以这里的“晚于”指的是事务数据延迟两天及其以上才到达ETL系统。
- 必须对标准的ETL过程进行特殊修改以处理迟到的事实。 首先, 当迟到度量事件出现时, 不得不反向搜索维度表历史记录, 以确定事务发生时间点的有效的维度代理键, 因为当前的维度内容无法匹配输入行的情况。 此外, 还需要调整后续事实行中的所有半可加度量, 例如, 由于迟到的事实导致客户当前余额的改变。 迟到事实可能还会引起周期快照事实表的数据更新。 例如 数仓--DW--Hadoop数仓实践Case-13-周期快照事实表节讨论的月销售周期快照表, 如果2016年6月的销售订单金额已经计算并存储在month_end_sale_order_fact快照表中, 这时一个迟到的6月订单在7月某天被装载, 那么2016年6月的快照金额必须因迟到事实而重新计算。
- 下面就以销售订单数据仓库为例, 说明如何处理迟到的事实。
修改数据仓库模式
- 借助 数仓--DW--Hadoop数仓实践Case-13-周期快照事实表中建立的月度周期快照表,其数据源自已经处理过的销售订单事务事实表。 因此为了确定事实表中的一条销售订单记录是否是迟到的, 需要把源数据中的登记日期列装载进销售订单事实表。 为此要在销售订单事实表上添加登记日期代理键列。 为了获取登记日期代理键的值, 还要使用维度角色扮演技术添加登记日期维度表。
- 执行下面的脚本在销售订单事实表里添加名为entry_date_sk的日期代理键列, 并且从日期维度表创建一个叫做entry_date_dim的数据库视图。
use dw;
-- 在事务事实表中添加登记日期代理键列
alter table
sales_order_fact rename to sales_order_fact_old;
-- 创建表
create table
sales_order_fact
(
order_number int comment'order number',
customer_sk int comment'customer SK',
customer_zip_code_sk int comment'customer zip code SK',
shipping_zip_code_sk int comment'shipping zip code SK',
product_sk int comment'product SK',
sales_order_attribute_sk int comment'sales order attribute SK',
order_date_sk int comment'order date SK',
entry_date_sk int comment 'entry date SK',
allocate_date_sk int comment'allocate date SK',
allocate_quantity int comment'allocate quantity',
packing_date_sk int comment'packing date SK',
packing_quantity int comment'packing quantity',
ship_date_sk int comment'ship date SK',
ship_quantity int comment'ship quantity',
receive_date_sk int comment'receive date SK',
receive_quantity int comment'receive quantity',
request_delivery_date_sk int comment'request delivery date SK',
order_amount decimal(10,2) comment'order amount',
order_quantity int comment'order quantity'
)
clustered by(order_number) into8 buckets
stored as
orc tblproperties ('transactional'='true');
-- 将数据插入到表中
insert into
sales_order_fact
select
order_number,
customer_sk,
customer_zip_code_sk,
shipping_zip_code_sk,
product_sk,
sales_order_attribute_sk,
order_date_sk,
null,
allocate_date_sk,
allocate_quantity,
packing_date_sk,
packing_quantity,
ship_date_sk,
ship_quantity,
receive_date_sk,
receive_quantity,
request_delivery_date_sk,
order_amount,
order_quantity
fromsales_order_fact_old;
drop table
sales_order_fact_old;
-- 建立登记日期维度视图
create view
entry_date_dim
(entry_date_sk, entry_date, month_name, month
, quarter, year
)
as select
date_sk, date, month_name, month, quarter, year
from
date_dim
;
修改定期装载脚本
- 在创建了登记日期维度视图, 并给销售订单事实表添加了登记日期代理键列以后, 需要修改数据仓库定期装载脚本来装载登记日期。 下面显示了修改后的regular_etl.sql定期装载脚本(只列出修改的部分) 。 注意sale_order源数据表及其对应的过渡表中都已经含有登记日期, 只是以前没有将其装载进数据仓库。
-- 修改定期装载脚本
insert into
sales_order_fact
select
a.order_number,
c.customer_sk,
i.customer_zip_code_sk,
j.shipping_zip_code_sk,
d.product_sk,
g.sales_order_attribute_sk,
e.order_date_sk,
h.entry_date_sk,
null, null, null, null, null, null, null, null,
f.request_delivery_date_sk,
order_amount,
quantity
from
rds.sales_order a,
customer_dim c,
product_dim d,
order_date_dim e,
request_delivery_date_dim f,
sales_order_attribute_dim g,
customer_zip_code_dim i,
shipping_zip_code_dim j,
entry_date_dim h,
rds.customer k,
rds.cdc_time l
where
a.order_status = 'N'
and
a.customer_number = c.customer_number
and
a.status_date >= c.effective_date
and
a.status_date < c.expiry_date
and
a.customer_number = k.customer_number
and
k.customer_zip_code = i.customer_zip_code
and
a.status_date >= i.effective_date
and
a.status_date <= i.expiry_date
and
k.shipping_zip_code = j.shipping_zip_code
and
a.status_date >= j.effective_date
and
a.status_date <= j.expiry_date
and
a.product_code = d.product_code
and
a.status_date >= d.effective_date
and
a.status_date < d.expiry_date
and to_date
(a.status_date) = e.order_date
and
to_date(a.entry_date) = h.entry_date
and
to_date(a.request_delivery_date) = f.request_delivery_date
and
a.verification_ind = g.verification_ind
and
a.credit_check_flag = g.credit_check_flag
and
a.new_customer_ind = g.new_customer_ind
and
a.web_order_flag = g.web_order_flag
and
a.entry_date >= l.last_load
and a.entry_date < l.current_load ;
-- 更新分配库房、 打包、 配送、 收货4种订单状态的时间代理键和度量,
-- 也要加上entry_date_sk
- 本节开头曾经提到, 需要为迟到的事实行获取事务发生时间点的有效的维度代理键。 在装载脚本中使用销售订单过渡表的状态日期字段限定当时的维度代理键。 例如, 为了获取事务发生时的客户代理键, 筛选条件为:
status_date >= customer_dim.effective_date
and
status_date < customer_dim.expiry_date
- 之所以可以这样做, 原因在于本示例满足以下两个前提条件: 在最初源数据库的销售订单表中, status_date存储的是状态发生时的时间; 维度的生效时间与过期时间构成一条连续且不重叠的时间轴, 任意status_date日期只能落到唯一的生效时间、 过期时间区间内。
修改装载月度周期快照事实表
- 数仓--DW--Hadoop数仓实践Case-13-周期快照事实表创建的month_sum.sql脚本文件用于装载月销售周期快照事实表。 迟到的事实记录会对周期快照中已经生成的月销售汇总数据产生影响, 因此必须做适当的修改。
- 月销售周期快照表存储的是某月某产品汇总的销售数量和销售金额, 表中有月份代理键、 产品代理键、 销售金额、 销售数量4个字段。 由于迟到事实的出现, 需要将事务事实表中的数据划分为三类: 非迟到的事实记录; 迟到的事实, 但周期快照表中尚不存在相关记录; 迟到的事实, 并且周期快照表中已经存在相关记录。 对这三类事实数据的处理逻辑各不相同, 前两类数据需要汇总后插入快照表, 而第三种情况需要更新快照表中的现有数据。 下面我们对修改后的month_sum.sql文件分解说明。
drop table if exists tmp;
create table tmp as
select a.order_month_sk order_month_sk,
a.product_sk product_sk,
a.month_order_amount + b.order_amount month_order_amount,
a.month_order_quantity + b.order_quantity month_order_quantity
from month_end_sales_order_fact a,
(select d.month_sk month_sk,
a.product_sk product_sk,
sum(order_amount) order_amount,
sum(order_quantity) order_quantity
from sales_order_fact a,
order_date_dim b,
entry_date_dim c,
month_dim d
where a.order_date_sk = b.order_date_sk
and a.entry_date_sk = c.entry_date_sk
and c.month = month(${hivevar:pre_month_date})
and c.year = year(${hivevar:pre_month_date})
and b.month = d.month
and b.year = d.year
and b.order_date <> c.entry_date
group by d.month_sk , a.product_sk) b
where a.product_sk = b.product_sk
and a.order_month_sk = b.month_sk;
delete from month_end_sales_order_fact
where exists
(select 1
from tmp t2
where month_end_sales_order_fact.order_month_sk = t2.order_month_sk
and month_end_sales_order_fact.product_sk = t2.product_sk);
insert into month_end_sales_order_fact select * from tmp;
- 按事务发生时间的先后顺序, 我们先处理第三种情况。 为了更新周期快照表数据, 需要创建一个临时表。 子查询用于从销售订单事实表中获取所有上个月录入的, 并且是迟到的数据行的汇总, 用b.order_date <> c.entry_date作为判断迟到的条件。 本示例中实际可以去掉这条判断语句, 因为只有迟到事实会对已有的快照数据造成影响。 外层查询把具有相同产品代理键和月份代理键的迟到事实的汇总数据加到已有的快照数据行上, 临时表中存储这个查询的结果。 注意产品代理键和月份代理键共同构成了周期快照表的逻辑主键, 可以唯一标识一条记录; 之后使用先删除再插入的方式更新周期快照表。 从周期快照表删除数据的操作也是以逻辑主键匹配作为过滤条件。
insert into
month_end_sales_order_fact
select
d.month_sk, a.product_sk, sum(order_amount), sum(order_quantity)
from
sales_order_fact a,
order_date_dim b,
entry_date_dim c,month_dim d
where
a.order_date_sk = b.order_date_sk
and
a.entry_date_sk = c.entry_date_sk
and
c.month= month(${hivevar:pre_month_date})
and
c.year= year(${hivevar:pre_month_date})
and
b.month= d.month
and
b.year= d.year
and
not exists
(select
1
from
month_end_sales_order_fact p
where
p.order_month_sk = d.month_sk
and
p.product_sk = a.product_sk)
group by
d.month_sk , a.product_sk
;
- 上面这条语句将第一、 二类数据统一处理。 使用相关子查询获取所有上个月新录入的, 并且在周期快照事实表中尚未存在的产品销售月汇总数据, 插入到周期快照表中。 销售订单事实表的粒度是每天, 而周期快照事实表的粒度是每月, 因此必须使用订单日期代理键对应的月份代理键进行比较。
迟到的事实总结
- 迟到的事实似乎是针对周期性快照事实表才需要处理的?
- 这个迟到的事实需要在模型建立的时候考虑进去,模型建立之初需要事实考虑的。
- 数仓模型的建立很考验实力啊。
- 还是对模型多思考才行。