查询编译综述

查询编译综述

前言

本文总结一下最近对查询编译/Query Compilation这个问题的学习结果。承接自上一篇Snapshot Isolation综述。本系列旨在通过系统视角对领域有全面认识,减少局部视角造成的局限。

研究这个问题是因为在大学期间对编译器/编程语言比较感兴趣,还曾翻译过一本LLVM的书籍,但后来因工作关系没再做继续探索;如今在数据库领域再次重逢,有种他乡遇故知的欣喜,所以便决定带着好奇一探究竟。

查询编译

所谓查询编译,是相对于查询解释的,那么什么是查询的解释执行?

这要从经典的Volcano开始说起。

在Volcano模型中,不论多复杂的查询,都会转成由关系代数表示的一个Query Execution Plan。而Plan由一个个小的Operator组成,例如HashJoin、Scan、IndexScan、Aggregation等等。为了连接不同的算子,它们遵循统一的接口,即迭代器模式:open/next/close,每个Operator从子节点的next接口获得一条数据,施加算子内部的计算逻辑,再返回给父节点。

Volcano模型有几个特征:

  • Extensible:Operator之间依赖于接口而非具体实现,很容易实现新的Operator替换现有的
  • Parallel:每个Operator可以在不同的线程运行,next不再是函数调用而是进程间通信,即可实现Operator之间的流水线化并行
  • Pipeline:每次调用next接口只需要返回一条数据,不需要把所有数据计算完成再返回,避免过多的数据物化的开销
  • Iterator pull:虽然称之为火山,但数据并不是从Plan Tree的底部网上喷发的,而是从顶部把数据通过迭代器pull上去的

在磁盘数据库的阶段,Volcano模型非常经济实惠,几乎所有主流的商业数据库都采用的这个模型。但发展到如今在硬件革新的背景下,这一模型开始暴露出一些局限性:

  • iterator overhead:实现iterator一般使用virtual function,每读出一行数据需要多次函数调用,带来一些额外开销
  • cache inefficiency:使用pull模型,数据加载到内存之后并不能保持在cache、register中,需要多次内存访问
  • CPU inefficiency:iterator模型中存在的大量分支语句、内存访问对CPU并不友好

基于这些问题,从业者从七十年代的System R便开始探索查询编译这条路,但一路坎坷,直到近些年才开始得到更多的实际应用,Impala、SparkSQL、PostgreSQL等工业产品也开始采用编译执行的方案。不过,查询编译仍然存在不少的想象空间,在工业界的讨论也相对较少,工业界的产品落地距离学术界的研究仍有一定差距。

本文围绕着查询编译的问题,从几个方向展开,试图获得对这一问题的较为全面的理解:

  • 所谓查询编译,编译什么?
  • 所谓编译,如何编译,编译成什么?

编译什么


回到前面的例子,对于一条SQL查询来说,至少有几样东西可以编译:

  • 谓词表达式:也就是SQL中的WHERE子句,例如上面的A.val = 123 AND A.id = C.a_id AND B.id = C.a_id;如果进行解释执行,那么就需要遍历这个AST,对每个运算符进行递归求值,虽然容易实现,但性能不容乐观
  • 数据解析:在反序列数据时,往往需要根据Schema进行解析,但提前知道Schema的情况下,可以对代码进行特化
  • 表达式:表达式还存在于Projection、GroupBy、Aggregation、Join等子句中,且计算结果不再是简单的Bool
  • Operator:这里的Operator指的是Volcano中的单个Operator,在具备Schema信息的情况下,很多执行分支可以去掉

Expression Compilation

Impala是工业界的一个典型代表,他们主要对数据解析、表达式计算方面进行了代码生成。参考自《Runtime Code Generation in Cloudera Impala》。

对于OLAP来说,一条简单的数据解析会在一次查询中需要执行几万几亿次,此时性能就显得重要了。左边的是传统的解释执行的代码实现,根据一行记录的每个字段的类型,调用相应的解析函数;虽然这个循环已经很紧凑,但其中有几个变量仍可以消除:字段个数,字段大小,字段类型。如果这三个变量都变成了常量,那么无论是编译器,还是Codegen,显然可以生成一个更加紧凑的代码。如右边所示,直接把数据解析完放到指定的偏移,不再需要分支或者地址计算。

另一个Codegen的地方是表达式计算。解释执行的实现中,往往是递归地解释每一个Operator、每一个Operand,会带来多次的函数调用;但实际上,它完全可以编译成右边这个函数,将递归的解释内联到一个表达式里;并且进一步,这个谓词表达式本身往往也可以被内联。

虽然这两个场景的代码生成看起来非常简单,但实际仍有一个问题,如何生成右边的代码?

Impala的做法比较实用主义。在已有的解释执行的基础上,如果开启了Codegen,就将一部分函数替换成Query-Specific的实现,替代通用的解释执行;编译则是把C++代码编译到LLVM IR,并插入一些查询特定的信息。这种方式较为简单,不用直接编写LLVM IR;但对于更多的优化仍然不够,因此仍有一部分代码会直接生成LLVM IR,来实现分支的消除、虚函数的内联。最终,将所有代码的LLVM IR表示再编译到native code去运行。

除此之外,借助LLVM IR这一通用表示,UDF也可以实现内联,对于C++编写的UDF将其编译到LLVM IR之后,再统一和Query 一起编译成native code,实现全局优化。

Operator Compilation

仅仅实现表达式和数据解析等局部的优化并没有改变Volcano模型本身的局限性,Query Plan的执行开销无法消除。

《Generating code for holistic query evaluation》试图从Query Operator的角度解决这一问题:

  • 不再使用iterator,而是采用了Data staging的方式,避免了iterator的开销
  • 使用代码模板,根据查询信息生成C源文件,再编译成动态库链接到数据库中执行
  • 由于所有Operator都采用编译的方式执行,更容易利用编译器实现所谓holistic优化

例如对于一个Select Operator来说,其代码模板就是遍历所有数据,直接调用谓词表达式,然后把数据放到Staging area;以此省去了谓词表达式的函数调用的开销,迭代器调用的开销。对于其他Operator的实现可以以此类推。

将所有Operator用这种方式生成相应的代码并链接起来之后,变得到这样一个查询引擎。

但这种方案的弊端也相当明显:

  • Data staging 的方式并不通用,对于大规模查询来说其内存开销过大
  • 代码模板的表达能力有限,没有进行更深层次的Operator之间的优化
  • 编译开销较大:从模板生成C语言,再调用编译器编译,其开销巨大

不过HIQUE的思路也引发我们的思考:

  • 如何将多个Operator进行更无缝地组合?
  • 如何减少编译开销?
  • 使用C++的template是不是也能够实现这种优化?将一个operator的参数都用C++的template实现,执行查询时再实例化template并编译运行

Pipeline Compilation

在《Efficiently Compiling Efficient Query Plans for Modern Hardware》一文中,Hyper对Operator Compilation进行了更深层次的探索。

为提高缓存局部性,Hyper提出了Pipelined Operator的思路:

  • 如果一个Operator在返回数据之前需要将数据从寄存器取到内存,那么称之为pipeline-breaker
  • 如果一个Operator需要把所有数据处理完才能返回,称之为full-pipeline-breaker
  • 在pipeline-breaker之间划分界限,称之为Pipeline Boundary
  • 在Pipeline Boundary内部,所有计算被内联到一个函数内部,不需要进行数据物化或传递,可以保持在寄存器中进行计算;例如简单的Select + Where,可以内联到一起

虽然思路很简单,但实现并不简单,完成这一想法需要对Volcano模型进行改造:

  • Volcano模型是Operator-Centric,数据在Operator之间流动;这一方式需要转变成Data-Centric,让数据在寄存器中,所有计算围绕着数据展开
  • 为此,原本的Pull模型需要改变成Push模型,数据从下往上push,在一个pipeline内部没有数据传递

为此:

  • 每个Operator不再提供next接口,而是换之以produce/consume
  • consume接口进行Codegen,例如对于scan来说,即生成全表扫描的代码,并调用parent operator的consume进行Codegen,实现的效果等同于手写了一个for循环然后调用if语句进行求值
  • produce则是驱动函数,从顶层的Operator递归向下,驱动所有Operator进行Codegen

在Codegen之后,便得到了这样一个非常紧凑几乎没有冗余(伪代码层面)的执行计划:

基于这样的Codegen技术,生成的代码几乎已经可以媲美手写的执行计划了,因此在TPC-H上也有很好的表现。

Pull VS Push

上面引出了Push的概念,虽然看起来CodeGen必须要用Push模型,但事实上Pull/Push可以脱离Codegen的语境单独存在。

同样是实现Select,如果是基于iterator,则是不断遍历child,直到遇到一个满足谓词条件的返回;如果是pull,通常就用回调的方式,如果满足谓词条件,则通过调用parent的回调函数将之返回给上层的operator。

但如果仅仅是这样就太过简单了,Pull/Push的区别在结合了Codegen之后显然更为显著。

UDF

在SQL查询中,除了数据库本身提供的函数之外,往往还存在一些UDF函数,来满足场景化的需求。对于数据库来说,这些UDF往往是个黑盒,无法深入内部进行优化。但对于一些UDF-Centric的场景,例如曾经的Spark(手写Operator),优化这些UDF就显得尤为重要。

在《An Architecture for Compiling UDF-centric Workflow》一文中,TUPLEWARE提出对UDF的优化。抛开其提出的新的编程模型不谈,其UDF优化主要在于:

  • 将UDF编译成LLVM IR,再用UDF Analyzer进行分析
  • Vectorizability:分析UDF是否可以向量化执行,对于1-1 map以及scalar Aggregation很容易识别
  • Compute Time:根据CPI对UDF的执行时间进行估算
  • Load Time:估算内存访问的开销,区分Memory-Bound和CPU-Bound

例如,这里对KMeans中使用的几个UDF进行分析,得到的统计结果如上图所示,区分出了Vectorizable的函数,以及每个函数估算的Compute Time、Load Time。

基于UDF Analyzer得到的统计信息,在查询优化阶段便可以对不同的UDF进行不同的优化策略:

  • Pipeline:根据UDF的Vectorizability,按照可向量化执行的和不可向量化的Operator将大的Query Plan拆分成多个子Pipeline,使得每个Pipeline按照最优的策略执行;因此,这里的执行策略是一种结合了Vectorization和Pipeline的Hybrid方案;以上图为例,一个KMeans计算中,将distance+minimum放在一个Pipeline中,reassign放在另一个Pipeline
  • Memory-Bound:对于一个Memory-Bound的Operator,将后面的Operator拆分成多个Pipeline意义已经不大,此时可以选择将Pipeline合并,减少内存带宽的占用
  • Operator:具体到单个Operator,也可以利用Vectorizability信息进行并行化;例如Aggregation,在Hash阶段可以向量化执行;在Select执行时,可以在Predicate evaluation进行向量化执行,再执行结果的物化,实现部分的向量化执行

在UDF-Centric的场景下,TUPLEWARE可以轻松获得十倍于Spark,以及秒杀HyPer等数据库的性能。

当然,TUPLEWARE本身的定位并不是一个数据库,和数据库进行比较并不合适;和Spark相比,对于短的任务又可以放弃容错机制,进一步领先于Spark;并且它的计算模型已经不同于Spark,引入了显示的共享状态,相比于Spark的广播变量机制更加LowLevel,几乎可以推测出,共享状态对于以上这些ML算法的实现有着很大的性能加成。所以,最终查询引擎的Codegen在整个Benchmark起到的比重,还需要细化分析,不能一概而论。

Relaxed Operator Fusion

TUPLEWARE中已经引入了Hybrid Pipeline Strategy的方法,即不再局限于纯粹的向量化执行或者纯粹的pipeline执行:整个执行计划中,一部分采用向量化,尽可能利用SIMD来并行计算,另一部分则采用Pipeline的方式融合多个Operator提高缓存局部性。

VLDB2018发表的《Relaxed Operator Fusion for In-Memory Databases: Making Compilation, Vectorization, and Prefetching Work Together At Last》则将这一思路进一步细化。以三种常见的优化技术为基础:

  • Operator Fusion:将多个Operator融合到一个循环中执行,可以避免在Operator之间传递数据;这通常需要借助Codegen技术来实现
  • Vectorized Processing:每个Operator不再是每次计算一条数据,而是计算一批数据,进而利用SIMD进行并行计算
  • Prefetching:利用预取来减少CPU停顿

为了融合这多种技术,引入了一个额外的Staging Operator,简单来说就是把child operator的计算结果做一下batch,再传递给parent。当然,纯粹地引入这个staging operator并没有什么用,它是为了enable接下来的Vectorized Processing和Prefetch优化:

  • Vectorized:对于每次处理一条数据的Operator来说,几乎不可能使用SIMD;在一次处理多条数据之后,SIMD成为了可能,对于Select、Join、Aggregation都有相应的算法,下图便是一个SIMD的谓词计算实现
  • Prefetch:在Hash Join这样的Operator中,大量的随机内存访问会导致CPU stall;因此这里使用Group Prefetching的方式来掩盖CPU stall;并使用了linear probe的开放地址hashtable,相比于封闭地址的hashtable对缓存更加友好

ROF提出的Staging point的方案和Volcano的Exchange Operator其实有一点神似,在尽可能不改变Query Plan的情况下,通过局部的增加Operator,使得整体获得额外的加成。

小结

上文讲述了一众对查询计划的编译,从表达式的编译,到Operator、Pipelined Operator,以及UDF。事实上对于DBMS来说,不仅是这些可以编译,还有更多的组件都可以通过编译来获得性能提升:

Any CPU intensive entity of database can be natively compiled if they have a similar execution pattern on different inputs.

如何编译

但以上显然忽略了一个很重要问题,如何编译?虽然现在不少查询编译的实现会选择LLVM IR作为目标语言,但是不是有其他选择?不同的选择之间的差异是什么?

接下来便是讨论如何编译的问题。

Machine code

早在上世纪七十年代的System R中,便有了查询编译的技术。(要知道,C语言是1972年才出现的)

The approach of compiling SQL statements into machine code was one of the most successful parts of the System R project. We were able to generate a machine-language routine to execute any SQL statement of arbitrary complexity by selecting code fragments from a library of approximately 100 fragments. The result was a beneficial effect on transaction programs, ad hoc query, and system simplicity.

当时的SystemR可以根据代码模板,直接将SQL编译成汇编,这在现在看来仍是相当hardcore,毕竟能够手写汇编的人已经不多了。但在八十年代早期,IBM便放弃了这个计划,考虑到较高的外部函数调用开销,汇编的较差的移植性,以及整个方案的可维护性。

由此可见,查询编译最大的一个阻碍,可能是工程上带来的复杂度,并使得项目的可维护性降低,导致其在很长一段时间内没有被广泛采用。

LLVM IR

LLVM IR几乎已经成为Codegen的事实标准,被工业界和学术界广泛采用,这里得益于几点:

  • LLVM IR的抽象层次比汇编要高:强类型,支持函数、复合类型等特性,使得手写LLVM IR的复杂度降低了不少
  • 强大的后端:编译器的各种优化主要在后端实现,对于任何语言只要编译成LLVM IR,后端都能够进行统一优化

虽然相对简单,但仍然是相对的,比起高级语言来说手写LLVM IR仍有不低的复杂度。比如说实现一个简单的for循环或者分支语句,仍需要类似汇编的跳转指令。

除了当红的LLVM IR之外,也有一些其他的JIT实现,例如libjit, lightning之类,不过都归入JIT这一类。

Virtual Machine bytecode

生成汇编或者LLVM IR基本是native code的专利,对于跑在JVM上的数据库来说,通常是选择生成bytecode了。

《Compiled Query Execution Engine using JVM》提出了将SQL查询编译成jvm bytecode的几个好处:

  • JVM本身往往具备JIT的能力,它可以自行对代码进行优化并编译成machine code
  • 在JVM平台可以享受到平台本身的免费午餐,得益于庞大的社区JVM会持续优化
  • JVM相对动态,反射等机制可以更简单地加载代码

只不过它实际并没有直接生成bytecode,而是生成了java 代码然后用外部的java编译器再编译成bytecode,通过反射机制加载进来。

现如今在java生态中已经有了更加简单的工具可用了,例如janino,可以动态编译java代码,进一步减少了编译的开销。

另一个很有意思的Codegen场景是LINQ to Object,在《Code generation for efficient query processing in managed runtimes》提出:

var qry_stmt = from s in source where s.Name == ‘London’ select s.Population;

在C#的LINQ to object中,可以编写类似SQL的代码对集合进行操作。编译时,则将这样的语句转换成基于迭代器、Lambda的实现,因此可以将LINQ to object视作一种语法糖。

这个转换的结果带来了效率的降低:

  • 用迭代器执行:迭代器具有额外的函数调用开销
  • 泛型和Lambda:任何简单的表达式都被Lambda包装过,且是泛型的,这也会带来额外开销
  • 缺少Operator优化:对于简单的order by X limit N来说,可以用一个heap进行优化,然而转换后的代码仍然只是简单的先执行orderby,再执行limit
  • 缺少查询优化:由于缺少Schema信息以及统计信息,转换的过程不具备数据库的查询优化的能力,只能进行简单的谓词下推

基于对这些问题的认识,他们选择了生成更加高效的C#代码,例如用简单的for循环替代迭代器的调用,内联lambda,甚至对于特定的数据会生成C代码来处理,进一步减少C#本身的开销。

借此可以联想到早期的Spark,基于RDD的手写lambda虽然编程简单,但lambda对优化器和执行起来说基本是个黑盒,后续的DataFrame&DataSet的API将手写的lambda暴露给优化器,实际上也是越来越像SQL的声明式,由优化器和执行器来承担执行的细节,用户只需要声明。

SQL Virtual Machine

除了编译到VM bytecode和machine code,还有相当一部分数据库选择自行实现一个VM,来更高效地执行查询。例如在SQLite中,如果对查询explain一下,会得到查询计划的字节码表示:

它的VM有自己的指令集,寄存器,大部分都是为数据库而定制。例如这里的Transaction指令,开始一个事务;Column指令,获取一行数据的某一列。

这样的VM其实可以类比编程语言中的VM,介于解释执行和编译执行之间的存在,具备解释执行的灵活性,又具备编译执行的效率。由于指令集专门为数据库设计,整体非常精简,在工程方面的开发成本相对较低,执行效率较高。

High-Level Language

更多的数据库做Codegen,则是简单地将SQL转换成高级语言,再调用编译器编译。这种方式虽然简单,但适用场景往往是OLAP的系统,能够接受较高的查询延迟。

例如SparkSQL中的CodeGenerator,得到Java代码之后,再用Janino进行编译:

  /**
   * Generates code for equal expression in Java.
   */
  def genEqual(dataType: DataType, c1: String, c2: String): String = dataType match {
    case BinaryType => s"java.util.Arrays.equals($c1, $c2)"
    case FloatType =>
      s"((java.lang.Float.isNaN($c1) && java.lang.Float.isNaN($c2)) || $c1 == $c2)"
    case DoubleType =>
      s"((java.lang.Double.isNaN($c1) && java.lang.Double.isNaN($c2)) || $c1 == $c2)"
    case dt: DataType if isPrimitiveType(dt) => s"$c1 == $c2"
    case dt: DataType if dt.isInstanceOf[AtomicType] => s"$c1.equals($c2)"
    case array: ArrayType => genComp(array, c1, c2) + " == 0"
    case struct: StructType => genComp(struct, c1, c2) + " == 0"
    case udt: UserDefinedType[_] => genEqual(udt.sqlType, c1, c2)
    case NullType => "false"
    case _ =>
      throw new IllegalArgumentException(
        "cannot generate equality code for un-comparable type: " + dataType.catalogString)
  }

Generative Programming

以上的Codegen技术,都不甚完美:

  • 对于字符串生成来说,虽然简单直观,但无法避免语法错误,也无类型安全可言,丧失了高级语言的优势
  • 对于直接生成IR或者AST来说,虽然语法的正确性不用考虑,但实现复杂,工程复杂度高,不利于维护

而在编程语言的领域,其实已经有了更好的方案,便是Generative Programming

在《How to Architect a Query Compiler, Revisited》一文中,用简单的例子展示了如何利用Generative Programming的技术,在几百行代码之内实现一个查询引擎。

def power(x: Int, n: Int): Int = if (n == 0) 1 else x * power(x, n - 1)
val power4 = x => power(x, 4) // partial evaluation
// val power4 = (x: Int) => x * x * x * x

在上面的例Scala语言的例子中,power是一个通用的计算乘方的函数,它接受两个参数;而power4是一个特化的函数,把参数n特化成4;这种形式称之为partial evaluation

对编译器有过基本了解的同学都知道,第二个实现通常要比第一个要快(取决于具体的编译器),因为在编译优化阶段,可以将这种递归函数调用内联成val power4 = (x: Int) => x * x * x * x。这里的原理在于,原本的两个变量其中一个变成了常量,基于这个已知信息可以对程序进行优化。

而这个思想同样可以被借鉴到解释器/Interpreter领域。所谓解释器,接受两个参数,一个是需要解释的程序源码,另一个则是被解释程序的输入。此时,如果我们将解释器和被解释程序源码交给一个partial evaluator,那么理论上它可以得到一个特化的解释器,即只能解释特定的程序,但也带来一个额外的好处:它可以比原本的通用解释器运行地更快。这里便是所谓的first Futamura Projection


对于数据库查询引擎来说原理也是一样,原本的查询引擎接受两个输入,SQL语句和数据;如果将查询引擎特化成只能执行某一个SQL查询,那么势必它可以做的更加高效。

不过这并不是一个trivial task,接下来便看一下如何实现。

首先依然是将传统的Volcano改造成Data-centric/Push模型,即由produce/consume来实现一个Operator的逻辑。除此之外,原本的produce/consume其实可以简化成一个函数,exec,它本身就接受一个回调。

为了实现Codegen,这里并没有选择字符串或者生成IR,而是通过LMS(Lightweight Modular Staging):

case class IntValue(value: Rep[Int]) extends Value { ... } // operations elided 

case class ColumnRec(fields: Seq[Value], schema: Seq[Field]) extends Record { 
    def apply(name: String) = fields(getFieldIndex(schema,name))
}

abstract class Buffer(schema: Seq[Field]) { 
    def apply(x: Rep[Int]): Record def update(x: Rep[Int], rec: Record): Unit
}

type Pred = Record => Rep[Boolean]
class Select(op: Op)(pred: Pred) extends Op { 
    def exec(cb: Record => Unit) = { 
        op.exec { tuple => if (pred(tuple)) cb(tuple) } 
    } 
}

注意到这里的代码看起来和普通的实现没有多少区别,但数据类型发生了变化:原本的一个IntValue,现在变成了用Rep[Int]表示,原本的谓词表达式type Pred = Record => Boolean变成了type Pred = Record => Rep[Boolean]。这里的Rep[T]便是LMS的Multi-Stage Programming的入口,简而言之,如果变量a是Int类型,那么表达式a + b的结果就是两个变量相加;而如果a和b是Rep[Int],那么执行表达式a + b则是生成执行这个加法的代码;更进一步,LMS也实现了对控制流的重载,if (c) a else b表达式中c的类型可以是Rep[Boolean],这个表达式会生成包含控制流的代码。那么最终,这里的每个Operator并不是在执行查询,而是生成了查询数据的代码!

做一个可能不太恰当的类比:

  • 普通的函数,会对结果进行求值:def +(x: Int, y: Int) = x + y
  • 生成Lambda的函数,结果需要再次求值:def +(x: Int, y: Int) = () => x + y

基于Generative Programming的技术,可以直接用高级语言实现类型安全的代码生成,同时又能够获得媲美LowLevel Codegen的性能。

Adaptive Compile

Codegen虽然好,但实际也并不是完美无缺的。例如,即便是直接生成IR,其编译时间往往也在毫秒级,而对于短的查询来说,可能执行时间也是在毫秒级,编译获得的收益被抵消了。

因此,相比于一味地进行Codegen,更好的方式显然是自适应地编译。例如采用简单的启发式策略,简单查询不做编译,复杂查询执行编译。《Adaptive Execution of Compiled Queries》做的更加细致:查询可以用两种方式执行,bytecode Interpreter和native code compilation,前者编译快但执行相对较慢,后者编译慢但执行快。问题的关键在于如何在两种执行方式之间无损切换?


这里采用了Morsel-Driven的方式,将一个Operator中的任务切分成多个,那么每个小任务执行之前,就可以判断采用解释执行还是编译执行了。为编译也是异步的动作,编译完成之后只是生成一个函数指针替换原来的解释执行,因此可以做到无缝切换。

总结

借用Hekaton中的一段话:『在内存数据库中,唯一提高吞吐的办法就是减少需要执行的指令。为了快10倍,需要减少90%的指令,为了快100倍,需要减少99%的指令』。因此,查询编译的目的其实就在于减少所需要执行的指令,让通用的代码变得专用,让编译器而不是人来优化代码。

虽然业界对查询编译已经进行了很多探索,从表达式编译、执行器编译,到流水线化,向量化,但跨领域的融合仍然迫切需要,『state of the art in query compiler construction is lagging behind that in the compilers field』。在上文的例子可以看到,数据库领域借鉴编译器技术的同时,也存在编译器技术对数据库技术的借鉴,在技术发展进入深水区的情况下,需要更多领域的融合,才能促使各自进一步发展。

此文旨在总结个人对查询编译的学习,如有疏漏,还请指教。

引用

  • [1] C. Freedman, E. Ismert, and P. Larson, “Compilation in the Microsoft SQL Server Hekaton Engine,” IEEE Data Eng. Bull., pp. 22–30, 2014.
  • [2] A. Kohn, V. Leis, and T. Neumann, “Adaptive Execution of Compiled Queries.”
  • [3] P. Boncz, M. Zukowski, and N. Nes, “MonetDB/X100: Hyper-Pipelining Query Execution,” Cidr, vol. 5, pp. 225–237, 2005.
  • [4] K. Krikellas, S. D. Viglas, and M. Cintra, “Generating code for holistic query evaluation,” ICDE, pp. 613–624, 2010.
  • [5] F. Nagel, G. Bierman, and S. D. Viglas, “Code generation for efficient query processing in managed runtimes,” Proc. VLDB Endow., vol. 7, no. 12, pp. 1095–1106, 2015.
  • [6] T. Neumann and V. Leis, “Compiling Database Queries into Machine Code,” IEEE Data Eng. Bull., pp. 3–11, 2014.
  • [7] S. D. Viglas, “Just-in-time compilation for SQL query processing.”
  • [8] J. Rao, H. Pirahesh, C. Mohan, and G. Lohman, “Compiled query execution engine using JVM,” in Proceedings - International Conference on Data Engineering, 2006, vol. 2006, p. 23.
  • [9] T. Rompf and N. Amin, “Functional Pearl : A SQL to C Compiler in 500 Lines of Code,” no. Section 4, pp. 2–9.
  • [10] R. Y. Tahboub, G. M. Essertel, and T. Rompf, “How to Architect a Query Compiler, Revisited,” 2018, pp. 307–322.
  • [11] W. Taha, “A gentle introduction to multi-stage programming, part II,” Lect. Notes Comput. Sci. (including Subser. Lect. Notes Artif. Intell. Lect. Notes Bioinformatics), vol. 5235 LNCS, pp. 260–290, 2008.
  • [12] A. Kemper, P. Boncz, T. Kersten, V. Leis, A. Pavlo, and T. Neumann, “Everything you always wanted to know about compiled and vectorized queries but were afraid to ask,” Proc. VLDB Endow., vol. 11, no. 13, pp. 2209–2222, 2018.
  • [13] C. Koch, “Abstraction Without Regret in Database Systems Building: a Manifesto,” IEEE Data Eng. Bull., vol. 37, no. 1, pp. 70–79, 2014.
  • [14] P. Menon, T. C. Mowry, and A. Pavlo, “Relaxed Operator Fusion for In-Memory Databases: Making Compilation, Vectorization, and Prefetching Work Together At Last,” VLDB, vol. 11, no. 1, pp. 1–13, 2017.
  • [15] A. Crotty et al., “An architecture for compiling UDF-centric workflows,” Proc. VLDB Endow., vol. 8, no. 12, pp. 1466–1477, 2015.
  • [16] T. Neumann, “Efficiently compiling efficient query plans for modern hardware,” Proc. VLDB Endow., vol. 4, no. 9, pp. 539–550, Jun. 2011.
  • [17] D. Butterstein and T. Grust, “Precision Performance Surgery for PostgreSQL LLVM – based Expression Compilation , Just in Time,” Vldb, vol. 9, no. 13, pp. 1517–1520, 2016.
  • [18] S. Wanderman-Milne and N. Li, “Runtime Code Generation in Cloudera Impala,” IEEE Data Eng. Bull., vol. 37, no. 1, pp. 31–37, 2014.
  • [19] Y. Klonatos, C. Koch, T. Rompf, and H. Chafi, “Building Efficient Query Engines in a High-Level Language.”
  • [20] A. Shaikhha, Y. Klonatos, L. Parreaux, L. Brown, M. Dashti, and C. Koch, “How to Architect a Query Compiler,” 2016.

编辑于 2019-04-01

文章被以下专栏收录