数据仓库的模型和join算法

   数据仓库中经典模型主要有两种:一种是雪花模型,另外一种就是星座模型。简单的来说,雪花模型中是一个事实表,和若干个维度表,事实表通过外键和维度表关联。星座模型中存在着不止一个事实表,引用若干维度表,事实表直接也相互关联,目前数据仓库领域公认的TPCH测试集所使用的数据就是一个星座模型。从某种意义上说星座模型是对雪花模型的一种优化,因为在雪花模型中,唯一的事实表很可能需要只做到了第三范式,如果进一步优化达到第四范式,雪花模型就演变成了星座模型。
   有过数据库调优经验的人会发现,在数据库中,第三范式有些时候可能性能比第四范式要好,具体来说,这个取决于具体的查询。第三范式相对第四范式来说,省去了两个表做连接的操作,但是付出的代价就是数据有冗余,在某些情况下,查询的性能要好于两个表之间的连接。因此,对于一个数据仓库的建模来说,具体是雪花模型还是星座模型,具体取决于实际的应用。在工业界的TPCH测试集中,整个的数据库的schema就是一个星座模型,两张事实表:lineitem和order表,其余均为维度表,表直接通过外键关联。TPCH的测试实例包括22个SQL,其中有2条SQL是针对lineitem表的单表查询,另外的20条SQL都涉及到了join。在这20条SQL中,其中有:一条SQL完全是对于维度表的查询;另外有一条SQL是对lineitem的查询,但是涉及到了子查询,这个简单的子查询一般在查询优化时,都会被上提作为一个左半连接;剩下的18条则都涉及到了内连接。所以,一个出色的数据库/数据仓库产品必须很好的处理这些join才能提供给有说服力的测试数据。
    因此,在这里,就不得不说说数据库中的join算法。join算法是一个很古老的话题,但是,也是一个很活跃的话题。
    目前,基本的传统关系数据库都提供了多种join算法。最简单的是nestloop,具体伪码如下:
    for each row R1 in the outer table
    for each row R2 in the inner table
        if R1 joins with R2
         {
            return (R1, R2)
          }
    nextloop适用于数据比较小的情况下使用,为了处理更大的数据,一般还都提供了基于hash的join和基于sort的merge-join。hash-join先选择一个小表作为outer table建立一张hash表,然后针对inner table 的每条记录在hash表中进行查找,很显然,hash表的查找算法复杂度为O(1),但是引入了建立hash表的代价;而merge-join则一般对两个表进行外部多路归并排序,然后,读取两个有序文件来做join。具体的算法这里就不一一列出了。
    这里说的数据较小,并不等价于说表很小,而是说join这个操作符所作用的数据很小。举个例子,对于SQL: select * from t1 inner join t2 on key1 where t1.key2 = a and t2.key3 = b,其中key2 和key3都不是索引或是键。一般来说,数据库会为这条SQL生成这样一个查询计划:
                          join
                        /      \
                       /        \
           filter(key2=a)     filter (key3 = b)
                     /            \
                    /              \
                 scan t1          scan t2
join的输入实际上是两个filter操作符的输出,而filter的输入来自下层的scan 操作符。数据库在确定采用何种join的算法时,对各种join算法的代价进行计算,选择最小的那个。
   一般来说,join算法中, nestloop的启动代价比较小,但是运行的代价比较大,因为nestloop这个操作符启动时的需要做的初始化步骤比较少,而对于hash-join和merge-join则需要一些准备工作,hash-join一般都需要初始化一个内置的hash表。merge-join需要排序。对于数据量超过一定阈值时,数据库一般倾向于根据代价来选择hash-join或是sort-merge-join (具体如何计算代价,回头有时间结合PG的源码再写一篇博客)。
    目前,PGSQL提供了前面提到的这些join算法,mysql 5.1只提供了nextloop(好久没有看mysql的源码,不清楚是否新版本的mysql在这方面的改进,抱歉)。相对来说,在数据量较大的情况下,nextloop的操作代价远大于hash-join或是sort-merge-join。假设这么一种情况:如果数据库所能使用内存只有L个单位(在PG中默认的一个单位一般是8K大小),而join运算符的outer table和 inner table分别需要的内存为M个单位和N个单位。而我们知道数据库都会为表提供缓存,对于这些缓存也有相应的替换算法,例如PGSQL中就提供了clock-wise算法,clock-wise可以看做是在FIFO的基础在再多给一次机会。而mysql的表的缓存管理主要取决于表的引擎类型,具体算法可以google。我们可以简单的将这些算法看做是FIFO。以nextloop为例,假设读取或是写入一个单位大小的磁盘数据需要一次IO,如果M和N都小于L,那么就可以将一个表先读入内存,然后逐块读入另外一个表的数据,来做join,这样操作的代价为M+N次IO,而如果N和M都大于等于L,则会因为页面的替换算法,不可避免的将已读取的页的换出,这样的操作代价就是M*N次IO。 这样的结论同样也适合hash join。而sort-merge-join来说,在内存有限的情况下,操作的复杂度则是O(M*log(M)+N*log(N))。在数据量比较大的情况下,很明显,merge-join的操作代价小很多。
   在这里需要指出的是,之所以把一个复杂的查询代价简化成IO代价,主要因为是目前在单机上,磁盘的访问速度远低于内存和CPU上的cache,数据仓库中SQL主要的开销还是在磁盘上。
   因此,如果join需要处理相对比较大的数据时,hash-join和smerge-join要比nextloop 快很多。所以,在这方面PGSQL相对来说要优于mysql。因此,从这方面来说,目前商业的MPP系统(例如Greenplum和AsterData)选择PGSQL做为内部的节点不是没有理由的。
   而在分布式的数据仓库中,为了处理join,也设计了若干种算法。这些算法和传统关系数据库中join算法有着很大的联系。比较经典的分布式数据仓库(例如teradata)一般使用share-nothing的架构,相关的研究论文可以搜索gamma系统,威斯康星麦迪逊分校在这个项目上发表了诸多用意义的论文,大家可以google。
   share-nothing的数据仓库,简单的架构可以看作是一个中心节点和若干从节点,中心节点将查询处理之后,产生查询计划,然后将查询计划分发到各个节点执行,从节点上存储着水平分片的数据,从节点执行查询计划之后,将结果返回给中心节点,中心节点进一步处理之后将结果返回给用户。
    为了处理join, share-noting的分布式数据仓库中主要使用了三种常见的并行join算法:基于排序的并行join的算法,将参与join的算法按照连接的字段进行排序之后再分片,将各个分片分发到各个从节点,然后各个从节点执行sort-merge-join操作;基于hash的并行join算法,则在各个从节点上采用统一的hash算法进行分桶,然后将各个桶分发到对应的从节点上,从节点上执行join算法;全复制的并行join算法,这种算法适用于某个连接的表比较小的情况下,这个小表会在每个从节点上都有一个副本,然后直接执行join算法。
    之前,有同学问我:一个只有两个表做join的SQL在mysql上半天没有出结果,但是在N个datanode的hadoop上很快就有了结果,这个是为什么?对于这个答案,其实很简单,在这种情况下mysql上做join,操作代价退化为O(M*N),如果在hadoop上先对两个表直接hash分桶,然后join,操作的复杂度不过是O(N+M)(具体的复杂度可以根据实际的算法实现进行分析),很明显,这种简单的分布式join算法要明显快于单台的nextloop。
    可能这几种并行join算法比较古老,但是,如果你经常使用hadoop进行表之间的连接操作,你就会发现这三种算法会经常使用到。而hive中更是直接使用了mapjoin,事实上这就是经典的分布式并行数据仓库中使用的全复制的并行join算法。而在很多大数据的职位面试中,这些算法更是被经常提到,因此,在”大数据“这个烂大街的术语的时代里,这些算法应当成为常识。
   (本人对这方面理解不深,欢迎拍砖)

猜你喜欢

转载自scarbrofair.iteye.com/blog/2118757