kylin是用于DW/BI的一种OLAP工具,满足多维环境下的特定查询。
术语
-
维度(Dimension)
一组属性,提供结构化的标签信息,一般作为报表的坐标轴。
-
度量(Measure)
一类可以进行聚合分析的特殊维度,聚合后的结果称为指标。
-
事实表(Fact Table)
数据仓库中的中央表,用于描述业务内特定事件的数据。
-
维度表(Lookup Table)
维度属性的集合,人们观察数据的特定角度。
-
基度
指数据表中某一列数据去重后的元素个数。
-
星型模型(Star Schema)
一种多维的数据关系,由一张事实表和一组维度表组成。
-
cuboid
某一维度组合下,度量聚合后的结果集合。
-
数据立方体(cube)
一组用于分析数据的相关度量值和维度,是所有cuboid的集合,作为存储和分析的基本单位。
示例
以手机销售为例,表SALE记录各手机品牌在各个国家,每年的销售情况。表PHONE是手机品牌,表COUNTRY是国家列表,两表通过外键与SALE表相关联。这三张表就构成星型模型,其中SALE是事实表,PHONE、COUNTRY是维度表。
现在需要知道各品牌手机于2010-2012年,在中国的总销量,那么查询sql为:
SELECT b.`name`, c.`NAME`, SUM(a.count)
FROM SALE AS a
LEFT JOIN PHONE AS b ON a.`pId`=b.`id`
LEFT JOIN COUNTRY AS c ON a.`cId`=c.`id`
WHERE a.`time` >= 2010 AND a.`time` <= 2012 AND c.`NAME` = "中国"
GROUP BY b.`NAME`
其中时间(time), 手机品牌(b.name,后文用phone代替),国家(c.name,后文用country代替)是维度,而销售数量(a.count)是度量。手机品牌的个数可用于表示手机品牌列的基度。各手机品牌在各年各个国家的销量可作为一个cuboid,所有的cuboid组成一个cube,如下图所示:
上图展示了有3个维度的cube,每个小立方体代表一个cuboid,其中存储的是度量列聚合后的结果,比如苹果在中国2010年的销量就是一个cuboid。
当数据量比较少时,可以直接使用RDBMS执行sql,在较短时间内得到结果,但对于大数据量,比如5000万条,RDBMS的性能就满足不了要求了,需要使用别的查询方案,如MPP,Hive。而kylin给我们提供了另外一个思路——预计算,即空间换时间,列出用户所有可能的查询sql,提前处理得到查询结果,并持久化到数据库中,在实际查询时,可复用之前计算的结果,速度可以达到毫秒级别。当然,用户的查询sql是多种多样,无法穷尽的,因此kylin假设用户查询局限于针对不同的维度组合,得到有限个指标的聚合结果。在实际生产中,这个假设符合一般的用户使用场景,所以,kylin仅遍历所有可能的维度组合,求得对应的cuboid,最终作为一个cube供用户查询。
原理
如上文所述,kylin使用了预计算的方法来加速查询,即针对一个星型拓扑结构的数据,预计算所有维度组合的指标,作为一个cube,保存在数据库中,提供特定模式sql的查询服务。首先给出kyin的架构:
kylin的整个架构包括三个部分:数据源(source),执行引擎(engine),存储(storage),元数据(metadata),其中执行引擎又分为查询引擎(sql查询转为cube查询)和cube构建引擎(构建cube)。以kylin2.1为例,其支持hive,kafka作为数据源,cube构建引擎支持spark和MapReduce,整体架构是一种可插拔的设计,支持自定义数据源,执行引擎和存储结构。此外,kyin支持查询push down操作,即对于cube不支持的sql,可通过上图中的Routing结构,将sql交由底层Hive执行。
概括的讲,kylin主要解决了以下两个问题:
- cube预计算
- sql查询转cube查询
cube预计算
kylin在计算cube时,有逐层算法和快速立方体算法,可根据用户需要进行配置。本文仅介绍逐层算法,因为其是最最容易理解的算法,并选择MapReduce作为执行引擎。核心部分是cuboid的计算,以示例中的3维模型为例,其可能的维度组合为:
图中每个顶点代表对应维数据的所有组合的指标结果集合,集合的元素个数等于各维度的基度值的乘积。以图中蓝色点为例,其代表不同手机在不同国家历年的总销量,假设phone的基度是4,国家的基度是8,那么该蓝色点包含有32(4*8)个cuboid。n维维度列的组合称为n-D cuboid,最高维cuboid也称为Base cuboid。cuboid的计算顺序由下至上,即先计算3个维度组合的cuboid,再计算2个维度组合的cuboid, 最后计算一个维度的cuboid:
- (2010, iphone, 中国)
- (2010, *,中国)
- (2010,*,*)
* 代表该列可取任意列值
除Base cuboid外,第n维cuboid结果可由n-1维结果聚合得到,以图中红色线为例,cuboid (2010,*,中国),可通过Base cuboid求和得到,即对2010年中国各手机品牌的销量求和。而Base cuboid可通过原始数据计算得到。
总的流程如下图所示:
- 根据star图,调用Hive,创建一张宽表。
- 对宽表中的维度列去除,并存到hdfs中
- 为去重后的维度列建词典
- 读取宽表数据,计算base cuboid
- 基于base cuboid,计算次级 cuboid
- 将cuboid批量导入hbase中
每种维度组合,都有对应的cuboid id,使用了位编码的方式表示组合关系,2.1版本使用64位的long型表示,这里省略为8位。还是以示例为例,
phone | encode | time | encode | country | encode | ||
---|---|---|---|---|---|---|---|
苹果 | 0 | 2010 | 0 | 中国 | 0 | ||
华为 | 1 | 2011 | 1 | 美国 | 1 | ||
小米 | 2 | 2012 | 2 | 日本 | 2 |
以
),最后cuboid的逻辑值为:
dimension | metric |
---|---|
2011,苹果 | 200 |
2012,华为 | 300 |
RowKey | totalSale |
---|---|
0000011010 | 200 |
0000011021 | 300 |
hbase对应的RowKey&Column值为
RowKey | totalSale |
---|---|
0000011010 | 200 |
0000011021 | 300 |
自此,一个基本cube构建过程就结束了。理论上对于N维维度,存在2N次种维度组合,而对于每种维度组合,其包含的cuboid的个数与列的基度有直接关系。维度设置不合理,不仅会增加cube构建时间,也同时会导致膨胀率较大的cube,因此有必要去除不需要的维度和维度组合。
cube优化
kylin在cube预计算时,主要进行了两方面的优化:
- 提出了聚合组的概念,去除不必要的维度组合,加速cube构建。
- 宽表数据重新分配,避免数据倾斜。
首先看第一点——聚合组,这是一个将维度进行分组,以求达到降低维度组合数目的手段。不同的聚合组内的维度之间不会计算cuboid,cuboid个数会明显降低,假设原来有(m+k)个维度,现在分为两个维度为m和k的聚合组,那么维度组合从2(m+k)降为2m+2k。聚合组的优化措施与具体的查询sql联系紧密,是一种定制优化,如果查询sql包含的维度是跨聚合组的,那么kylin需要较大的代价进行N-Coboid的聚合,最终得到所需要的结果,这需要在cube构建时,做仔细的评估。
同时,在聚合组内又存在以下三种优化方式:
-
强制维度(Mandatroy Dimensions)
每次查询都会携带的维度
-
层次维度(Hierarchy Dimensions)
具有层次关系的维度组成一个Hierarchy,如年、月、日。在cube中,如果不设置Hierarchy,会有年、月、日、年月、年日、月日6个cuboid,但是设置了Hierarchy后,会增加一个cuboid约束,即低level的Dimension一定会伴随高Level的Dimension一起出现。因此会存在group by year, group by year,month的查询,但是不可以查询group by month,day。
-
联合维度(Joint Dimensions)
会一同出现的查询维度。cuboid要么都不包含这些维度,要么包含所有的维度。
对于第二点——数据重新分配,kylin会对宽表执行再分配策略,即调用Hive的Distribute by,若配置了shard by,则按照该列的值重新划分数据位置,否则随机分配。
sql查询转cube查询
kylin的sql查询基于calcite,其实现了自己的一套优化规则,可进行sql优化并获取sql中的table、aggreation等查询信息,具体映射规则如下:
- table => cube
- group, column => cuboid id
- group,where => Row Key
- aggreations => Row value
从sql中获取了hbase需要扫描的Row key和需要的指标值,最后只需要将cube值转为sql查询结果样式即可。