4. 递归
数据仓库中的关联实体经常表现为一种“父—子”关系。在这种类型的关系中,一个父亲可能有多个孩子,而一个孩子只能属于一个父亲。例如,通常一名企业员工只能被分配到一个部门,而一个部门会有很多员工。“父—子”之间形成一种递归型树结构,是一种比较理想和灵活的存储层次关系的数据结构。本小节说明一些递归处理的问题,包括数据装载、树的展开、递归查询、树的平面化等技术实现。销售订单数据仓库中没有递归结构,为了保持示例的完整性,将会使用另一个与业务无关的通用示例。
(1)建立示例表并添加实验数据
-- 在mysql的source库中建立源表
use source;
create table tree (c_child int, c_name varchar(100),c_parent int);
create index idx1 on tree (c_parent);
create unique index tree_pk on tree (c_child);
-- 递归树结构,c_child是主键,c_parent是引用c_child的外键
alter table tree add (constraint tree_pk primary key (c_child));
alter table tree add (constraint tree_r01 foreign key (c_parent) references tree (c_child));
-- 添加数据
insert into tree (c_child, c_name, c_parent)
values (1, '节点1', null),(2, '节点2', 1),(3, '节点3', 1),(4, '节点4', 1),
(5, '节点5', 2),(6, '节点6', 2),(7, '节点7', 2),(8, '节点8', 3),
(9, '节点9', 3),(10, '节点10', 4),(11, '节点11', 4);
commit;
-- 在hive的rds库中建立过渡表
use rds;
create table tree (c_child int,c_name string,c_parent int)
row format delimited fields terminated by ','
stored as textfile;
-- 在hive的dw库中建立相关维度表
use dw;
create table tree_dim
(sk int,c_child int,c_name string ,c_parent int,
version int,effective_date date,expiry_date date)
clustered by (sk) into 8 buckets
stored as orc tblproperties ('transactional'='true');
以上脚本用于建立递归结构的测试数据环境。我们在MySQL的源库中建立了名为tree的表,并插入了11条测试数据。该表只有子节点、节点名称、父节点三个字段,其中父节点是引用子节点的外键,它们构成一个典型的递归结构。可以把tree表想象成体现员工上下级关系的一种抽象。数据仓库过渡区的表结构和源表一样,使用Hive表缺省的文本文件格式。数据仓库维度表使用ORC存储格式,为演示SCD2,除了对应源表的三个字段,还增加了代理键、版本号、生效时间和过期时间四个字段。初始时源表数据的递归树结构如图8-17所示。
图8-17 递归树的初始数据
(2)数据装载 递归树结构的本质是,在任意时刻,每个父—子关系都是唯一的。操作型系统通常只维护层次树的当前视图。因此,输入数据仓库的数据通常是当前层次树的时间点快照,这就需要由ETL过程来确定发生了哪些变化,以便正确记录历史信息。为了检测出过时的父—子关系,必须通过孩子键进行查询,然后将父亲作为结果返回。在这个例子中,对tree表采用整体拉取模式抽数据,tree_dim表的c_name和c_parent列上使用SCD2装载类型。也就是说,把c_parent当做源表的一个普通属性,当一个节点的名字或者父节点发生变化时,都增加一条新版本记录,并设置老版本的过期时间。这样的装载过程和销售订单的例子并无二致。实现tree_dim维度表的初始装载和定期装载的Kettle作业如图8-18所示。
图8-18 初始装载和定期装载递归树的作业
两个Sqoop import作业项都是如下设置,全量覆盖抽取tree表的数据:
--connect jdbc:mysql://node3:3306/source --delete-target-dir --password 123456 --table tree --target-dir /user/hive/warehouse/rds.db/tree --username root
初始装载的SQL作业项如下:
use dw;
truncate table tree_dim;
insert into tree_dim
select row_number() over (order by t1.c_child) + t2.sk_max,
t1.c_child, t1.c_name, t1.c_parent, 1, '2020-03-01', '2200-01-01'
from rds.tree t1
cross join (select coalesce(max(sk),0) sk_max from tree_dim) t2;
初始装载的过程很简单,用Sqoop全量抽取数据到过渡区,然后装载进数据仓库,同时生成代理键和其它字段。有了前面篇章的基础,这些都很好理解。定期装载的“设置系统日期”调用的转换与销售订单示例相同,设置系统变量和时间戳表数据。SQL作业项如下,注意勾选“使用变量替换”属性。
use dw;
update tree_dim set expiry_date = '${PRE_DATE}'
where tree_dim.sk in
(select a.sk
from (select sk,c_child,c_name,c_parent
from tree_dim where expiry_date = '${MAX_DATE}') a
left join rds.tree b on a.c_child = b.c_child
where b.c_child is null
or (!(a.c_name <=> b.c_name) or !(a.c_parent <=> b.c_parent) ));
insert into tree_dim
select row_number() over (order by t1.c_child) + t2.sk_max,
t1.c_child, t1.c_name, t1.c_parent,
t1.version, t1.effective_date, t1.expiry_date
from (select t2.c_child c_child, t2.c_name c_name, t2.c_parent c_parent,
t1.version + 1 version, '${PRE_DATE}' effective_date, '${MAX_DATE}' expiry_date
from tree_dim t1
inner join rds.tree t2 on t1.c_child = t2.c_child and t1.expiry_date = '${PRE_DATE}'
left join tree_dim t3 on t1.c_child = t3.c_child and t3.expiry_date = '${MAX_DATE}'
where (!(t1.c_name <=> t2.c_name) or !(t1.c_parent <=> t2.c_parent)) and t3.sk is null) t1
cross join (select coalesce(max(sk),0) sk_max from tree_dim) t2;
insert into tree_dim
select row_number() over (order by t1.c_child) + t2.sk_max,
t1.c_child, t1.c_name, t1.c_parent, 1, '${PRE_DATE}', '${MAX_DATE}'
from (select t1.* from rds.tree t1
left join tree_dim t2 on t1.c_child = t2.c_child where t2.sk is null) t1
cross join (select coalesce(max(sk),0) sk_max from tree_dim) t2;
update rds.cdc_time set last_load=current_load;
这段代码和销售订单的SCD2处理类似。下面测试装载过程。
1. 执行初始装载Kettle作业。 此时查询dw.tree_dim表,可以看到新增了全部11条记录。
2. 修改源表所有节点的名称。
-- 修改名称
update tree set c_name = concat(c_name,'_1');
3. 设置Kettle所在服务器的系统日期 date -s "2020-10-27 `date +%T`" ,然后执行定期装载Kettle作业。 此时查询dw.tree_dim表,可以看到维度表中共有22条记录,其中新增11条当前版本记录,老版本的11条记录的过期时间字段被设置为'2020-10-26'。4. 修改源表部分节点的名称,并新增两个节点。
-- 修改名称
update tree set c_name = replace(c_name,'_1','_2')
where c_child in (1, 3, 5, 8, 11);
-- 增加新的根节点,并改变原来的父子关系
insert into tree values (12, '节点12', null), (13, '节点13', 12);
update tree
set c_parent = (case when c_child = 1 then 12 else 13 end)
where c_child in (1,3);
此时源表数据的递归树结构如图8-19所示。
图8-19 新增了根节点
5. 设置Kettle所在服务器的系统日期 date -s "2020-10-28 `date +%T`" ,然后执行定期装载Kettle作业。 此时查询dw.tree_dim表可以看到,现在维度表中共有29条记录,其中新增7条当前版本记录(5行因为改名新增版本,其中1、3既改名又更新父子关系,2行新增节点),更新了5行老版本的过期时间,被设置为'2020-10-27'。
6. 修改源表部分节点的名称,并删除三个节点。
set foreign_key_checks=0;
update tree
set c_name = (case when c_child = 2 then '节点2_2' else '节点3_3' end)
where c_child in (2,3);
delete from tree where c_child in (10,11,4);
此时源表数据的递归树结构如图8-20所示。
图8-20 删除子树
7. 设置Kettle所在服务器的系统日期 date -s "2020-10-29 `date +%T`" ,然后执行定期装载Kettle作业。 此时查询dw.tree_dim表可以看到,现在维度表中共有31条记录,其中新增2条当前版本记录(因为改名),更新了5行老版本的过期时间(2行因为改名,3行因为节点删除),被设置为'2020-10-28'。
(3)树的展开 有些BI工具的前端不支持递归,这时递归层次树的数据交付技术就是“展开”(explode)递归树。展开是这样一种行为,一边遍历递归树,一边产生新的结构,该结构包含了贯穿树中所有层次的每个可能的关系。展开的结果是一个非递归的关系对表,该表也可能包含描述层次树中关系所处位置的有关属性。将树展开消除了对递归查询的需求,因为层次不再需要自连接。当按这种表格形式将数据交付时,使用简单的SQL查询就可以生成层次树报表。Kettle转换中的“Closure generator”步骤可以简单处理树展开,如图8-21所示。
图8-21 处理树展开的转换
“表输入”查询tree表,将数据输出到“Closure generator”步骤。“Closure generator”步骤中设置父字段、子字段,以及新增的距离字段名,表示父子之间相差的级别。展开后的表中不再有递归结构,每行表示一对父子关系。预览“Closure generator”步骤的数据,可以看到记录数由rds.tree中的10条变为展开后的31条,部分展开后记录如下所示:
c_parent c_child distince
1 1 0
12 1 1
1 2 1
2 2 0
12 2 2
3 3 0
12 3 2
13 3 1
...
(4)树的遍历 通过“Closure generator”步骤和排序、分组等步骤,能够解决树的遍历问题,转换如8-22所示。前两个步骤就是树展开的两个步骤,“排序记录”步骤按c_child和distince排序,“分组”步骤按c_child分组,将同一组的c_parent拼接成字符串。
图8-22 遍历树的转换
最后“分组”步骤的数据预览如下,实现了从下至上的树遍历。
+---------+---------------+
| c_child | c_parent |
+---------+---------------+
| 1 | 1, 12 |
| 2 | 2, 1, 12 |
| 3 | 3, 13, 12 |
| 5 | 5, 2, 1, 12 |
| 6 | 6, 2, 1, 12 |
| 7 | 7, 2, 1, 12 |
| 8 | 8, 3, 13, 12 |
| 9 | 9, 3, 13, 12 |
| 12 | 12 |
| 13 | 13, 12 |
+---------+---------------+
(5)递归树的平面化 递归树适合于数据仓库,而非递归结构则更适合于数据集市。前面的递归树展开用于消除递归查询,但缺点在于为检索与实体相关的属性必须执行额外的连接操作。对于层次树来说,很常见的情况是,层次树元素所拥有的唯一属性就是描述属性,如本例中的c_name字段,并且树的最大深度是固定的,本例是4层。对这种情况,最好是将层次树作为平面化的1NF结或者2NF结构交付给数据集市。这类平面化操作对于平衡的层次树发挥得最好。将缺失的层次置空可能会形成不整齐的层次树,因此它对深度未知的层次树(列数不固定)来说并不是一种有用的技术。本例递归树平面化的Kettle转换如图8-23所示。
图8-23 递归树平面化的转换
第一行的四个步骤就是树遍历的四个步骤。“字符串替换”步骤将c_parent字段中的逗号替换掉,并输出为新字段c。“计算器”步骤的设置如图8-24所示。
图8-24 计算树的层数
length_c为c字段的长度,length_c1为c_parent字段的长度,num_level为length_c1 - length_c的值,即c_parent中逗号的个数,也即树的层数。“过滤记录”步骤判断num_level是否等于3,将不是叶子节点的记录过滤掉,叶子节点对应的数据输出到“JavaScript代码”步骤,其中的JS代码如下:
var level0="";
var level1="";
var level2="";
var level3="";
var strArray=new Array();
strArray=c_parent.split(", ");
var level0=strArray[0];
var level1=strArray[1];
var level2=strArray[2];
var level3=strArray[3];
该步骤用split函数,将c_parent一列,按逗号分隔为level0、level1、level2、level3四列,并将它们作为新字段添加到输出流中。最后的“字段选择”步骤只选择level3、level2、level1、level0作为最终输出,其预览数据如下。
12 1 2 5
12 1 2 6
12 1 2 7
12 13 3 8
12 13 3 9
如果后续需要获取节点名称,可以在转换的最后加一个表输出步骤,将递归树平面化的结果存入表中,假设为rds.tree_complanate。然后将转换封装在一个Kettle作业里,后面加一个SQL作业项执行如下关联查询即可。
select t0.c_0 c_0,t1.c_name c_0_name,t0.c_1 c_1,t2.c_name c_1_name,
t0.c_2 c_2,t3.c_name c_2_name,t0.c_3 c_3,t4.c_name c_3_name
from rds.tree_complanate t0
inner join (select * from rds.tree) t1 on t0.c_0= t1.c_child
inner join (select * from rds.tree) t2 on t0.c_1= t2.c_child
inner join (select * from rds.tree) t3 on t0.c_2= t3.c_child
inner join (select * from rds.tree) t4 on t0.c_3= t4.c_child;
再次强调,不要轻易使用Kettle的“数据库查询”或“数据库连接”步骤,它们会对每一行输入执行一次SQL查询。对于数据流的多行输入,尤其在Hive这类Hadoop上的数据库上执行这些步骤,将会慢到无法容忍的地步。
本节讨论一种称为退化维度的技术,该技术减少维度的数量,简化维度数据仓库模式。简单的模式比复杂的更容易理解,也有更好的查询性能。
有时,维度表中除了业务主键外没有其它内容。例如,在我们的销售订单示例中,订单维度表除了订单号,没有任何其它属性,而订单号是事务表的主键。我们将这种维度称为退化维度。业务系统中的主键通常是不允许修改的。销售订单只能新增,不能修改已经存在的订单号,也不会删除订单记录。因此订单维度表也不会有历史数据版本问题。退化维度常见于事务和累计快照事实表中,后面一篇将讨论累积快照等几个事实表技术。
销售订单事实表中的每行记录都包括作为退化维度的订单号代理键。在操作型系统中,销售订单表是最细节事务表,订单号是订单表的主键,每条订单都可以通过订单号定位,订单中的其它属性,如客户、产品等,都依赖于订单号。也就是说,订单号把与订单属性有关的表联系起来。但是,在维度模型中,事实表中的订单号代理键通常与订单属性的其它表没有关联。可以将订单事实表所有关心的属性分类到不同的维度中,例如,订单日期关联到日期维度,客户关联到客户维度等。在事实表中保留订单号最主要的原因是用于连接数据仓库与操作型系统,它也可以起到事实表主键的作用。某些情况下,可能会有一个或两个属性仍然属于订单而不属于其它维度。当然,此时订单维度就不再是退化维度了。
退化维度通常被保留作为操作型事务的标识符。实际上可以将订单号作为一个属性加入到事实表中。这样订单维度就没有数据仓库需要的任何数据,此时就可以退化订单维度。需要把退化维度的相关数据迁移到事实表中,然后删除退化的维度。操作型事务中的控制号码,例如订单号码、发票号码、提货单号码等通常产生空的维度并且表示为事务事实表中的退化维度。
使用维度退化技术时先要识别数据,分析从来不用的数据列,订单维度的order_number列就可能是这样一列。但如果用户想看事务的细节,还需要订单号。因此在退化订单维度前,要把订单号迁移到sales_order_fact事实表。图8-25显示了修改后的模式。
图8-25 退化订单维度
按顺序执行下面的四步退化order_dim维度表: (1)给sales_order_fact表添加order_number列 (2)把order_dim表里的订单号迁移到sales_order_fact表 (3)删除sales_order_fact表里的order_sk列 (4)删除order_dim表
创建执行如图8-26所示的Kettle作业完成所有退化订单维度所需步骤。
图8-26 退化维度初始数据装载
第一个SQL作业项执行以下SQL创建新表:
use dw;
create table sales_order_fact_new
(order_number int COMMENT '销售订单号',
customer_sk int COMMENT '客户维度代理键',
product_sk int COMMENT '产品维度代理键',
order_date_sk int COMMENT '日期维度代理键',
order_amount decimal(10,2) COMMENT '销售金额',
order_quantity int COMMENT '销售数量',
request_delivery_date_sk int COMMENT '请求交付日期')
clustered by (order_number) into 8 buckets
stored as orc tblproperties ('transactional'='true');
中间调用的Kettle转换如图8-27所示
图8-27 装载新表的转换
“表输入”步骤执行查询,“ORC output”将查询结果上传到新表所在HDFS目录。
第二个SQL作业项执行以下SQL删除老表、改名新表:
use dw;
drop table
drop table sales_order_fact;
alter table sales_order_fact_new rename to sales_order_fact;
drop table order_dim;
虽然到目前为止,订单号维度表中代理键和订单号业务主键的值相同,但还是建议使用标准的方式重新生成数据,不要简单地将事实表的order_sk字段改名为order_number,这种图省事的做法不值得提倡。
退化一个维度后需要做的另一件事就是修改定期装载Kettle作业和转换。需要做三点修改:一是装载维度表的转换中去掉装载订单维度表的步骤;二是修改装载销售订单事实表转换的数据库连接步骤的中SQL,去掉获取订单维度代理键的部分,改为:
select a.order_number,
c.customer_sk,
d.product_sk,
e.date_sk,
a.order_amount,
a.order_quantity,
f.date_sk request_delivery_date_sk
from rds.sales_order a,
dw.customer_dim c,
dw.product_dim d,
dw.date_dim e,
dw.date_dim f
where a.customer_number = c.customer_number
and a.order_date >= c.effective_date
and a.order_date < c.expiry_date
and a.product_code = d.product_code
and a.order_date >= d.effective_date
and a.order_date < d.expiry_date
and to_date(a.order_date) = e.dt
and to_date(a.request_delivery_date) = f.dt
and a.entry_date >= ? and a.entry_date < ?
三是在装载销售订单事实表转换的“ORC output”步骤中以order_number字段替换原来的order_sk。
(1)准备两行销售订单测试数据。
use source;
set @start_date := unix_timestamp('2020-10-31');
set @end_date := unix_timestamp('2020-10-31 12:00:00');
set @order_date := from_unixtime(@start_date + rand() * (@end_date - @start_date));
set @amount := floor(1000 + rand() * 9000);
set @quantity := floor(10 + rand() * 90);
set @customer_number := floor(1 + rand() * 14);
set @product_code := floor(1 + rand() * 5);
insert into sales_order values (null,@customer_number,@product_code,@order_date,'2020-11-01',@order_date,@amount,@quantity);
set @start_date := unix_timestamp('2020-10-31 12:00:01');
set @end_date := unix_timestamp('2020-11-01');
set @order_date := from_unixtime(@start_date + rand() * (@end_date - @start_date));
set @amount := floor(1000 + rand() * 9000);
set @quantity := floor(10 + rand() * 90);
set @customer_number := floor(1 + rand() * 14);
set @product_code := floor(1 + rand() * 5);
insert into sales_order values (null,@customer_number,@product_code,@order_date,'2020-11-01',@order_date,@amount,@quantity);
commit ;
以上语句在源库上生成2020年10月31日的两条销售订单。为了保证自增订单号与订单时间顺序相同,注意一下@order_date变量的赋值。
(2)执行Kettle定期装载作业并查看结果。 作业执行成功后,查询sales_order_fact表,验证新增的两条订单是否正确装载。
本节讨论杂项维度。简单地说,杂项维度就是一种包含的数据具有很少可能值的维度。事务型商业过程通常产生一系列混杂的、低基数的标志位或状态信息,这里所说的基数可以简单理解为某列数据中不同值的个数。与其为每个标志或属性定义不同的维度,不如建立单独的将不同维度合并到一起的杂项维度。这些维度,通常在一个模式中标记为事务型概要维度,一般不需要所有属性可能值的笛卡尔积,但应该至少包含实际发生在源数据中的组合值。
在销售订单中,可能存在有很多离散数据(yes-no这种开关类型的值),如:
这类数据常被用于增强销售分析,其特点是属性可能很多但每种属性的可能值很少。在建模复杂的操作型源系统时,经常会遭遇大量五花八门的标志或状态信息,它们包含小范围的离散值。处理这些较低基数的标志或状态位可以采用以下几种方法。
(1)忽略这些标志和指标。 姑且将这种回避问题的处理方式也算作方法之一吧。在开发ETL系统时,ETL开发小组可以向业务用户询问有关忽略这些标志的必要问题,如果它们是微不足道的。但是这样的方案通常立即就被否决了,因为有人偶尔还需要它们。如果来自业务系统的标志或状态是难以理解且不一致的,也许真的应该考虑去掉它们。
(2)保持事实表行中的标志位不变。 还以销售订单为例,和源数据库一样,可以在事实表中也建立这四个标志位字段。在装载事实表时,除了订单号以外,同时装载这四个字段的数据,这些字段没有对应的维度表,而是作为订单的属性保留在事实表中。
这种处理方法简单直接,装载程不需要做大量的修改,也不需要建立相关的维度表。但是一般不希望在事实表中存储难以识别的标志位,尤其是当每个标志位还配有一个文字描述字段时。不要在事实表行中存储包含大量字符的描述符,因为每一行都会有文字描述,它们可能会使表快速地膨胀。在行中保留一些文本标志是令人反感的,比较好的做法是分离出单独的维度表保存这些标志位字段的数据,它们的数据量很小,并且极少改变。事实表通过维度表的代理键引用这些标志。
(3)将每个标志位放入其自己的维度中。 例如为销售订单的四个标志位分别建立四个对应的维度表。在装载事实表数据前先处理这四个维度表,必要时生成新的代理键,然后在事实表中引用这些代理键。这种方法是将杂项维度当做普通维度来处理,多数情况下这也是不合适的。
首先,当类似的标志或状态位字段比较多时,需要建立很多的维度表,其次事实表的外键数也会大量增加。处理这些新增的维度表和外键需要大量修改数据装载脚本,还会增加出错的机会,同时会给ETL的开发、维护、测试过程带来很大的工作量。最后,杂项维度的数据有自己明显的特点,即属性多但每个属性的值少,并且极少修改,这种特点决定了它应该与普通维度的处理区分开。作为一个经验值,如果外键的数量处于合理的范围中,即不超过20个,则在事实表中增加不同的外键是可以接受的。但是,若外键列表已经很长,则应该避免将更多的外键加入到事实表中。
(4)将标志位字段存储到订单维度中。 可以将标志位字段添加到订单维度表中。上一节我们将订单维度表作为退化维度删除了,因为它除了订单号,没有其它任何属性。与其将订单号当成是退化维度,不如视其为将低基数标志或状态作为属性的普通维度。事实表通过引用订单维度表的代理键,关联到所有的标志位信息。
尽管该方法精确地表示了数据关系,但依然存在前面讨论的问题。在订单维度表中,每条业务订单都会存在对应的一条销售订单记录,该维度表的记录数会膨胀到跟事实表一样多,而在如此多的数据中,每个标志位字段都存在大量的冗余,需要占用很大的存储空间。通常维度表应该比事实表小得多。
(5)使用杂项维度。 处理这些标志位的适当替换方法是仔细研究它们,并将它们包装为一个或多个杂项维度。杂项维度中放置各种离散的标志或状态数据。尽管为每个标志位创建专门的维度表会非常容易定位这些标志信息,但这会增加系统实现的复杂度。此外,正因为杂项维度的值很少,也不会频繁使用它们,所以不建议为保证单一目的分配存储空间。杂项维度能够合理地存放离散属性值,还能够维持其它主要维度的存储空间。在维度建模领域,杂项维度术语主要用在DW/BI专业人员中。在与业务用户讨论时,通常将杂项维度称为事务指示器或事务概要维度。
杂项维度是低基数标志和指标的分组。通过建立杂项维度,可以将标志和指标从事实表中移出,并将它们放入到有用的多维框架中。对杂项维度数据量的估算也会影响其建模策略。如果某个简单的杂项维度包含10个二值标识,例如,现金或信用卡支付类型、是否审核、在线或离线、本国或海外等等,则最多将包含1024(2^10)行。假设由于每个标志都与其它标志一起发生作用,在这种情况下浏览单一维度内的标识可能没什么意义。但是杂项维度可提供所有标识的存储,并用于基于这些标识的约束和报表。事实表与杂项维度之间存在一个单一的、小型的代理键。
另一方面,如果具有高度非关联的属性,包含更多的数量值,则将它们合并为单一的杂项维度是不合适的。遗憾的是,是否使用统一杂项维度的决定并不完全是公式化的,要依据具体的数据范围而定。如果存在5个标识,每个仅包含3个值,则单一杂项维度是这些属性的最佳选择,因为维度最多仅有243(3^5)行。但是如果5个没有关联的标识,每个具有100个可能值,建议建立不同维度,因为单一杂项维度表最大可能存在1亿(100^5)行。
关于杂项维度的一个微妙的问题是,在杂项维度中行的组合确定并已知的前提下,是应该事先为所有组合的完全笛卡尔积建立行,还是建立杂项维度行,只用于保存那些在源系统中出现的组合情况的数据。答案要看大概有多少可能的组合,最大行数是多少。一般来说,理论上组合的数量较小,比如只有几百行时,可以预装载所有组合的数据,而组合的数量大,那么在数据获取时,当遇到新标志或指标时再建立杂项维度行。当然,如果源数据中用到了全体组合时,那别无选择只能预先装载好全部杂项维度数据。
如果杂项维度的取值事先并不知道,只有在获取数据时才能确定,那么就需要在处理业务系统事务表时,建立新观察到的杂项维度行。这一过程需要聚集杂项维度属性并将它们与已经存在的杂项维度行比较,已确定该行是否已经存在。如果不存在,将组建新的维度行,建立代理键。在处理事务表过程中适时地将该行加载到杂项维度中。
解释了杂项维度之后,将它们与处理标志位作为订单维度属性的方法进行比较。如希望分析订单事实的审核情况,其订单属性包含“是否审核”标志位,如果使用杂项维度,维度表中只会有很少的记录。相反这些属性如果被存储到订单维度中,针对事实表的约束将会是一个巨大的列表,因为每一条订单记录都包含“是否审核”标志。在与事实表关联查询时,这两种处理方式将产生巨大的性能差异。
下面描述销售订单示例数据仓库中杂项维度的具体实现。图8-28显示了增加杂项维度表后的数据仓库模式,这里只显示了和销售订单事务相关的表。
图8-28 杂项维度
给现有的数据仓库新增一个销售订单属性杂项维度。需要新增一个名为sales_order_attribute_dim的杂项维度表,该表包括四个yes-no列:verification_ind、credit_check_flag、new_customer_ind和web_order_flag,各列的含义已经在本节开头说明。每个列可以有两个可能值中的一个,Y 或 N,因此sales_order_attribute_dim表最多有16(2^4)行。假设这16行已经包含了所有可能的组合,因此可以预装载这个维度,并且只需装载一次。
注意,如果知道某种组合是不可能出现的,就不需要装载这种组合。执行下面的脚本修改数据库模式。这个脚本做了四项工作:建立sales_order_attribute_dim表;向表中预装载全部16种可能的数据;给销售订单事实表添加杂项维度代理键字段;给源数据库里的sales_order表增加对应的四个属性列。
use dw;
-- 建立杂项维度表
create table sales_order_attribute_dim (
sales_order_attribute_sk int comment '销售订单属性代理键',
verification_ind char(1) comment '是否审核,y or n',
credit_check_flag char(1) comment '是否检查过信用状态,y or n',
new_customer_ind char(1) comment '是否新用户,y or n',
web_order_flag char(1) comment '是否线上订单,y or n',
version int comment '版本号',
effective_date date comment '生效日期',
expiry_date date comment '到期日期'
)
clustered by (sales_order_attribute_sk) into 8 buckets
stored as orc tblproperties ('transactional'='true');
-- 生成杂项维度数据
insert into sales_order_attribute_dim
values (1, 'y', 'n', 'n', 'n', 1,'2020-03-01', '2200-01-01');
...
-- 共插入16条记录
-- 建立杂项维度外键
alter table sales_order_fact add columns (sales_order_attribute_sk int comment '订单属性代理键') ;
-- 给源库的销售订单表增加对应的属性
use source;
alter table sales_order
add verification_ind char (1) after product_code
, add credit_check_flag char (1) after verification_ind
, add new_customer_ind char (1) after credit_check_flag
, add web_order_flag char (1) after new_customer_ind ;
-- 给销售订单过渡表增加对应的属性
use rds;
alter table sales_order add columns
(
verification_ind char(1) comment '是否审核,y or n',
credit_check_flag char(1) comment '是否检查过信用状态,y or n',
new_customer_ind char(1) comment '是否新用户,y or n',
web_order_flag char(1) comment '是否线上订单,y or n'
) ;
和所有维度表(除日期相关维度)一样,为了处理可能的SCD情况,订单属性杂项维度表也具有版本号、生效日期、过期日期等列。
(1)修改装载过渡区销售订单表的转换,增加四列。 “数据库连接”步骤中的SQL改为:
select order_number,
customer_number,
product_code,
order_date,
entry_date,
order_amount,
order_quantity,
request_delivery_date,
verification_ind,
credit_check_flag,
new_customer_ind,
web_order_flag
from source.sales_order
where entry_date >= ? and entry_date < ?
“Hadoop file output”步骤中添加String类型的四个字段verification_ind、credit_check_flag、new_customer_ind、web_order_flag,字段顺序与rds.sales_order表列顺序一致,如图8-29所示。
图8-29 在过渡区销售订单表中增加四个杂项字段
(2)修改装载销售订单事实表转换中数据库连接步骤中的SQL。
select a.order_number,
c.customer_sk,
d.product_sk,
e.date_sk,
a.order_amount,
a.order_quantity,
f.date_sk request_delivery_date_sk,
g.sales_order_attribute_sk
from rds.sales_order a,
dw.customer_dim c,
dw.product_dim d,
dw.date_dim e,
dw.date_dim f,
dw.sales_order_attribute_dim g
where a.customer_number = c.customer_number
and a.order_date >= c.effective_date
and a.order_date < c.expiry_date
and a.product_code = d.product_code
and a.order_date >= d.effective_date
and a.order_date < d.expiry_date
and to_date(a.order_date) = e.dt
and to_date(a.request_delivery_date) = f.dt
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 >= ? and a.entry_date < ?
最后的“ORC output”步骤的Fields属性最后增加一个int类型的sales_order_attribute_sk字段。
杂项属性维度数据已经预装载,所以在定期装载Kettle作业中只需要修改处理事实表的部分。源数据中有四个属性列,而事实表中只对应一列,因此需要使用四列关联条件的组合确定杂项维度表的代理键值,并装载到事实表中。
(1)执行下面的脚本添加八个销售订单。
use source;
drop table if exists temp_sales_order_data;
create table temp_sales_order_data as select * from sales_order where 1=0;
set @start_date := unix_timestamp('2020-11-01');
set @end_date := unix_timestamp('2020-11-02');
set @order_date := from_unixtime(@start_date + rand() * (@end_date - @start_date));
set @amount := floor(1000 + rand() * 9000);
set @quantity := floor(10 + rand() * 90);
set @customer_number := floor(1 + rand() * 14);
set @product_code := floor(1 + rand() * 5);
insert into temp_sales_order_data
values (1, @customer_number, @product_code, 'y', 'y', 'n', 'y',
@order_date, '2020-11-04', @order_date, @amount, @quantity);
...
-- 一共添加各种属性组合的八条记录
insert into sales_order
select null,
customer_number,
product_code,
verification_ind,
credit_check_flag,
new_customer_ind,
web_order_flag,
order_date,
request_delivery_date,
entry_date,
order_amount,
order_quantity
from temp_sales_order_data t1
order by t1.order_date;
commit;
(2)执行Kettle定期装载作业并查看结果。 作业执行成功后,可以使用下面的分析性查询确认装载是否正确。该查询分析出检查了信用状态的新用户有所占的比例。
select concat(round(checked / (checked + not_checked) * 100),' % ')
from (select sum(case when credit_check_flag='y' then 1 else 0 end) checked,
sum(case when credit_check_flag='n' then 1 else 0 end) not_checked
from dw.sales_order_fact a, dw.sales_order_attribute_dim b
where new_customer_ind = 'y'
and a.sales_order_attribute_sk = b.sales_order_attribute_sk) t;
sum(case when...)是SQL中一种常用的行转列方法,用于列数固定的场景。在这次测试数据中,以上查询语句的返回值为60%。查询中销售订单事实表与杂项维度表使用的是内连接,因此只会匹配新增的8条记录,而查询结果比例的分母只能出自这8条记录。
在多维数据仓库建模时,如果维度属性中的两个组存在多对多关系时,应该将它们建模为不同的维度,并在事实表中构建针对这些维度的不同外键。另一种处理多对多关系的方法是,使用桥接表,将一个多对多关系转化为两个一对多关系。我们在前面讨论的展开树也是一种典型的桥接表。事实表通过引用桥接表的一个代理键,同时关联到多个维度值。这样做的目的是消除数据冗余,保证数据一致性。多对多关系的常见示例包括:每个学生登记了许多课程,每个课程有许多学生;一名医生有许多患者,每个患者有许多医生;一个产品或服务属于多个类别,每个类别包含多个产品或服务等。从结构上来说,创建多对多维度关系的方式类似于在关系数据模型中创建多对多关系。
然而,有时会遇到一些情况,更适合将两个维度合并到单一维度中,而不是在事实表中引用两个不同维度的外键,或使用桥接表。例如,在一个飞行服务数据分析系统中,业务用户希望分析乘客购买机票的服务级别。此外,用户还希望方便地按照是否发生服务的升级或降级情况过滤并构建报表。最初的想法可能是建立两个角色扮演维度,一个表示最初购买的机票服务等级,另外一个表示实际乘机时的服务级别。可能还希望建立第三个维度表示升降级情况,否则BI应用需要包括用于区分众多升降级情况的逻辑,例如经济舱升级到商务舱,经济舱升级到头等舱,商务舱升级到头等舱等等。但是,面对这个特殊场景,在维度表中只有用于区分头等舱、商务舱、经济舱的三行记录。同样,升降级标准维度表也仅包含三行,分别对应升级、降级、无变化。因为维度的基数太小,而且不会进行更新,所以可以选择将这些维度合并成单一服务级别变动维度,如表8-4所示。
机票升降级主键 | 最初购买级别 | 实际乘坐级别 | 服务等级变动标识 |
---|---|---|---|
1 | 经济舱 | 经济舱 | 无变化 |
2 | 经济舱 | 商务舱 | 升级 |
3 | 经济舱 | 头等舱 | 升级 |
4 | 商务舱 | 经济舱 | 降级 |
5 | 商务舱 | 商务舱 | 无变化 |
6 | 商务舱 | 头等舱 | 升级 |
7 | 头等舱 | 经济舱 | 降级 |
8 | 头等舱 | 商务舱 | 降级 |
9 | 头等舱 | 头等舱 | 无变化 |
表8-4 服务级别变动维度
不同维度的笛卡尔积将产生9行的维度表。在合并维度中还可以包含描述购买服务级别和乘坐服务级别之间的关系,例如表中的服务等级变动标识。应该将此类服务级别变动维度当成杂项维度。在此案例研究中,属性是紧密关联的。其它的航空事实表,如有效座位或机票购买,不可避免地需要引用包含3行的一致性机票等级维度表。
还有一种合并维度的情况,就是本来属性相同的维度,因为某种原因被设计成重复的维度属性。在销售订单示例中,随着数据仓库中维度的增加,会发现有些通用的数据存在于多个维度中,如客户维度的客户地址相关信息、送货地址相关信息里都有邮编、城市和省份。下面说明如何把客户维度里的两个邮编相关信息合并到一个新的维度中。
为了合并维度,需要改变数据仓库模式。图8-30显示了修改后的模式。新增了一个zip_code_dim邮编信息维度表,sales_order_fact事实表的结构也做了相应的修改。注意图中只显示了与邮编维度相关的表。
图8-30 合并邮编信息维度
zip_code_dim维度表与销售订单事实表相关联。这个关系替换了事实表与客户维度的关系。sales_order_fact表需要两个关系,一个关联到客户地址邮编,另一个关联到送货地址邮编,相应的增加了两个逻辑上的外键字段。下面说明用于修改数据仓库模式的脚本。
use dw;
create table zip_code_dim (
zip_code_sk int,
zip_code int,
city varchar(30),
state varchar(2),
version int,
effective_date date,
expiry_date date
)
clustered by (zip_code_sk) into 8 buckets
stored as orc tblproperties ('transactional'='true');
执行上面的语句创建邮编维度表。该维度表有邮编、城市、省份三个业务属性,和其它维度表一样,使用ORC存储类型。
insert into zip_code_dim
select row_number() over (order by t1.customer_zip_code),
customer_zip_code,
customer_city,
customer_state,
1,'2020-03-01','2200-01-01'
from (select distinct customer_zip_code, customer_city, customer_state
from customer_dim
where customer_zip_code is not null
union
select distinct shipping_zip_code, shipping_city, shipping_state
from customer_dim
where shipping_zip_code is not null) t1;
执行上面的语句初始装载邮编相关数据。初始数据是从客户维度表中来,这只是为了演示数据装载的过程。客户的邮编信息很可能覆盖不到所有邮编,所以更好的方法是装载一个完整的邮编信息表。由于客户地址和送货地址可能存在交叉的情况,因此使用union联合两个查询。注意这里不能使用union all,因为需要去除重复的数据。送货地址的三个字段是在第一节中后加的,在此之前数据的送货地址为空,邮编维度表中不能含有NULL值,所以要加上where shipping_zip_code is not null过滤条件去除邮编信息为NULL的数据行。
create view customer_zip_code_dim
(customer_zip_code_sk, customer_zip_code, customer_city,
customer_state, version, effective_date, expiry_date) as
select zip_code_sk, zip_code, city, state,
version, effective_date, expiry_date
from zip_code_dim;
create view shipping_zip_code_dim
(shipping_zip_code_sk, shipping_zip_code, shipping_city,
shipping_state, version, effective_date, expiry_date) as
select zip_code_sk, zip_code, city, state,
version, effective_date, expiry_date
from zip_code_dim;
上面的语句基于邮编维度表创建客户邮编和送货邮编视图,分别用作两个地理信息的角色扮演维度。
alter table sales_order_fact add columns
(customer_zip_code_sk int comment '客户邮编代理键',
shipping_zip_code_sk int comment '送货邮编代理键'
) ;
以上语句给销售订单事实表增加客户邮编代理键和送货邮编代理键,引用两个邮编信息角色扮演维度。
drop table if exists tmp;
create table tmp as select * from sales_order_fact;
truncate table sales_order_fact;
以上语句创建临时表,并清空销售订单事实表,用于重新初始装载它。
创建如图8-31的Kettle转换,初始装载增加了邮编维度的销售订单事实表。
图8-31 初始装载增加了邮编维度的销售订单事实表的转换
“表输入”步骤中的SQL查询如下,注意要去掉结尾的分号。
select t1.order_number,
t1.customer_sk,
t1.product_sk,
t1.order_date_sk,
t1.order_amount,
t1.order_quantity,
t1.request_delivery_date_sk,
t1.sales_order_attribute_sk,
t2.customer_zip_code_sk,
t3.shipping_zip_code_sk
from dw.tmp t1
left join
(select a.order_number order_number,c.customer_zip_code_sk customer_zip_code_sk
from dw.tmp a, dw.customer_dim b, dw.customer_zip_code_dim c
where a.customer_sk = b.customer_sk
and b.customer_zip_code = c.customer_zip_code) t2
on t1.order_number = t2.order_number
left join
(select a.order_number order_number,c.shipping_zip_code_sk shipping_zip_code_sk
from dw.tmp a, dw.customer_dim b, dw.shipping_zip_code_dim c
where a.customer_sk = b.customer_sk
and b.shipping_zip_code = c.shipping_zip_code) t3
on t1.order_number = t3.order_number;
这条查询语句有些复杂。它是把数据临时表tmp中的数据装载回销售订单事实表,同时需要关联两个邮编角色维度视图,查询出两个代理键,装载到事实表中。注意临时表与新的邮编维度表是通过客户维度表关联起来的,所以在子查询中需要三表连接,然后用两个左外连接查询出所有原事实表数据,装载到新的增加了邮编维度代理键的事实表中。
alter table customer_dim rename to customer_dim_old;
create table customer_dim
(customer_sk int comment '代理键',
customer_number int comment '客户编号,业务主键',
customer_name varchar(50) comment '客户名称',
customer_street_address varchar(50) comment '客户住址',
shipping_address varchar(50) comment '送货地址',
version int comment '版本号',
effective_date date comment '生效日期',
expiry_date date comment '到期日期')
clustered by (customer_sk) into 8 buckets
stored as orc tblproperties ('transactional'='true');
insert into customer_dim
select customer_sk,
customer_number,
customer_name,
customer_street_address,
shipping_address,
version,
effective_date,
expiry_date
from customer_dim_old;
drop table customer_dim_old;
-- 修改pa_customer_dim表,同样将邮编相关字段删除
...
以上语句在客户维度表上删除客户和送货邮编及其它们的城市和省份列,因为是ORC表,所以需要重新组织数据。使用类似的语句修改PA维度子集表,代码从略。
定期装载作业有四个地方的修改: (1)删除客户维度装载里所有邮编信息相关的列,因为客户维度里不再有客户邮编和送货邮编相关信息。注意查询中列的顺序。 (2)去掉客户维度装载里pa_customer_dim表的部分。 (3)在事实表中引用客户邮编视图和送货邮编视图中的代理键。 (4)增加装载pa_customer_dim的作业项,从销售订单事实表的customer_zip_code_sk获取客户邮编。
前两步修改“装载客户维度”步骤中的SQL。第三步的装载事实表转换中数据库连接步骤中的SQL改为:
select a.order_number,
b.customer_sk,
c.product_sk,
d.date_sk,
a.order_amount,
a.order_quantity,
e.date_sk request_delivery_date_sk,
f.sales_order_attribute_sk,
g.customer_zip_code_sk,
h.shipping_zip_code_sk
from rds.sales_order a,
dw.customer_dim b,
dw.product_dim c,
dw.date_dim d,
dw.date_dim e,
dw.sales_order_attribute_dim f,
dw.customer_zip_code_dim g,
dw.shipping_zip_code_dim h,
rds.customer i
where a.customer_number = b.customer_number
and a.order_date >= b.effective_date and a.order_date < b.expiry_date
and a.product_code = c.product_code
and a.order_date >= c.effective_date and a.order_date < c.expiry_date
and to_date(a.order_date) = d.dt
and to_date(a.request_delivery_date) = e.dt
and a.verification_ind = f.verification_ind
and a.credit_check_flag = f.credit_check_flag
and a.new_customer_ind = f.new_customer_ind
and a.web_order_flag = f.web_order_flag
and a.customer_number = i.customer_number
and i.customer_zip_code = g.customer_zip_code
and a.order_date >= g.effective_date and a.order_date < g.expiry_date
and i.shipping_zip_code = h.shipping_zip_code
and a.order_date >= h.effective_date and a.order_date < h.expiry_date
and a.entry_date >= ? and a.entry_date < ?
“ORC output”步骤的Fields属性最后增加int类型的customer_zip_code_sk和shipping_zip_code_sk字段。
第四步的定期装载作业如图8-32所示。
图8-32 定期装载作业增加一个SQL作业项
增加的SQL作业项装载pa_customer_dim维度表,语句如下:
truncate table dw.pa_customer_dim;
insert into dw.pa_customer_dim
select distinct a.*
from dw.customer_dim a,
dw.sales_order_fact b,
dw.customer_zip_code_dim c
where c.customer_state = 'pa'
and b.customer_zip_code_sk = c.customer_zip_code_sk
and a.customer_sk = b.customer_sk;
州代码已经从客户维度表删除,被放到了新的邮编维度表中,而客户维度和邮编维度并没有直接关系,它们是通过事实表的客户代理键和邮编代理键产生联系,因此必须关联事实表、客户维度表、邮编维度表三个表才能取出PA子维度数据。正因如此才把PA子维度的装载放到了事实表装载之后,一个连带结果是该步骤的数据装载结果与前面介绍的维度子集不同,现在只维护有购买记录的PA州客户维度数据。
按照以下步骤测试修改后的定期装载作业。 (1)对源数据的客户邮编相关信息做一些修改。 (2)装载新的客户数据前,从DW库查询最后的客户和送货邮编,后面可以用改变后的信息和此查询的输出作对比。 (3)新增销售订单源数据。 (4)修改定期装载执行的时间窗口。 (5)执行定期装载。 (6)查询客户维度表、售订单事实表和PA子维度表,确认数据已经正确装载。
在客户维度中,最具有分析价值的属性就是各种分类,这些属性的变化范围比较大。对某个个体客户来说,可能的分类属性包括:性别、年龄、民族、职业、收入和状态,例如,新客户、活跃客户、不活跃客户、已流失客户等。在这些分类属性中,有一些能够定义成包含连续值的分段,例如年龄和收入这种数值型的属性,天然就可以分成连续的数值区间,而象状态这种描述性的属性,可能需要用户根据自己的实际业务仔细定义,通常定义的根据是某种可度量的数值。
组织还可能使用为其客户打分的方法刻画客户行为。分段维度模型通常以不同方式按照积分将客户分类,例如,基于他们的购买行为、支付行为、流失走向等。每个客户用所得的分数标记。
一个常用的客户评分及分析系统是考察客户行为的相关度(R)、频繁度(F)和强度(I),该方法被称为RFI方法。有时将强度替换为消费度(M),因此也被称为RFM度量。相关度是指客户上次购买或访问网站距今的天数。频繁度是指一段时间内客户购买或访问网站的次数,通常是指过去一年的情况。强度是指客户在某一固定时间周期中消费的总金额。在处理大型客户数据时,某个客户的行为可以按照如图8-33所示的RFI多维数据仓库建模。在此图中,每个维度形成一条数轴,某个轴的积分度量值从1到5,代表某个分组的实际值,三条数轴组合构成客户积分立方体,每个客户的积分都在这个立方体之中。
图8-33 RFI立方体
定义有意义的分组至关重要。应该由业务人员和数据仓库开发团队共同定义可能会利用的行为标识,更复杂的场景可能包含信用行为和回报情况,例如定义如下8个客户标识: A:活跃客户,信誉良好,产品回报多 B:活跃客户,信誉良好,产品回报一般 C:最近的新客户,尚未建立信誉等级 D:偶尔出现的客户,信誉良好 E:偶尔出现的客户,信誉不好 F:以前的优秀客户,最近不常见 G:只逛不买的客户,几乎没有效益 H:其它客户
至此可以考察客户时间序列数据,并将某个客户关联到报表期间的最近分类中。如某个客户在最近10个考察期间的情况可以表示为:CCCDDAAABB。这一行为时间序列标记来自于固定周期度量过程,观察值是文本类型的,不能计算或求平均值,但是它们可以被查询。例如可以发现在以前的第5个、第4个或第3个周期中获得A且在第2个或第1个周期中获得B的所有客户。通过这样的进展分析还可以发现那些可能失去的有价值的客户,进而用于提高产品回报率。
行为标记可能不会被当成普通事实存储,因为它虽然由事实表的度量所定义,但其本身不是度量值。行为标记的主要作用在于为前面描述的例子制定复杂的查询模式。推荐的处理行为标记的方法是为客户维度建立分段属性的时间序列。这样BI接口比较简单,因为列都在同一个表中,性能也较好,因为可以对它们建立时间戳索引。除了为每个行为标记时间周期建立不同的列,建立单一的包含多个连续行为标记的连接字符串,也是较好的一种方法,例CCCDDAAABB。该列支持通配符模糊搜索模式,例“D后紧跟着B”可以简单实现为“where flag like '%DB%'”。
下面以销售订单为例,说明分段维度的实现技术。分段维度包含连续的分段度量值,例如年度销售订单分段维度可能包含有叫做“低”、“中”、“高”的三个档次,各档定义分别为消费额在0.01到3000、3000.01到6000.00、6000.01到99999999.99区间。如果一个客户的年度销售订单金额累计为1000,则被归为“低”档。分段维度可以存储多个分段集合。可能有一个用于促销分析的分段集合,另一个用于市场细分,可能还有一个用于销售区域计划。分段一般由用户定义,而且很少能从源事务数据直接获得。
为了实现年度订单分段维度,我们需要两个新的星型模式,如图8-34所示。
图8-34 年度销售额分段维度
第一个星型模式由annual_sales_order_fact事实表、customer_dim维度表和year_dim维度表构成。年维度是新建的维度表,是日期维度的子集。年度销售额事实表存储客户一年的消费总额,数据从现有的销售订单事实表汇总而来。第二个星型模式由annual_customer_segment_fact事实表、annual_order_segement_dim维度表、customer_dim维度表和year_dim维度表构成。客户年度分段事实表中没有度量,只有来自三个相关维度表的代理键,因此它是一个无事实的事实表,存储的数据实际上就是前面所说的行为标记时间序列。下一篇的事实表技术中将详细讨论无事实事实表。年度订单分段维度表用于存储分段的定义,在此只与年度分段事实表有关系。
如果多个分段的属性相同,可以将它们存储到单一维度表中,因为分段通常都有很小的基数。本例中annual_order_segment_dim表存储了“project”和“grid”两种分段集合,它们都是按照客户的年度销售订单金额将其分类。分段维度按消费金额的定义如表8-5所示,project 分六段,grid分三段。
分段类别 | 分段名称 | 开始值 | 结束值 |
---|---|---|---|
Project | bottom | 0.01 | 2500.00 |
Project | low | 2500.01 | 3000.00 |
Project | mid-low | 3000.01 | 4000.00 |
Project | mid | 4000.00 | 5500.00 |
Project | mid-high | 5500.01 | 6500.00 |
Project | top | 6500.01 | 99999999.99 |
Grid | low | 0.01 | 3000.00 |
Grid | mid | 3000.01 | 6000.00 |
Grid | high | 6000.01 | 99999999.99 |
表8-5 客户年度消费分段维度定义
每一分段有一个开始值和一个结束值。分段的粒度就是本段和下段之间的间隙。粒度必须是度量的最小可能值,在销售订单示例中,金额的最小值是0.01。最后一个分段的结束值是销售订单金额可能的最大值。下面的脚本用于建立分段维度数据仓库模式。
use dw;
create table annual_order_segment_dim (
segment_sk int,
segment_name varchar(30),
band_name varchar(50),
band_start_amount decimal(10,2),
band_end_amount decimal(10,2),
version int,
effective_date date,
expiry_date date
)
clustered by (segment_sk) into 8 buckets
stored as orc tblproperties ('transactional'='true');
insert into annual_order_segment_dim values
(1, 'project', 'bottom', 0.01, 2500.00, 1, '2020-03-01', '2200-01-01'),
(2, 'project', 'low', 2500.01, 3000.00, 1, '2020-03-01', '2200-01-01'),
(3, 'project', 'mid-low', 3000.01, 4000.00, 1, '2020-03-01', '2200-01-01'),
(4, 'project', 'mid', 4000.01, 5500.00, 1, '2020-03-01', '2200-01-01'),
(5, 'project', 'mid_high', 5500.01, 6500.00, 1, '2020-03-01', '2200-01-01'),
(6, 'project', 'top', 6500.01, 99999999.99, 1, '2020-03-01', '2200-01-01'),
(7, 'grid', 'low', 0.01, 3000, 1, '2020-03-01', '2200-01-01'),
(8, 'grid', 'med', 3000.01, 6000.00, 1, '2020-03-01', '2200-01-01'),
(9, 'grid', 'high', 6000.01, 99999999.99, 1, '2020-03-01', '2200-01-01');
create table year_dim (
year_sk int,
year int)
row format delimited fields terminated by ','
stored as textfile;
create table annual_sales_order_fact (
customer_sk int,
year_sk int,
annual_order_amount decimal(10, 2))
row format delimited fields terminated by ','
stored as textfile;
create table annual_customer_segment_fact (
segment_sk int,
customer_sk int,
year_sk int)
row format delimited fields terminated by ','
stored as textfile;
上面的语句新建四个表,包括年份维度表、分段维度表、年度销售事实表和年度客户消费分段事实表。在这四个表中,只有分段维度表采用ORC文件格式,因为我们使用insert into...values向该表语句插入9条分段定义数据,并且该表需要SCD处理。其它三个表没有行级更新的需求,所以使用Hive缺省的文本文件格式,以逗号作为列分隔符。
初始转换的Kettle作业如图8-35所示。
图8-35 初始装载分段维度的作业
该作业串行调用“装载年份维度”、“装载年度销售事实表”、“装载年度客户销售分段事实表”三个转换。如图表8-36所示。
图8-36 初始装载分段维度的三个转换
“装载年份维度”中的“表输入”步骤中SQL语句如下,查询日期维度中的年份。
select year from dw.order_date_dim
“去除重复记录”步骤对year字段去重。“排序记录”步骤对year字段执行升序输出。“增加序列”步骤输出“year_sk”年份维度代理键。最后的“Hadoop file output”步骤生成HDFS文件/user/hive/warehouse/dw.db/year_dim/year_dim.txt,包含year_sk、year两个Integer类型的字段,以逗号作为字段分隔符,格式为LF terminated (Unix),编码为UTF-8。“装载年度销售事实表”、“装载年度客户销售分段事实表”两个转换都只包含“表输入”和“Hadoop file output”两个步骤,区别只是SQL查询语句和输出的HDFS文件不同。“装载年度销售事实表”中“表输入”步骤的SQL为:
select a.customer_sk,
year_sk,
sum(order_amount) order_amount
from dw.sales_order_fact a,
dw.year_dim c,
dw.order_date_dim d
where a.order_date_sk = d.order_date_sk
and c.year = d.year
and d.year < 2021
group by a.customer_sk, c.year_sk
“Hadoop file output”输出的HDFS文件为/user/hive/warehouse/dw.db/annual_sales_order_fact/annual_sales_order_fact.txt。包含customer_sk、year_sk、order_amount三个字段,前两个为Integer类型,order_amount是Number类型,以逗号作为字段分隔符,格式为LF terminated (Unix),编码为UTF-8。 “装载年度客户销售分段事实表”中“表输入”步骤的SQL为:
select d.segment_sk,
a.customer_sk,
a.year_sk
from dw.annual_sales_order_fact a,
dw.annual_order_segment_dim d
where annual_order_amount >= band_start_amount
and annual_order_amount <= band_end_amount
“Hadoop file output”输出的HDFS文件为/user/hive/warehouse/dw.db/annual_customer_segment_fact/annual_customer_segment_fact.txt。包含segment_sk、customer_sk、year_sk三个Integer类型的字段,以逗号作为字段分隔符,格式为LF terminated (Unix),编码为UTF-8。 初始装载作业将订单日期角色扮演维度表(date_dim表的一个视图)里的去重年份数据导入年份维度表,将销售订单事实表中按年客户和分组求和的汇总金额数据导入年度销售事实表。因为装载过程不能导入当年的数据,所以使用year < 2021过滤条件作为演示。这里是按客户代理键customer_sk分组求和来判断分段,实际情况可能是以customer_number进行分组的,因为无论客户的SCD属性如何变化,一般还是认为是一个客户。将年度销售事实表里与分段维度表关联,把年份、客户和分段三个维度的代理键插入年度客户消费分段事实表。注意,数据装载过程中并没有引用客户维度表,因为客户代理键可以直接从销售订单事实表得到。分段定义中,每个分段结束值与下一分段的开始值是连续的,并且分段之间不存在数据重叠,所以装载分段事实表时,订单金额判断条件两端都使用闭区间。 执行初始装载脚本后,使用下面的语句查询客户分段事实表,确认装载的数据是正确的。
select a.customer_sk csk,
a.year_sk ysk,
annual_order_amount amt,
segment_name sn,
band_name bn
from dw.annual_customer_segment_fact a,
dw.annual_order_segment_dim b,
dw.year_dim c,
dw.annual_sales_order_fact d
where a.segment_sk = b.segment_sk
and a.year_sk = c.year_sk
and a.customer_sk = d.customer_sk
and a.year_sk = d.year_sk
cluster by csk, ysk, sn, bn;
除了无需装载年份表以外,定期装载与初始装载类似。年度销售事实表里的数据被导入分段事实表。每年调度执行如图8-37的定期装载Kettle作业,此作业装载前一年的销售数据。
图8-37 定期装载分段维度的作业
“设置年份”转换如图8-38所示。
图8-38 设置年份变量的转换
“获取系统信息”步骤输出当前系统日期字段cur_date。“公式”步骤用year([cur_date])公式生成Integer类型的当前年份cur_year新字段。“计算器”步骤如图8-39所示,计算前一年的年份。“设置变量”步骤设置一个名为pre_year的环境变量,变量活动类型为Valid in the root job,供后面的转换使用。
图8-39 计算前一年份的“计算器”步骤
“装载年度销售事实表”中“表输入”步骤的SQL为:
select a.customer_sk,
year_sk,
sum(order_amount)
from dw.sales_order_fact a,
dw.year_dim c,
dw.order_date_dim d
where a.order_date_sk = d.order_date_sk
and c.year = d.year
and d.year = ${pre_year}
group by a.customer_sk, c.year_sk
需要勾选“替换SQL语句里的变量”,取得前一年份。
“Hadoop file output”输出的HDFS文件为/user/hive/warehouse/dw.db/annual_sales_order_fact/annual_sales_order_fact_{pre_year}.txt,{pre_year}替换为前一年份。
“装载年度客户销售分段事实表”中“表输入”步骤的SQL为:
select d.segment_sk,
a.customer_sk,
c.year_sk
from dw.annual_sales_order_fact a,
dw.year_dim c,
dw.annual_order_segment_dim d
where a.year_sk = c.year_sk
and c.year = ${pre_year}
and annual_order_amount >= band_start_amount
and annual_order_amount <= band_end_amount
“Hadoop file output”输出的HDFS文件为/user/hive/warehouse/dw.db/annual_customer_segment_fact/annual_customer_segment_fact_{pre_year}.txt,{pre_year}替换为前一年份。