大规模并行分析(MPP)数据库自诞生以来,为数据驱动型企业提供了巨大的机会。近日,HashData联合创始人王占伟围绕MPP数据库的基础原理及技术特点与网友进行了分享交流。本文摘选部分精彩观点,与大家共享。
如上图所示,MPP数据库内核主要包括模块查询、编译查询、优化任务调度查询、执行存储。
MPP数据库的API(application programming interface )或者命令行接收到了SQL查询请求之后,系统先经过查询编译,然后查询优化,通过任务调度执行从存储引擎里面把数据拉出来,计算出结果,再返回一个结果集给客户。
在数据库里面遇到的第一个模块叫查询编译。查询编译分大概分为三大块:词法分析、语法分析、语义检查。
一个查询语句,经过词法分析、语法分析、语义检查生成的结果叫做query tree,在经过优化器之后叫做 plan tree ,这个就是编译的作用。
查询编译模块在整个数据库里面是最基础的。查询编译之后的过程是查询优化。查询优化器主要负责两个功能:逻辑优化和物理优化。
逻辑优化是指基于关系代数的理论,把查询语句的 SQL 语句做一些等价变换。这里要注意的是,“等价”非常重要,如果把一个 SQL 语句变成了不等价的,结果就会出错。等价变换的目的是为了提供更多可用来参考这个查询计划的解空间。
物理优化是指选择最优的物理方案。选择优化的依据有两种,一种是基于规则的优化,在做等价变换的时候,如果变换之后的这个执行方案一定比变换之前的更优秀,那么会选择等价变换;如果变换之后的方案不一定更优秀,那就要考虑变换之前和变换之后的两种方案。前者相当于去做了剪枝,把解空间减掉了一半,后者相当于把解空间增加了一半。
第二类依据是基于代价的查询优化,接下来会重点讲述依据代价的查询优化。
以上图为例,要做到三表关联,关联操作要满足交换率和结合率。通过计算可以得出三种可能的执行方式。这三种方式里面哪种更优秀呢?这个时候要去进行评估,付出的成本越小,这个方案就越好。这样产生的查询计划就是基于代价的查询优化。接下来讲查询优化的代价模型。什么是代价模型?代价是怎么算出来的?代价就是服务器用了多少 CPU访问多少磁盘。在算法确定的情况下,网络连接其实是确定的,就能算出来一个操作所需要的开销。每一个操作的开销算出来之后,整个查询计划的开销也就能算出来。
查询计划准确度统计取决于信息的准确度GIGO、模型的准确度、代价模型的前置依赖等几个因素。
统计信息是基于代价的查询优化器的输入,如果统计信息不准,那它输出的查询计划一定是垃圾。
影响准确度的第二个因素就是模型是不是准确,因为一个算法,一个数据库的算法的模型是非常复杂的。模型能不能高度还原你的算法,对于查询计划准确度很重要。
代价模型普遍都有一些前置条件,假设这些模型没有相关性,那算出来一定是错的。
综上所述,查询计划的代价就是把所有的可能性都计算一遍,然后挑选其中最好的一个。
如上图中的例子,是两个表关联出来的一个分布式的查询计划。查询优化计算出来后,会得到一个查询计划(plant tree) ,它是一个树状结构。MPP 数据库通常会把一个任务分给所有的执行节点去执行。有没有特例呢?当我们确定一个任务只需要一个节点就能完成的时候,那我们可以把它调度为一个节点,这是一个特例。当然,有的时候我们会让调度器做得更灵活一点,把任务调度给一部分节点去执行,这也是一种策略。
查询计划确定后,就是查询执行。执行器大概可分为两种方式:
第一种方式叫做拉模式,这也是目前普遍采用的一种方式。拉模式可以理解为所有的计算机一起工作,一台机器计算出来一个数据后,交给下一台机器接着算,就像流水线上的工人一样。
另一种方式叫推模式。这种模式与拉模式正好相反,它是由下层节点先计算,然后将数据逐步推到最顶上。
接下来着重讲一下motion 节点。motion 节点负责分布式数据库里的数据交换。
在分布式数据库里,有两类数据的链路交换。一类数据是控制流,它传递的是这个控制命令,传递的是 plan 和一些控制的结果。执行的结果成功了还是失败了、有没有报错、错误信息是什么等都是这类结果。
另外一类的数据通路是数据流,也就是tuple 数据,motion节点处理的都是tuple 数据。这个数据流量非常大,它不像控制流比较小。它有一个特点,就是基于 TCP 协议去实现这个 motion 节点数据交换。
分布式数据库执行器如何去实现聚集操作?我们通常有三种处理方式:一阶段聚集、两阶段聚集、三阶段聚集。
一阶段聚集,是指这个聚集操作只有一台机器执行,和单机数据库一样。它的优点是简单,缺点就是慢,适用于数据量较少的场景。
两阶段聚集是分布式系统中最常见一个方法。这种方法先在每个节点上计算一遍 sum 中间结果,这样能减少数据总量,降低传输的代价,而且不同节点的 sum 都是并行计算的。
三阶段聚集场景非常少,比较特殊,这里不再细讲。
向量化和代码生成是近几年数据库执行器领域出现的新技术。这两种技术的出现都是为了提升性能,更准确地讲是为了提升 CPU 的运行性能。
实向量化和代码生成并不能提升 IO 的性能。以上图为例,就是计算一个元组所需要的指令周期。
解释执行的成本在于抽象的代价。那么怎么能把这些解释执行的代价降到最低?主要有两种手段,一种手段是使用计时编译的方法,这样生成的代码是非常紧凑的,消除了不必要的分支,消除了抽象的代价,并且进行了内链优化。这意味着可以把解释执行的成本降得非常低。
第二个方式就是提升分母,以此来摊薄解释执行的成本,这就是向量化。
向量化也好,还是用Jit计时编译的方法也好,都可以使用一个叫做SIMD指令。
SIMD指令是 CPU 提供的一种功能,它可以让用户以一条指令来计算多个数据。所以不论是内链优化、Jit 还是向量化,都可以用上SIMD指令。
具体实现过程中,向量化的实现可能相对来说容易一些,而运行器代码生成在做代码调试的时候相当的痛苦。我们通常选择其中一种来做。但实际上两种结合一起做 Jit 的方式在处理表达式计算的时候是非常有效的。同样的,我们也可以把它运用到元组解析上。
最后我们要讲的主题是存储引擎。通常大家谈论存储的时候,经常提到列存。那什么是列存?列存和行存有什么区别?
如上图所示,把数据以行的形式存放,一行存完了再存下一行,下一行存完了再存下一行,这是按行来存,叫做行存。行存主要面向事务型应用。
列存是指同一条数据被拆散在了不同的地方存储,同一列的数据、同一类型的数据紧密地存放在一起。列存主要面向分析型事务,具有更高效的压缩、节省IO等优势,缺点是对内存消耗较大。
那么分布式数据库的存储有什么特点呢?分布式数据库在存储方面一个十分重要的的概念叫做数据分布。刚才讲的分布式数据库会有很多个节点,每个节点处理的是不同的数据。那这个数据怎么来区别?谁处理哪些数据呢?这就考虑到数据分布了。
数据分布通常我们有两种办法:一种是随机分布;另一种是哈希分布,计算一个哈希值,然后分给大家,这样分布式比较有规律。存储引擎在设计时要考虑高可用。什么叫高可用?通常的做法是增加副本的数量。那么,增加副本的数量又会带来新的问题,即怎么保证副本之间的数据是一样的呢?现阶段常见的办法是采用一致性协议,比如状态机复制,从而保证多个副本之间的一致性,不会造成丢失数据。