【CMU 15-445/645 Database Systems】12 Query Processing - 01A

996 阅读5分钟

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

Query plan,执行计划。每一条SQL语句的执行都需要首先将它从关系代数的形式转化成一个执行计划。这个执行计划是一个树模型,数据从叶子节点流向root,逐层被操作。我们通常说的SQL语句被解析成的有向无环图(DAG)就是指的这个树模型。它本质上是对SQL语句中的每一个部分进行了拓扑排序。

Processing models

定义了系统如何执行一个plan。这里介绍了三种方式:迭代模型(火山模型),物化模型,向量化模型。

Iterator Model

多数row-based数据库使用的模型;也就是最经典的火山架构(Volcano)的实现。所谓的流水线(pipeline)执行方式指的也是这个模型。

每个操作符都实现了一个next()函数,next函数遍历集合里的所有数据来判断是否还有下一个符合该操作符所定义的next函数中条件的tuple。有就emit,没有返回null。

之所以叫做迭代模型,是因为每个next里都遍历一遍当前的所有数据(所有row)。

例如下面这一段sql:

几个值得关注的点:

  • 由于迭代执行next的特性,可以很容易实现输出控制(limit方法返回topN),直接控制next中的迭代次数即可。
  • 火山模型属于pull类型的执行方式,也就是算子的执行顺序是自上而下的(从root节点到叶子结点),上层节点调用自己的next()会引起对下层节点的“拉取”。
  • 上次next()调用下层next()会导致层层嵌套的函数调用,而且是虚函数的调用。而且由于是按照元组来进行迭代执行(tuple-at-a-time),这里带来的开销,在数据量很大的情况下,是不容忽视的。
  • 虽然被称为流水线方式,但是某些算子是会阻塞(block)流水线的,也就是需要等到子节点完成所有数据的处理后才能继续运转,比如join,order by,subquery。

Materialization Model

操作符将操作的结果进行物化(materialize),相当于给整合成一个single result。类比迭代模型,是逐个元组进行emit,这里是全部遍历完以后,作为一个整体进行return。

物化的结果可以是whole tuple(NSM,行存)或者subsets of columns(DSM,列存).

这种模型更适合OLTP,因为每次查询相关联的数据量都不大;因而,对于OLAP这种需要复杂语句、较多中间结果的查询就不太好,因为生成中间结果的开销会很大。

Vectorization Model

类似前两种的结合,也需要每个函数实现next(),也是通过emit逐次返回元组,但是不像迭代模型每次返回一条,这里是每次返回一个batch,batch size可以根据硬件情况或具体SQL进行调整。

适用于OLAP,简化了每个操作符调用next的次数。OLAP通常需要扫描大量的tuples。每次获得的tuple batch,操作符可以使用vectorized(SIMD, simplified)的操作去并行处理。

这就是我们常说的向量化执行模型。

Vectorized(SIMD)

向量化,可拆解成并发操作,N个数据每次操作1个转化成t个数据同时并行操作,每个只需要操作N/t个数据。SIMD指令是CPU的一种指令集,可以用在向量化执行引擎中。

对比

一言以蔽之,三种模型每次数据流动的内容是:

  • single tuple
  • all tuples
  • tuple batch

Query plan执行方向

前面提到的这三种模型都是pull类型,也就是自上而下;push类型,也就是自底向上的执行模型,是data-centric code generation的引擎,以数据为中心,尽可能的利用一条数据存储在寄存器中的时间去更多的执行相关的逻辑,也就是另一种很有效的优化方式codegen的思想。

总结

query的执行方式,可以分为两大类,pull模型和push模型,分别对应自上而下(top-to-bottom)和自底向上(bottom-to-top)这两种截然相反的逻辑。

pull模型,本文中介绍的三种都属于pull,本质上的区别在于每个算子之间,进行数据流动(交换)的尺度:tuple-at-a-time, or vector-at-a-time?还是直接传递一个物化视图上去。不同的方式有不同的适用场景,不能绝对的区分其优劣。这里涉及到了经典的火山模型(volcano)和向量化执行模型。

push模型,这里没有介绍,它将每一条数据作为中心,围绕一条数据进行逻辑的展开,将原本在多个流水执行的算子中的逻辑整合成一个逻辑,将多个for循环各自执行的逻辑整合到一个for循环中,显而易见的使执行变得简洁。

query处理阶段的两大优化方式,向量化执行和代码生成(codegen)分别对应实现这两种执行模型的引擎。这两种方法的提出的对引擎的性能起到了重大的提升作用,我也正在学习中,后面有机会专门写一篇讲讲。