飞道的博客

Kettle构建Hadoop ETL实践(八-1):维度表技术

424人阅读  评论(0)

目录

一、增加列

1. 修改数据库模式

2. 修改Sqoop作业项

3. 修改定期装载维度表的转换

4. 修改定期装载事实表的转换

5. 测试

二、维度子集

1. 建立包含属性子集的子维度

2. 建立包含行子集的子维度

3. 使用视图实现维度子集

三、角色扮演维度

1.修改数据库模式

2. 修改Kettle定过期装载作业

3. 测试

4. 一种有问题的设计

四、层次维度

1. 固定深度的层次

2. 多路径层次

3. 参差不齐的层次


        前面文章中,我们用Kettle工具实现了Hadoop多维数据仓库的基本功能,如使用Sqoop作业项、SQL脚本、Hadoop file output、ORC output等步骤实现ETL过程,使用Oozie、Start作业项定期执行ETL任务等。本篇将继续讨论常见的维度表技术,以最简单的“增加列”开始,继而讨论维度子集、角色扮演维度、层次维度、退化维度、杂项维度、维度合并、分段维度等基本的维度表技术。这些技术都是在实际应用中经常使用的。在说明这些技术的相关概念和使用场景后,我们以销售订单数据仓库为例,给出Kettle实现和测试过程。

一、增加列

        业务的扩展或变化是不可避免的,尤其像互联网行业,需求变更已经成为常态,唯一不变的就是变化本身,其中最常碰到的扩展是给一个已经存在的表曾加列。以销售订单为例,假设因为业务需要,在操作型源系统的客户表中增加了送货地址的四个字段,并在销售订单表中增加了销售数量字段。由于数据源表增加了字段,数据仓库中的表也要随之修改。本节说明如何在客户维度表和销售订单事实表上添加列,并在新列上应用SCD2,以及对定时装载Kettle作业所做的修改。图8-1显示了增加列后的数据仓库模式。

图8-1 增加列后的数据仓库模式

1. 修改数据库模式

        使用下面的SQL语句修改MySQL中的源数据库模式。


  
  1. use source;  
  2. alter table customer  
  3.   add shipping_address varchar( 50) after customer_state  
  4. , add shipping_zip_code int after shipping_address  
  5. , add shipping_city varchar( 30) after shipping_zip_code  
  6. , add shipping_state varchar( 2) after shipping_city ;  
  7. alter table sales_order add order_quantity int after order_amount ;

        以上语句给客户表增加了四列,表示客户的送货地址。销售订单表在销售金额列后面增加了销售数量列。注意after关键字,这是MySQL对标准SQL的扩展,Hive目前还不支持这种扩展,只能把新增列加到已有列的后面,分区列之前。在关系理论中,列是没有顺序的。

        使用如下HiveQL语句修改RDS数据库模式。


  
  1. use rds;  
  2. alter table customer add columns  
  3.  (shipping_address varchar( 50) comment '送货地址'  
  4. , shipping_zip_code int comment '送货邮编'  
  5. , shipping_city varchar( 30) comment '送货城市'  
  6. , shipping_state varchar( 2) comment '送货省份') ;  
  7. alter table sales_order add columns (order_quantity int comment '销售数量') ;

        上面的DDL语句和MySQL的很像,增加了对应的数据列,并添加了列的注释。RDS库表使用的是缺省的文本存储格式,因此可以直接使用alter table语句修改表结构。需要注意的是RDS表中列的顺序要和源数据库严格保持一致。因为客户表和产品表是全量覆盖抽取数据,所以如果源和目标顺序不一样,将产生错误的结果。

        使用下面的HiveQL语句修改DW数据库模式。


  
  1. alter table customer_dim add columns  
  2.  (shipping_address varchar( 50) comment '送货地址'  
  3. , shipping_zip_code int comment '送货邮编'  
  4. , shipping_city varchar( 30) comment '送货城市'  
  5. , shipping_state varchar( 2) comment '送货省份') ;  
  6. alter table sales_order_fact add columns (order_quantity int comment '销售数量') ;

        上面这段代码修改DW数据库模式,它和之前的RDS表修改语句类似。

2. 修改Sqoop作业项

        由于增加了数据列,定期装载Kettle作业中,装载客户维度表和销售订单表的转换需要做相应的修改。对于过渡区,rds.customer是用Sqoop作业项全量抽取,由于与源数据source.customer表的字段结构与顺序完全相同,因此不需要修改。对于rds.sales_order表,只需在“Hadoop file output”步骤中添加order_quantity字段即可,如图8-2所示。

图8-2 在“Hadoop file output”步骤中添加order_quantity字段

        修改数据库模式后,还要修改装载维度表和装载事实表的作业项,增加对新增数据列的处理。

3. 修改定期装载维度表的转换

        下面说明装载客户维度表的SQL脚本步骤所做的修改。


  
  1. -- 装载customer维度  
  2. -- 设置已删除记录和地址相关列上scd2的过期,用<=>运算符处理null值。
  3. update customer_dim   
  4.     set expiry_date = '${PRE_DATE}'   
  5.   where customer_dim.customer_sk in    
  6. ( select a.customer_sk   
  7.     from ( select customer_sk,customer_number,customer_street_address, shipping_address
  8.             from customer_dim where expiry_date = '${MAX_DATE}') a left join   
  9.                 rds.customer b on a.customer_number = b.customer_number   
  10.           where b.customer_number is null 
  11.               or (!(a.customer_street_address <=> b.customer_street_address) or !(a.shipping_address <=> b.shipping_address) ));

        同客户地址一样,新增的送货地址列也是用SCD2新增历史版本。与上一篇建立的定期装载SQL脚本步骤中相同部分比较,会发现这里使用了一个新的关系操作符“<=>”,这是因为原来的脚本中少判断了一种情况。在源系统库中,客户地址和送货地址列都是允许为空的,这样的设计是出于灵活性和容错性的考虑。我们以送货地址为例进行讨论。

        使用“t1.shipping_address <> t2.shipping_address”条件判断送货地址是否更改,根据不等号两边的值是否为空,会出现以下三种情况:

  1. t1.shipping_address和t2.shipping_address都不为空。这种情况下如果两者相等则返回false,说明地址没有变化,否则返回true,说明地址改变了,逻辑正确。
  2. t1.shipping_address和t2.shipping_address都为空。两者的比较会演变成null<>null,根据Hive对“<>”操作符的定义,会返回NULL。因为查询语句中只会返回判断条件为true的记录,所以不会返回数据行,这符合业务逻辑,说明地址没有改变。
  3. t1.shipping_address和t2.shipping_address只有一个为空。就是说地址列从NULL变成非NULL,或者从非NULL变成NULL,这种情况明显应该新增一个版本,但根据“<>”的定义,此时返回值是NULL,查询不会返回行,不符合业务需求。

        现在使用“!(a.shipping_address <=> b.shipping_address)”作为判断条件,我们先看一下Hive里是怎么定义“<=>”操作符的:A <=> B — Returns same result with EQUAL(=) operator for non-null operands, but returns TRUE if both are NULL, FALSE if one of the them is NULL。从这个定义可知,当A和B都为NULL时返回TRUE,其中一个为NULL时返回FALSE,其它情况与等号返回相同的结果。下面再来看这三种情况:

  1. t1.shipping_address和t2.shipping_address都不为空。这种情况下如果两者相等则返回!(true),即false,说明地址没有变化,否则返回!(false),即true,说明地址改变了,符合我们的逻辑。
  2. t1.shipping_address和t2.shipping_address都为空。两者的比较会演变成!(null<=>null),根据“<=>”的定义,会返回!(true),即返回false。因为查询语句中只会返回判断条件为true的记录,所以查询不会返回行,这符合业务逻辑,说明地址没有改变。
  3. t1.shipping_address和 t2.shipping_address只有一个为空。根据“<=>”的定义,此时会返回!(false),即true,查询会返回行,符合业务需求。

        空值的逻辑判断有其特殊性,为了避免不必要的麻烦,数据库设计时应该尽量将字段设计成非空,必要时用默认值代替NULL,并将此作为一个基本的设计原则。


  
  1. -- 处理地址列上scd2的新增行
  2. insert into customer_dim  
  3. select  
  4.     row_number() over ( order by t1.customer_number) + t2.sk_max,  
  5.     t1.customer_number,  
  6.     t1.customer_name,  
  7.     t1.customer_street_address,  
  8.     t1.customer_zip_code,  
  9.     t1.customer_city,  
  10.     t1.customer_state,
  11.     t1.version,    
  12.     t1.effective_date,    
  13.     t1.expiry_date,        
  14.     t1.shipping_address,
  15.     t1.shipping_zip_code,
  16.     t1.shipping_city,
  17.     t1.shipping_state
  18. from    
  19. (    
  20. select    
  21.     t2.customer_number customer_number,  
  22.     t2.customer_name customer_name,  
  23.     t2.customer_street_address customer_street_address,  
  24.     t2.customer_zip_code,  
  25.     t2.customer_city,  
  26.     t2.customer_state, 
  27.     t1.version + 1 version,  
  28.     '${PRE_DATE}' effective_date,    
  29.     '${MAX_DATE}' expiry_date,
  30.     t2.shipping_address shipping_address,  
  31.     t2.shipping_zip_code shipping_zip_code,  
  32.     t2.shipping_city shipping_city,  
  33.     t2.shipping_state shipping_state
  34.   from customer_dim t1   
  35. inner join rds.customer t2    
  36.     on t1.customer_number = t2.customer_number     
  37.   and t1.expiry_date = '${PRE_DATE}'   
  38.   left join customer_dim t3   
  39.     on t1.customer_number = t3.customer_number   
  40.   and t3.expiry_date = '${MAX_DATE}'    
  41. where (!(t1.customer_street_address <=> t2.customer_street_address) or !(t1.shipping_address <=> t2.shipping_address) )
  42.   and t3.customer_sk is null) t1    
  43. cross join    
  44. ( select coalesce( max(customer_sk), 0) sk_max from customer_dim) t2; 

        上面的语句生成SCD2的新增版本行,增加了送货地址的处理,注意列的顺序要正确。


  
  1. -- 处理customer_name列上的scd1
  2. drop table if exists tmp;  
  3. create table tmp as  
  4. select  
  5.     a.customer_sk,  
  6.     a.customer_number,  
  7.     b.customer_name,  
  8.     a.customer_street_address,  
  9.     a.customer_zip_code,  
  10.     a.customer_city,  
  11.     a.customer_state,
  12.     a.version,  
  13.     a.effective_date,  
  14.     a.expiry_date,
  15.     a.shipping_address,
  16.     a.shipping_zip_code,
  17.     a.shipping_city,
  18.     a.shipping_state
  19.   from customer_dim a, rds.customer b    
  20.   where a.customer_number = b.customer_number and !(a.customer_name <=> b.customer_name);    
  21. delete from customer_dim where customer_dim.customer_sk in ( select customer_sk from tmp);    
  22. insert into customer_dim select * from tmp;

        customer_name列上的scd1处理只是在select语句中增加了送货地址的四列,并出于同样的原因使用了“<=>”关系操作符。


  
  1. -- 处理新增的customer记录
  2. insert into customer_dim  
  3. select  
  4.     row_number() over ( order by t1.customer_number) + t2.sk_max,  
  5.     t1.customer_number,  
  6.     t1.customer_name,  
  7.     t1.customer_street_address,  
  8.     t1.customer_zip_code,  
  9.     t1.customer_city,  
  10.     t1.customer_state,
  11.     1,  
  12.     '${PRE_DATE}',
  13.     '${MAX_DATE}',    
  14.     t1.shipping_address,
  15.     t1.shipping_zip_code,
  16.     t1.shipping_city,
  17.     t1.shipping_state
  18. from    
  19. (    
  20. select t1.* from rds.customer t1 left join customer_dim t2 on t1.customer_number = t2.customer_number    
  21.   where t2.customer_sk is null) t1    
  22. cross join    
  23. ( select coalesce( max(customer_sk), 0) sk_max from customer_dim) t2; 

        对于新增的客户,也只是在select语句中增加了送货地址的四列,其它没有变化。

4. 修改定期装载事实表的转换

        装载销售订单事实表的转换需要做两点修改。第一是“销售订单事务数据”数据库连接步骤的SQL查询语句中增加order_quantity列:


  
  1. select order_sk,
  2.        customer_sk,
  3.        product_sk,
  4.        date_sk,
  5.        order_amount,
  6.        order_quantity
  7.   from rds.sales_order a,
  8.        dw.order_dim b,
  9.        dw.customer_dim c,
  10.        dw.product_dim d,
  11.        dw.date_dim e
  12.   where a.order_number = b.order_number
  13.    and a.customer_number = c.customer_number
  14.    and a.order_date >= c.effective_date
  15.    and a.order_date < c.expiry_date
  16.    and a.product_code = d.product_code
  17.    and a.order_date >= d.effective_date
  18.    and a.order_date < d.expiry_date
  19.    and to_date(a.order_date) = e.dt
  20.    and a.entry_date >= ? and a.entry_date < ?

        第二是“ORC output”步骤增加order_quantity字段,如图8-3所示。

图8-3 在“ORC output”步骤中添加order_quantity字段

5. 测试

(1)执行下面的SQL脚本,在MySQL的源数据库中增加客户和销售订单测试数据。


  
  1. use source;    
  2. update customer set shipping_address = customer_street_address, 
  3.                     shipping_zip_code = customer_zip_code,
  4.                     shipping_city = customer_city, 
  5.                     shipping_state = customer_state ;
  6. insert into customer
  7.        (customer_name,
  8.         customer_street_address,
  9.         customer_zip_code,
  10.         customer_city,
  11.         customer_state,
  12.         shipping_address,
  13.         shipping_zip_code, 
  14.         shipping_city,
  15.         shipping_state)    
  16. values ( 'online distributors',
  17.         '2323 louise dr.'
  18.         17055,
  19.         'pittsburgh',
  20.         'pa',
  21.         '2323 louise dr.',
  22.         17055,
  23.         'pittsburgh'
  24.         'pa') ;    
  25.   
  26. -- 新增订单日期为2020年10月25日的9条订单。  
  27. set @start_date := unix_timestamp( '2020-10-25');    
  28. set @end_date := unix_timestamp( '2020-10-26');  
  29. drop table if exists temp_sales_order_data;    
  30. create table temp_sales_order_data as select * from sales_order where 1= 0;     
  31. set @customer_number := floor( 1 + rand() * 9);  
  32. set @product_code := floor( 1 + rand() * 4);       
  33. set @order_date := from_unixtime(@start_date + rand() * (@end_date - @start_date));    
  34. set @amount := floor( 1000 + rand() * 9000);  
  35. set @quantity := floor( 10 + rand() * 90);  
  36. insert into temp_sales_order_data 
  37. values ( 121, @customer_number, @product_code, @order_date, @order_date, @amount, @quantity);    
  38. ... 新增9条订单 ...   
  39. insert into sales_order    
  40. select null,
  41.        customer_number,
  42.        product_code,
  43.        order_date,
  44.        entry_date,
  45.        order_amount,
  46.        order_quantity 
  47.   from temp_sales_order_data 
  48.   order by order_date;      
  49. commit ;

        上面的语句生成了两个表的测试数据。客户表更新了已有八个客户的送货地址,并新增编号为9的客户。销售订单表新增了九条记录。

(2)执行定期装载Kettle作业并查看结果。
        执行定期装载Kettle作业前,需要在spoon环境中点击菜单“工具”->“数据库”->“清除缓存”,清除数据库缓存。成功执行定期装载Kettle作业后查询dw.customer_dim表,应该看到已存在客户的新版本有了送货地址。老的过期版本的送货地址为空。9号客户是新加的,具有送货地址。查询dw.sales_order_fact表,应该只有9个订单有销售数量,老的销售数据数量字段为空。


  
  1. ...
  2. 121     13     1     1029     2095.00     47
  3. 122     11     4     1029     5937.00     11
  4. 123     18     2     1029     2138.00     20
  5. 124     12     2     1029     2151.00     20
  6. 125     12     1     1029     5740.00     97
  7. 126     10     4     1029     4893.00     38
  8. 127     16     2     1029     2786.00     11
  9. 128     11     4     1029     6804.00     13
  10. 129     12     1     1029     6181.00     59

二、维度子集

        有些需求不需要最细节的数据。例如更想得到某个月的销售汇总,而不是某天的数据。再比如相对于全部的销售数据,可能对某些特定状态的数据更感兴趣等。此时事实数据需要关联到特定的维度,这些特定维度包含在从细节维度选择的行中,所以叫维度子集。维度子集比细节维度的数据少,因此更易使用,查询也更快。

        有时称细节维度为基本维度,维度子集为子维度,基本维度表与子维度表具有相同的属性或内容,我们称这样的维度表具有一致性。一致的维度具有一致的维度关键字、一致的属性列名字、一致的属性定义以及一致的属性值。如果属性的含义不同或者包含不同的值,维度表就不是一致的。

        子维度是一种一致性维度,由基本维度的列与行的子集构成。当构建聚合事实表,或者需要获取粒度级别较高的数据时,需要用到子维度。例如,有一个进销存业务系统,零售过程获取原子产品级别的数据,而预测过程需要建立品牌级别的数据。无法跨两个业务过程模式,共享单一产品维度表,因为它们需要的粒度是不同的。如果品牌表属性是产品表属性的严格的子集,则产品和品牌维度仍然是一致的。在这个例子中需要建立品牌维度表,它是产品维度表的子集。对基本维度和子维度表来说,属性(如品牌和分类描述)是公共的,其标识和定义相同,两个表中的值相同,然而,基本维度和子维度表的主键是不同的。注意:如果子维度的属性是基本维度属性的真子集,则子维度与基本维度保持一致。

        还有另外一种情况,就是当两个维度具有同样粒度级别的细节数据,但其中一个仅表示行的部分子集时,也需要一致性维度子集。例如,某公司产品维度包含跨多个不同业务的所有产品组合,如服装类、电器类等等。对不同业务的分析可能需要浏览企业级维度的子集,需要分析的维度仅包含部分产品行。与该子维度连接的事实表必须被限制在同样的产品子集。如果用户试图使用子集维度,访问包含所有产品的集合,则因为违反了参照完整性,他们可能会得到预料之外的查询结果。需要认识到这种造成用户混淆或错误的维度行子集的情况。

        ETL数据流应当根据基本维度建立一致性子维度,而不是独立于基本维度,以确保一致性。本节中将准备两个特定子维度,月份维度与Pennsylvania州客户维度。它们均取自现有的维度,月份维度是日期维度的子集,Pennsylvania州客户维度是客户维度的子集。

1. 建立包含属性子集的子维度

        当事实表获取比基本维度更高粒度级别的度量时,需要上卷到子维度。在销售订单示例中,当除了需要日销售数据外,还需要月销售数据时,会出现这样的需求。下面的脚本用于建立月份维度表。


  
  1. use dw;  
  2. -- 建立月份维度表  
  3. create table month_dim (    
  4.     month_sk int comment '月份代理键',  
  5.     month tinyint comment '月份',  
  6.     month_name varchar( 9) comment '月名称',  
  7.     quarter tinyint comment '季度',  
  8.     year smallint comment '年份'  
  9. )  
  10. comment '月份维度表'  
  11. row format delimited fields terminated by ','  
  12. stored as textfile;

        然后创建如图8-4所示的Kettle转换用于装载月份维度表。

图8-4 装载月份维度表的转换

该转换包括五个步骤。第一个步骤是“表输入”,获取日期维度表数据,其SQL查询语句如下:

select date_sk, dt, month, month_name, quarter, year from dw.date_dim;

第二个步骤是“排序记录”,按date_sk字段升序进行排序。第三个步骤“去除重复记录”中,用来比较的字段为month、month_name、quarter、year,即按这字段去重。第四的步骤“增加序列”用于生成month_sk字段值。最后的“Hadoop file output”步骤将生成的文本文件上传到month_dim表所对应的HDFS目录下。该步骤属性如下:

“文件”标签

  • Hadoop Cluster:选择“CDH631”。
  • Folder/File:输入“/user/hive/warehouse/dw.db/month_dim/month_dim.csv”。该路径是month_dim表所对应的HDFS路径。

其它属性都为空。

        
“内容”标签

  • 分隔符:输入“,”。这是我们在创建month_dim表时选择的文本文件列分隔符。
  • 封闭符:空。
  • 头部:勾掉。
  • 格式:选择“LF terminated(Unix)”。
  • 编码:选择“UTF-8”。

“字段”标签
输入如表8-1所示。

名称

类型

格式

精度

month_sk

Integer

 

0

month

Integer

 

0

month_name

String

 

 

quarter

Integer

 

0

year

Integer

 

0

表8-1 month_dim.csv文件对应的字段保存并执行转换,HDFS上生成的文件如下:


  
  1. [hdfs @manager~]$hdfs dfs -ls /user/hive /warehouse/dw.db /month_dim/
  2. Found 1 items
  3. -rw-r--r--   3 root hive       1326 2020 -10 -27 10: 27 /user/hive /warehouse/dw.db /month_dim/month_dim.csv
  4. [hdfs @manager~]$

查询month_dim表结果如下:


  
  1. hive> select * from month_dim;
  2. OK
  3. 1     1     January       1     2018
  4. 2     2     February      1     2018
  5. ...
  6. 59     11     November      4     2022
  7. 60     12     December      4     2022
  8. Time taken: 0.171 seconds, Fetched: 60 row(s)

        该转换可以重复执行多次,每次执行结果是相同的,即实现了所谓的“幂等操作”。除了利用已有的日期维度数据生成月份维度,我们还可以一次性生成日期维度和月份维度数据,只需对“Kettle构建Hadoop ETL实践(四):建立ETL示例模型()”中图4-4的转换稍加修改,如图8-5所示。

图8-5 同时生成日期维度和月份维度数据的转换

        转换中每个步骤的定义前面已做详细说明。第一排的四个步骤生成日期维度表数据,第二排的四个步骤生成月份维度表数据。“JavaScript代码”步骤的输出被复制到“Hadoop file output”、“排序记录”两个步骤。

        一致性日期和月份维度是用于展示行和列维度子集的独特实例。显然,无法简单地使用同样的日期维度访问日或月事实表,因为它们的粒度不同。月维度中要排除所有不能应用月粒度的列。例如,假设日期维度有一个促销期标志列,用于标识该日期是否属于某个促销期之中。该列不适用月层次上,因为一个月中可能有多个促销期,而且并不是一个月中的每一天都是促销期。促销标记适用于天这个层次。

2. 建立包含行子集的子维度

        当两个维度处于同一细节粒度,但是其中一个仅仅是行的子集时,会产生另外一种一致性维度构造子集。销售订单示例中,客户维度表包含多个州的客户信息。对于不同州的销售分析可能需要浏览客户维度的子集,需要分析的维度仅包含部分客户数据。通过使用行的子集,不会破坏整个客户集合。当然,与该子集连接的事实表必须被限制在同样的客户子集中。

        月份维度是一个上卷维度,包含基本维度的上层数据。而特定维度子集是选择基本维度的行子集。执行下面的脚本建立特定维度表。


  
  1. use dw;    
  2. create table pa_customer_dim (    
  3.     customer_sk int comment '代理键',   
  4.     customer_number int comment '客户编号,业务主键',    
  5.     customer_name varchar( 50) comment '客户名称',    
  6.     customer_street_address varchar( 50) comment '客户住址',  
  7.     customer_zip_code int comment '邮编',    
  8.     customer_city varchar( 30) comment '所在城市',    
  9.     customer_state varchar( 2) comment '所在省份',   
  10.     shipping_address varchar( 50) comment '送货地址',   
  11.     shipping_zip_code int comment '送货邮编',   
  12.     shipping_city varchar( 30) comment '送货城市',     
  13.     shipping_state varchar( 2) comment '送货省份',   
  14.     version int comment '版本号',  
  15.     effective_date date comment '生效日期',   
  16.     expiry_date date comment '到期日期'   
  17. )  
  18. clustered by (customer_sk) into 8 buckets      
  19. stored as orc tblproperties ( 'transactional'= 'true');

注意,PA客户维度子集与月份维度子集有两点区别:

  • . pa_customer_dim表和customer_dim表有完全相同的列(除了列的顺序),而month_dim不包含date_dim表的日期列。
  • . pa_customer_dim表的代理键就是客户维度的代理键,而month_dim表里的月份维度代理键并不来自日期维度,而是独立生成的。

        通常在基本维度表装载数据后,进行包含其行子集的子维度表的数据装载。我们修改定期装载Kettle作业,增加对PA客户维度的处理,这里只是在装载完customer_dim后简单重载PA客户维度数据,只要在“装载客户维度表”步骤中的SQL脚本中,增加对pa_customer_dim的处理即可。下面为增加的部分。


  
  1. -- 装载customer维度
  2. -- 设置已删除记录和地址相关列上scd2的过期
  3. ...
  4. -- 处理地址列上scd2的新增行
  5. ...
  6. -- 处理customer_name列上的scd1
  7. ...
  8. -- 处理新增的customer记录
  9. ...
  10. -- 重载pa客户维度
  11. truncate table pa_customer_dim;    
  12. insert into pa_customer_dim    
  13. select customer_sk,customer_number,customer_name,
  14.        customer_street_address,customer_zip_code,customer_city,customer_state,
  15.        shipping_address,shipping_zip_code,shipping_city,shipping_state,
  16.         version,effective_date,expiry_date    
  17.   from customer_dim    
  18.   where customer_state = 'pa' ;

        上面的语句在处理完客户维度表后,装载PA客户维度。每次重新覆盖pa_customer_dim表中的所有数据。先用truncate table语句清空表,然后用insert into ... select语句,从客户维度表中选取Pennsylvania州的数据,并插入到pa_customer_dim表中。之所以没有使用insert overwrite table这种一句话的解决方案,是因为对事务表使用overwrite会出错:

FAILED: SemanticException [Error 10295]: INSERT OVERWRITE not allowed on table with OutputFormat that implements AcidOutputFormat while transaction manager that supports ACID is in use

        保存修改后的定期装载Kettle作业,执行以下步骤测试PA客户子维度的数据装载。
(1)执行下面的SQL脚本往客户源数据里添加一个PA州的客户和四个OH州的客户。


  
  1. use source;  
  2. insert into customer  
  3. (customer_name, customer_street_address, customer_zip_code, customer_city, customer_state, shipping_address, shipping_zip_code, shipping_city, shipping_state)  
  4. values  
  5. ( 'pa customer', '1111 louise dr.', '17050', 'mechanicsburg', 'pa', '1111 louise dr.', '17050', 'mechanicsburg', 'pa'),  
  6. ( 'bigger customers', '7777 ridge rd.', '44102', 'cleveland', 'oh', '7777 ridge rd.', '44102', 'cleveland', 'oh'),   
  7. ( 'smaller stores', '8888 jennings fwy.', '44102', 'cleveland', 'oh', '8888 jennings fwy.', '44102', 'cleveland', 'oh'),  
  8. ( 'small-medium retailers', '9999 memphis ave.', '44102', 'cleveland', 'oh', '9999 memphis ave.', '44102', 'cleveland', 'oh'),  
  9. ( 'oh customer', '6666 ridge rd.', '44102', 'cleveland', 'oh', '6666 ridge rd.', '44102', 'cleveland', 'oh') ;  
  10. commit;

以上代码在一条insert into ... values语句中插入多条数据,这种语法是MySQL对标准SQL语法的扩展。

(2)执行定期装载Kettle作业并查看结果。
        使用下面的查询验证结果:


  
  1. select customer_number, customer_name, customer_state, effective_date, expiry_date 
  2.   from dw.pa_customer_dim
  3.   order by customer_number;

3. 使用视图实现维度子集

        为了实现维度子集,我们创建了新的子维度表。这种实现方式还有两个主要问题,一是需要额外的存储空间,因为新创建的子维度是物理表;二是存在数据不一致的潜在风险。本质上,只要相同的数据存储多份,就会有数据不一致的可能。这也就是为什么在数据库设计时要强调规范化以最小化数据冗余的原因之一。为了解决这些问题,还有一种常用的做法是在基本维度上建立视图生成子维度。下面是创建子维度视图的HiveQL语句。


  
  1. use dw;
  2. -- 建立月份维度视图
  3. create view month_dim as
  4. select row_number() over ( order by t1.year,t1.month) month_sk, t1.*
  5.   from ( select distinct month, month_name, quarter, year from date_dim) t1;
  6. -- 建立PA维度视图
  7. create view pa_customer_dim as 
  8. select * from customer_dim where customer_state = 'pa';

        这种方法的主要优点是:实现简单,只要创建视图,不需要修改原来的逻辑;不占用存储空间,因为视图不真正存储数据;消除了数据不一致的可能,因为数据只有一份。虽然优点很多,但此方法的缺点也十分明显:当基本维度表和子维度表的数据量相差悬殊时,性能会比物理表差得多;如果定义视图的查询很复杂,并且视图很多的话,可能会对元数据存储系统造成压力,严重影响查询性能。下面我们看一下Hive对视图的支持。

        Hive从0.6版本开始支持视图功能。视图具有唯一的名字,如果所在数据库中已经存在同名的表或视图,创建语句会抛出错误信息,可以使用CREATE ... IF NOT EXISTS语句跳过错误。如果在视图定义中不显式写列名,视图列的名字自动从select表达式衍生出来。如果select包含没有别名的标量表达式,例如x+y,视图的列名将会是_c0、_c1等等。重命名视图的列名时,可以给列增加注释。注释不会自动从底层表的列继承。

        注意视图是与存储无关的纯粹的逻辑对象,本环境的Hive 2.1.1版本不支持物化视图。当查询引用了一个视图,视图的定义被评估后产生一个行集,用作查询后续的处理。这只是一个概念性的描述,实际上,作为查询优化的一部分,Hive可能把视图的定义和查询结合起来考虑,而不一定是先生成视图所定义的行集。例如,优化器可能将查询的过滤条件下推到视图中。

        一旦视图建立,它的结构就是固定的,之后底层表的结构改变,如添加字段等,不会反映到视图的结构中。如果底层表被删除了,或者表结构改变成一种与视图定义不兼容的形式,视图将变为无效状态,其上的查询将失败。

        视图是只读的,不能对视图使用LOAD或INSERT语句装载数据,但可以使用alter view语句修改视图的某些元数据。视图定义中可以包含order by和limit子句,例如,如果一个视图定义中指定了limit 5,而查询语句为select * from v limit 10,那么至多会返回5行记录。使用SHOW CREATE TABLE语句会显示创建视图的CREATE VIEW语句。

三、角色扮演维度

        单个物理维度可以被事实表多次引用,每个引用连接逻辑上存在差异的角色维度。例如,一个销售订单有一个订单日期,还有一个请求交付日期,这时就需要引用日期维度表两次。每个日期通过外键引用不同的日期维度,原则上每个外键表示不同的日期维度视图,这样引用具有不同的含义。这些不同的维度视图具有唯一的代理键列名,被称为角色,相关维度被称为角色扮演维度。

        我们期望在每个事实表中设置日期维度,因为总是希望按照时间来分析业务情况。在事务型事实表中,主要的日期列是事务日期,如订单日期。有时会发现其它日期也可能与每个事实关联,如订单事务的请求交付日期。每个日期应该成为事实表的外键。

        本节将说明两类角色扮演维度的实现,分别是表别名和数据库视图。这两种实现都使用了Hive支持的功能。表别名是在SQL语句里引用维度表多次,每次引用都赋予维度表一个别名。而数据库视图,则是按照事实表需要引用维度表的次数,建立相同数量的视图。我们先修改销售订单数据库模式,添加一个请求交付日期字段,并对Kettle ETL作业做相应的修改。这些表结构修改好后,插入测试数据,演示别名和视图在角色扮演维度中的用法。

1.修改数据库模式

        使用下面的脚本修改数据库模式。分别给数据仓库里的事实表sales_order_fact和源库中销售订单表sales_order增加request_delivery_date_sk和request_delivery_date字段。


  
  1. -- in hive  
  2. -- 修改数据仓库中的事实表sales_order_fact
  3. use dw;   
  4. alter table sales_order_fact add columns (request_delivery_date_sk int comment '请求交付日期') ;
  5. -- 修改过渡区的sales_order表 
  6. use rds;    
  7. alter table sales_order add columns (request_delivery_date date comment '请求交付日期') ;    
  8. -- in mysql
  9. use source;    
  10. alter table sales_order add request_delivery_date date after order_date ;

        增加列的过程已经在本篇开头详细讨论过。在销售订单事实表上增加请求交付日期代理键字段,数据类型是整型。已有记录在该新增字段上的值为空。过渡区的销售订单表也增加请求交付日期字段。与订单日期不同的是,该列的数据类型是date,不考虑请求交付日期中包含时间的情况。因为不支持after语法,新增的字段会加到所有已存在字段的后面。最后给源数据库的销售订单事务表增加请求交付日期列,同样是date类型。修改后DW数据库模式如图8-6所示。

图8-6 数据仓库中增加请求交付日期属性

        从图中可以看到,销售订单事实表和日期维度表之间有两条连线,表示订单日期和请求交付日期都是引用日期维度表的外键。注意,虽然图中显示了表之间的关联关系,但Hive中并没有主外键数据库约束。

2. 修改Kettle定过期装载作业

(1)修改增量抽取sales_order的Kettle转换
        由于增加了数据列,定期装载Kettle作业中,装载销售订单过渡区表和销售订单事实表的转换需要做相应的修改。对于过渡区rds.sales_order表的增量数据抽取转换,需要修改两个地方。一是“数据库连接”步骤要保持输出字段的顺序与rds.sales_order表相同,因此修改SQL语句为:


  
  1. select order_number,
  2.        customer_number,
  3.        product_code,
  4.        order_date,
  5.        entry_date,
  6.        order_amount,
  7.        order_quantity,
  8.        request_delivery_date
  9.   from source.sales_order 
  10.   where entry_date >= ? and entry_date < ?

二是在“Hadoop file output”步骤中增加request_delivery_date字段,同样要注意保持字段顺序与rds.sales_order表相同,如图8-7所示。

图8-7 在“Hadoop file output”步骤中添加request_delivery_date字段

(2)修改定期装载事实表的转换
        定期装载事实表的转换需要修改两个地方。一是“销售订单事务数据”数据库连接步骤的SQL查询语句中增加request_delivery_date列,改为:


  
  1. select b.order_sk,
  2.         c.customer_sk,
  3.        d.product_sk,
  4.        e.date_sk,
  5.        a.order_amount,
  6.        a.order_quantity,
  7.        f.date_sk request_delivery_date_sk
  8.   from rds.sales_order a,
  9.        dw.order_dim b,
  10.        dw.customer_dim c,
  11.        dw.product_dim d,
  12.        dw.date_dim e,
  13.        dw.date_dim f
  14.   where a.order_number = b.order_number
  15.    and a.customer_number = c.customer_number
  16.    and a.order_date >= c.effective_date
  17.    and a.order_date < c.expiry_date
  18.    and a.product_code = d.product_code
  19.    and a.order_date >= d.effective_date
  20.    and a.order_date < d.expiry_date
  21.    and to_date(a.order_date) = e.dt
  22.    and to_date(a.request_delivery_date) = f.dt
  23.    and a.entry_date >= ? and a.entry_date < ? 

二是“ORC output”步骤中增加request_delivery_date字段,注意保持字段顺序与dw.sales_order_fact表相同,如图8-8所示。

图8-8 “ORC output”步骤中增加request_delivery_date字段

3. 测试

(1)执行下面的SQL脚本在源库中增加三个带有交货日期的销售订单。


  
  1. use source;  
  2. /*** 新增订单日期为2020年10月27日的3条订单。***/      
  3. set @start_date := unix_timestamp( '2020-10-27');      
  4. set @end_date := unix_timestamp( '2020-10-28');   
  5. set @request_delivery_date := '2020-10-30';     
  6. drop table if exists temp_sales_order_data;      
  7. create table temp_sales_order_data as select * from sales_order where 1= 0;       
  8. set @customer_number := floor( 1 + rand() * 14);  
  9. set @product_code := floor( 1 + rand() * 4);   
  10. set @order_date := from_unixtime(@start_date + rand() * (@end_date - @start_date));      
  11. set @amount := floor( 1000 + rand() * 9000);    
  12. set @quantity := floor( 10 + rand() * 90);    
  13. insert into temp_sales_order_data 
  14. values ( 130, @customer_number, @product_code, @order_date, @request_delivery_date, @order_date, @amount, @quantity);      
  15. ... 插入3条订单记录 ...
  16. insert into sales_order      
  17. select null,customer_number,product_code,order_date,
  18. request_delivery_date,entry_date,order_amount,order_quantity 
  19. from temp_sales_order_data order by order_date;        
  20. commit ;

        以上代码在源库中新增了三条销售订单记录,订单日期为2020年10月27日,请求交付日期为2020年10月30日。(2)执行定期装载Kettle作业并查看结果。
        使用下面的查询验证结果。


  
  1. use dw;  
  2. select a.order_sk, request_delivery_date_sk, c.dt 
  3.   from sales_order_fact a, date_dim b, date_dim c  
  4.   where a.order_date_sk = b.date_sk   
  5.     and a.request_delivery_date_sk = c.date_sk ; 
  6. +-------------+---------------------------+-------------+
  7. | a.order_sk   | request_delivery_date_sk   |     c.dt     |
  8. +-------------+---------------------------+-------------+
  9. | 130         | 1034                       | 2020-10-30   |
  10. | 131         | 1034                       | 2020-10-30   |
  11. | 132         | 1034                       | 2020-10-30   |
  12. +-------------+---------------------------+-------------+
  13. 3 rows selected (36.268 seconds)

可以看到只有三个新的销售订单具有request_delivery_date_sk值,日期代理键1034对应的是2020年10月30日。(4)使用角色扮演维度查询


  
  1. -- 使用表别名查询  
  2. use dw;    
  3. select order_date_dim.dt order_date,    
  4.        request_delivery_date_dim.dt request_delivery_date,    
  5.         sum(order_amount), count(*)    
  6.   from sales_order_fact a,
  7.        date_dim order_date_dim,    
  8.        date_dim request_delivery_date_dim    
  9.   where a.order_date_sk = order_date_dim.date_sk    
  10.     and a.request_delivery_date_sk = request_delivery_date_dim.date_sk    
  11.   group by order_date_dim.dt , request_delivery_date_dim.dt    
  12. cluster by order_date_dim.dt , request_delivery_date_dim.dt;  
  13.   
  14. -- 使用视图查询  
  15. use dw; 
  16.  
  17. -- 创建订单日期视图  
  18. create view order_date_dim 
  19. (order_date_sk, 
  20.  order_date, 
  21.   month
  22.  month_name,  
  23.   quarter
  24.   year
  25. as select * from date_dim;    
  26. -- 创建请求交付日期视图
  27. create view request_delivery_date_dim
  28. (request_delivery_date_sk, 
  29.  request_delivery_date, 
  30.   month
  31.  month_name, 
  32.   quarter
  33.   year)   
  34. as select * from date_dim;  
  35. -- 查询
  36. select order_date,request_delivery_date, sum(order_amount), count(*)    
  37.   from sales_order_fact a,order_date_dim b,request_delivery_date_dim c    
  38.   where a.order_date_sk = b.order_date_sk    
  39.     and a.request_delivery_date_sk = c.request_delivery_date_sk    
  40.   group by order_date , request_delivery_date    
  41. cluster by order_date , request_delivery_date;

        上面两个查询等价。尽管不能连接到单一的日期维度表,但可以建立并管理单独的物理日期维度表,然后使用视图或别名建立两个不同日期维度的描述。注意在每个视图或别名列中需要唯一的标识。例如,订单日期属性应该具有唯一标识order_date以便与请求交付日期request_delivery_date区别。此外,HiveQL支持使用别名,别名与视图在查询中的作用并没有本质的区别,都是为了从逻辑上区分同一个物理维度表。许多BI工具也支持在语义层使用别名。但是,如果有多个BI工具,连同直接基于SQL的访问,都同时在组织中使用的话,不建议采用语义层别名的方法。当某个维度在单一事实表中同时出现多次时,则会存在维度模型的角色扮演。基本维度可能作为单一物理表存在,但是每种角色应该被当成标识不同的视图展现到BI工具中。

        在标准SQL中,使用order by子句对查询结果进行排序,而在上面的查询中使用的是cluster by子句,这是Hive有别于SQL的地方。Hive中的order by、sort by、distribute by、cluster by子句都用于对查询结果进行排序,但处理方式是不一样的。

        Hive中的order by跟传统的SQL语言中的order by作用是一样的,会对查询的结果做一次全局排序,所以如果使用了order by,所有的数据都会发送到同一个reducer进行处理。不管有多少map,也不管文件有多少的block只会启动一个reducer,因为多个reducer无法保证全局有序。对于大量数据这将会消耗很长的时间去执行。

        如果HiveQL语句中指定了sort by,那么在每个reducer端都会做排序,也就是说保证了局部有序。每个reducer出来的数据是有序的,但是不能保证所有数据全局有序,除非只有一个reducer。这样做的好处是,执行了局部排序之后可以为接下去的全局排序提高不少的效率(其实就是做一次归并排序就可以做到全局有序了)。

        ditribute by是控制map的输出在reducer是如何划分的。假设有一张名为store商店表,mid是指这个商店所属的商户,money是这个商户的盈利,name是商店的名字。执行Hive查询:


  
  1. select mid, money, name 
  2.   from store distribute by mid sort by mid asc, money asc;

        所有mid相同的数据会被送到同一个reducer去处理,这就是因为指定了distribute by mid,这样的话就可以统计出每个商户中各个商店盈利的排序了。这肯定是全局有序的,因为相同的商户会放到同一个reducer去处理。这里需要注意的是distribute by必须要写在sort by之前。

        cluster by的功能就是distribute by和sort by相结合,但是排序只能是升序(至少Hive 1.1.0是这样的),不能指定排序规则为asc或者desc。获得与上面的查询语句一样的效果的cluster by写法如下:

select mid, money, name from store cluster by mid sort by money;

4. 一种有问题的设计

为处理多日期问题,一些设计者试图建立单一日期维度表,该表使用一个键表示每个订单日期和请求交付日期的组合:


  
  1. create table date_dim (date_sk int, order_date date, delivery_date date);
  2. create table sales_order_fact (date_sk int, order_amount int);

这种方法存在两个方面的问题。首先,如果需要处理所有日期维度的组合情况,则包含大约每年365行的清楚、简单的日期维度表将会极度膨胀。例如订单日期和请求交付日期存在如下多对多关系:


  
  1. 订单日期           请求交付日期
  2. 2020-10-27          2020-10-30
  3. 2020-10-28          2020-10-30
  4. 2020-10-29          2020-10-30
  5. 2020-10-27          2020-10-31
  6. 2020-10-28          2020-10-31
  7. 2020-10-29          2020-10-31

        如果使用角色扮演维度,日期维度表中只需要2020-10-27到2020-10-31五条记录。而采用单一日期表设计方案,每一个组合都要唯一标识,明显需要六条记录。当两种日期及其组合很多时,这两种方案的日期维度表记录数会相去甚远。

        其次,合并的日期维度表不再适合其它经常使用的日、周、月等日期维度。日期维度表每行记录的含义不再指唯一一天,因此无法在同一张表中标识出周、月等一致性维度,进而无法简单地处理按时间维度的上卷、聚合等需求。

四、层次维度

        大多数维度都具有一个或多个层次。如示例数据仓库中的日期维度就有一个四级层次:年、季度、月和日。这些级别用date_dim表里的列表示。日期维度是一个单路径层次,因为除了年-季度-月-日这条路径外,它没有任何其它层次。为了识别数据仓库里一个维度的层次,首先要理解维度中列的含义,然后识别两个或多个列是否具有相同的主题。例如,年、季度、月和日具有相同的主题,因为它们都是关于日期的。具有相同主题的列形成一个组,组中的一列必须包含至少一个组内的其它成员(除了最低级别的列),如在前面提到的组中,月包含日。这些列的链条形成了一个层次,例如,年-季度-月-日这个链条是一个日期维度的层次。除了日期维度,客户维度中的地理位置信息,产品维度的产品与产品分类,也都构成层次关系。表8-2显示了三个维度的层次。注意客户维度具有双路径层次。

customer_dim

product_dim

date_dim

customer_street_address

shipping_address

product_name

date

customer_zip_code

shipping_zip_code

product_category

month

customer_city

shipping_city

 

quarter

customer_state

shipping_state

 

year

表8-2 销售订单数据仓库中的层次维度

        本节描述处理层次关系的方法,包括在固定深度的层次上进行分组和钻取查询,多路径层次和参差不齐层次的处理等,最后单独说明Kettle中的递归处理。我们从最基本的情况开始讨论。

1. 固定深度的层次

        固定深度层次是一种一对多关系,例如,一年中有四个季度,一个季度包含三个月等等。当固定深度层次定义完成后,层次就具有固定的名称,层次级别作为维度表中的不同属性出现。只要满足上述条件,固定深度层次就是最容易理解和查询的层次关系,固定层次也能够提供可预测的、快速的查询性能。可以在固定深度层次上进行分组和钻取查询。

        分组查询是把度量按照一个维度的一个或多个级别进行分组聚合。图8-9所示的Kettle转换是一个分组查询的例子。该转换按产品(product_category列)和日期维度的三个层次级别(year、quarter和month列)分组返回销售金额。

图8-9 分组查询转换

该转换有三个步骤。第一个是表输入步骤,查询销售订单事实表的销售金额,SQL语句如下:


  
  1. select product_category, year, quarter, month,order_amount
  2.   from dw.sales_order_fact a, dw.product_dim b, dw.date_dim c    
  3.   where a.product_sk = b.product_sk    
  4.     and a.order_date_sk = c.date_sk

该步骤输出相关维度和度量的明细数据。这里直接用SQL进行表连接,而不要使用Kettle中的“数据库连接步骤”。“数据库连接”步骤会对每一行输入执行一次查询,在这个场景性能极差。

        第二个是排序记录步骤,在执行分组查询前需要先进行排序。排序的字段按顺序为product_category、year、quarter、month,均为升序。第三个步骤是分组,其分组字段和聚合操作如图8-10所示。Kettle转换中的步骤以数据流方式并行,本例中Kettle排序和聚合的操作要比Hive中的group by + cluster by快11%。

图8-10 分组求和

这是一个非常简单的分组查询转换,结果输出的每一行度量(销售订单金额)都沿着年-季度-月的层次分组,结果如下。


  
  1. +-------------------+-------+----------+--------+----------------+
  2. | product_category   | year   | quarter   | month   |   order_amount   |
  3. +-------------------+-------+----------+--------+----------------+
  4. | monitor           | 2020   |       4   |     10   |       42997.00   |
  5. | peripheral         | 2020   |       4   |     10   |       37554.00   |
  6. | storage           | 2020   |       1   |     3   |       98109.00   |
  7. | storage           | 2020   |       2   |     4   |       51765.00   |
  8. | storage           | 2020   |       2   |     5   |       89471.00   |
  9. | storage           | 2020   |       2   |     6   |     143495.00   |
  10. | storage           | 2020   |       3   |     7   |       87671.00   |
  11. | storage           | 2020   |       3   |     8   |       58064.00   |
  12. | storage           | 2020   |       3   |     9   |       10365.00   |
  13. | storage           | 2020   |       4   |     10   |       91107.00   |
  14. +-------------------+-------+----------+--------+----------------+

        与分组查询类似,钻取查询也把度量按照一个维度的一个或多个级别进行分组。但与分组查询不同的是,分组查询只返回分组后最低级别、即本例中月级别上的度量,而钻取查询返回分组后维度每一个级别的度量。图8-11所示的转换用于钻取查询,输出每个日期维度级别,即年、季度和月各级别的订单汇总金额。

图8-11 钻取查询转换

第一个是表输入步骤,查询销售订单事实表的销售金额,SQL语句如下:


  
  1. select product_category, year, quarter, month,dt,order_amount
  2.   from dw.sales_order_fact a, dw.product_dim b, dw.date_dim c    
  3.   where a.product_sk = b.product_sk    
  4.     and a.order_date_sk = c.date_sk

dt列用于不会出现在最终结果中,这里查询该列是为了后续对(year,quarter,month)组间进行排序显示。排序记录步骤,在执行分组前先进行排序,排序的字段按顺序为product_category、year、quarter、month、dt,均为升序。

        后面是三个分组步骤,先按product_category分组,然后分别按年、年-季度、年-季度-月分组,对order_amount求和,对dt求最小值,步骤的分组与聚合设置如图8-12所示。

图8-12 分别按年、年-季度、年-季度-月分组聚合

        后面的三个增加常量步骤,增加一个名为sequence的Integer类型字段,分别赋值1、2、3,用于对(year,quarter,month)组内进行排序显示。后面是三个选择字段步骤的设置如图8-13所示。

图8-13 选择年、季度、月字段

        之后的“排序合并”按product_category、dt、sequence、time的字段顺序升序合并。后面的JavaScript步骤中的代码如下:


  
  1. var time_str;
  2. if( sequence== 1)
  3. {
  4.    time_str = "year: ". concat( time);
  5. }
  6. else if ( sequence== 2)
  7. {
  8.    time_str = "quarter: ". concat( time);
  9. }
  10. else
  11. {
  12.    time_str = "month: ". concat( time);
  13. }

        这段代码用于输出显示,在年、季度、月的数字前拼接文字描述,增加结果可读性。该步骤在输出流中增加一个名为time_str的String类型字段。最后的“删除日期字段”一个是选择字段步骤,用于选择最终的输出字段,设置如图8-14所示。

图8-14 选择最终输出字段

        转换的输出结果如下:


  
  1. +-------------------+-------------+---------------+
  2. | product_category   |     time     | order_amount   |
  3. +-------------------+-------------+---------------+
  4. | monitor           | year: 2020   |     42997.00   |
  5. | monitor           | quarter: 4   |     42997.00   |
  6. | monitor           | month: 10   |     42997.00   |
  7. | peripheral         | year: 2020   |     37554.00   |
  8. | peripheral         | quarter: 4   |     37554.00   |
  9. | peripheral         | month: 10   |     37554.00   |
  10. | storage           | year: 2020   |     630047.00   |
  11. | storage           | quarter: 1   |     98109.00   |
  12. | storage           | month: 3     |     98109.00   |
  13. | storage           | quarter: 2   |     284731.00   |
  14. | storage           | month: 4     |     51765.00   |
  15. | storage           | month: 5     |     89471.00   |
  16. | storage           | month: 6     |     143495.00   |
  17. | storage           | quarter: 3   |     156100.00   |
  18. | storage           | month: 7     |     87671.00   |
  19. | storage           | month: 8     |     58064.00   |
  20. | storage           | month: 9     |     10365.00   |
  21. | storage           | quarter: 4   |     91107.00   |
  22. | storage           | month: 10   |     91107.00   |
  23. +-------------------+-------------+---------------+

2. 多路径层次

        本小节讨论多路径层次,它是对单路径层次的扩展。当前数据仓库的月维度只有一条层次路径,即年-季度-月这条路径。现在增加一个新的“促销期”级别,并且加一个新的年-促销期-月的层次路径。这时月维度将有两条层次路径,因此是多路径层次维度。        下面的脚本给month_dim表添加一个叫做campaign_session的新列,并建立rds.campaign_session过渡表。


  
  1. -- 增加促销期列  
  2. use dw;
  3. alter table month_dim add columns (campaign_session varchar( 30) comment '促销期') ;
  4. -- 建立促销期过渡表    
  5. use rds;  
  6. create table campaign_session 
  7. (campaign_session varchar( 30), month tinyint, year smallint)  
  8. row format delimited fields terminated by ',' stored as textfile;

假设所有促销期都不跨年,并且一个促销期可以包含一个或多个月份,但一个月份只能属于一个促销期。为了理解促销期如何工作,表8-3给出了一个促销期定义的示例。

促销期

月份

2020 年第一促销期

1月—4月

2020 年第二促销期

5月—7月

2020 年第三促销期

8

2020 年第四促销期

9月—12月

表8-3 2020年促销期

        每个促销期有一个或多个月。一个促销期也许并不是正好一个季度,也就是说,促销期级别不能上卷到季度,但是促销期可以上卷至年级别。假设2020年促销期的数据如下,并保存在campaign_session.csv文件中。


  
  1. 2020 First Campaign, 1, 2020
  2. 2020 First Campaign, 2, 2020
  3. 2020 First Campaign, 3, 2020
  4. 2020 First Campaign, 4, 2020
  5. 2020 Second Campaign, 5, 2020
  6. 2020 Second Campaign, 6, 2020
  7. 2020 Second Campaign, 7, 2020
  8. 2020 Third Campaign, 8, 2020
  9. 2020 Last Campaign, 9, 2020
  10. 2020 Last Campaign, 10, 2020
  11. 2020 Last Campaign, 11, 2020
  12. 2020 Last Campaign, 12, 2020

        把2020年的促销期数据装载进月维度的Kettle作业如图8-15所示。

图8-15 将促销期数据装载进月维度的作业

该作业调用的三个转换如图8-16所示。

图8-16 将促销期数据装载进月维度的三个转换


        第一个转换将本地的campaign_session.csv文件传输到rds.campaign_session表对应的HDFS目录,用以装载rds.campaign_session表数据。第二个转换中表输入步骤的SQL语句为:


  
  1. select a.month_sk,a. month,a.month_name,a.quarter,a. year,b.campaign_session 
  2.   from dw.month_dim a left join rds.campaign_session b on a. year=b. year and a. month=b. month

        将以上查询的结果输出到一个本地文件中。第三个步骤读取转换2生成的本地文件,上传到HDFS的/user/hive/warehouse/dw.db/month_dim/month_dim.csv,覆盖原有的dw.month_dim表所对应的month_dim.csv文件。这里利用的是通过向HDFS上传文本文件以达到装载对应表数据的方法。需要注意的是必须使用Kettle作业,因为三个转换必须串行。Kettle转换中的步骤是并行的,如果输入步骤中调用的是输出步骤中的对象,则不会得到想要的结果。

        成功执行作业后,查询dw.month_dim表可以看到2020年的促销期已经有数据,其它年份的campaign_session列值为null。

3. 参差不齐的层次

        在一个或多个级别上没有数据的层次称为不完全层次。例如在特定月份没有促销期,那么月维度就具有不完全促销期层次。本小节说明不完全层次,还有在促销期上如何应用它。下面是一个不完全促销期的例子,数据存储在ragged_campaign.csv文件中。2020年1月、4月、6月、9月、10月、11月和12月没有促销期。


  
  1. ,1,2020
  2. 2020 Early Spring Campaign,2,2020
  3. 2020 Early Spring Campaign,3,2020
  4. ,4,2020
  5. 2020 Spring Campaign,5,2020
  6. ,6,2020
  7. 2020 Last Campaign,7,2020
  8. 2020 Last Campaign,8,2020
  9. ,9,2020
  10. ,10,2020
  11. ,11,2020
  12. ,12,2020

还是可以用前面的Kettle作业向月份维度装载促销期数据,只需要将转换1中的输入文件换成ragged_campaign.csv即可。这里还做了一点修改,将转换2中表输入步骤的SQL改为:


  
  1. select a.month_sk,a. month,a.month_name,a.quarter,a. year,
  2.         if(length( trim(b.campaign_session))= 0, a.month_name, b.campaign_session) campaign_session
  3.   from dw.month_dim a left join rds.campaign_session b on a. year=b. year and a. month=b. month

        在有促销期的年月,campaign_session列填写促销期名称,有促销期的年份但没有促销期的月份,该列填写月份名称,没有促销期的年月保持为空。轻微参差不齐层次没有固定的层次深度,但层次深度有限。如地理层次深度通常包含3到6层。与其使用复杂的机制构建难以预测的可变深度层次,不如将其变换为固定深度位置设计,针对不同的维度属性确立最大深度,然后基于业务规则放置属性值。
 


转载:https://blog.csdn.net/wzy0623/article/details/109570543
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场