一、Calcite官方文档翻译--概览

目录

一、概览

1.1 背景

 

Apache Calcite是一个动态数据管理框架。它包含了许多组成典型数据管理系统的经典模块,但省略了一些关键功能: 数据存储,数据处理算法和元数据存储库。

Calcite有意地远离了存储和处理数据的任务。如我们所见,这使得它成为在应用程序和一个或多个数据存储位置和数据处理引擎之间的最佳中间层选择。

它同样也是构建数据库的完美基础选择: 在它的基础上,我们只需要添加数据。

下面为了展示说明,我们建立了一个空的Calcite实例并查询数据。

image2017-9-21 11:59:32.png

大家可能对上面的代码比较疑惑,数据库在哪里?这里没有数据库。在我们调用ReflectiveSchema.create注册一个java object作为schema,以及这个集合的成员emps和depts作为表之前,connection都是空的。

Calcite并不想管理数据,它甚至没有标准的数据格式。上面的例子使用了内存数据集,并且使用linq4j libaray的groupBy和join操作来对他们进行处理,但Calcite同样也支持以其他标准数据格式对数据进行处理,例如JDBC。在上面的例子中,将下面的代码

image2017-9-21 12:0:19.png

替换成

image2017-9-21 12:0:30.png

Calcite就可以通过JDBC来执行同样的查询了。对应用来说,数据和API不会产生任何变化,但底层的实现却差异巨大。

Calcite使用优化规则来将JOIN和GROUP BY操作下推到源数据库中进行执行。

基于内存和基于JDBC只是两个大家较为熟悉的例子。Calcite可以处理任意一种数据源和数据格式。

如果想要增加数据源,我们需要编写一个适配器来告诉Calcite,它应该将数据源中的什么样的集合视为“table”来进行操作。

如果想要进一步更智能地集成,我们可以编写自己的优化器规则。优化器规则允许Calcite来处理新格式的数据,并注册新的算子(如更优化的join算法),同时还允许Calcite来对查询转化为算子的过程进行优化。Calcite会结合用户提供的规则和算子和系统内建规则和算子,执行基于成本的优化,生成高效的执行计划

1.1.1 编写适配器Adapter

 

Calcite在example/csv子项目下提供了CSV的适配器。它能很好地支持应用程序的功能需求,同时如果你正在编写自己的适配器,它也能作为一个足够简单的例子来作为参考模板。

具体使用CSV 适配器和编写其他适配器的方法请查看下一章节1.2。

HOWTO章节提供了更多使用其他适配器的信息,和常用的使用场景。

1.1.2 功能状态

 

Calcite提供了以下特性:

  1. 查询解析器、验证器和优化器

  2. 以JSON格式读取模型

  3. 标准函数以及标准聚合函数

  4. 针对Linq4j和JDBC后端的JDBC查询

  5. Linq4j front-end

  6. SQL特性:SELECT, FROM (包括JOIN语法), WHERE, GROUP BY (包括GROUPING SETS), 聚合函数 (包括COUNT(DISTINCT...) 和FILTER),

             HAVING, ORDER BY(包括NULLS FIRST/LAST), 集合操作 (UNION, INTERSECT, MINUS), 子查询(包括相关子查询), 窗口聚合函数,

             LIMIT (Postgres语法); SQL reference章节中提供了更详细的信息

     7. 本地和远程JDBC驱动器,详情参考Avatica章节

     8. 多种适配器

1.2 教程

 

本章针对Calcite的连接建立提供了循序渐进的教程,使用一个简单的适配器来将一个CSV文件目录以包含Schema信息的tables形式呈现,并提供了一个完全SQL接口。

Calcite-example-CSV是一个Calcite中的一个功能完备的适配器,它可以读取CSV格式的文本文件。值得称赞的是,几百行的java代码就足够提供完全的SQL查询功能。

CSV适配器同样作为一个其他数据格式的适配器构建参考模板。尽管代码量不大,但它覆盖了一些重要的概念:

  1. 用户通过使用SchemaFactory和Schema interfaces来自定义schema

  2. 以JSON模型文件声明schemas

  3. 以JSON模型文件声明视图views

  4. 通过Table interface自定义table

  5. 定义table的record类型

  6. 使用Scannable Table interface作为Table的简单实现,来直接枚举所有的rows

  7. 进阶实现FilterableTable,来根据简单的谓词predicates过滤rows

  8. 以Translatable Table进阶实现Table,将关系型算子翻译为执行计划规则

1.2.1 源码搭建

 

版本依赖:Java (1.7 or higher; 1.8 preferred), git and maven (3.2.1 or later).

image2017-9-21 14:17:0.png

1.2.2 查询测试

我们可以通过工程内置的sqlline脚本来连接到Calcite

$ ./sqlline 
sqlline> !connect jdbc:calcite:model=target/test-classes/model.json admin admin

执行一个metadata 查询

image2017-9-21 12:3:38.png

 

JDBC提示:sqlline中的 !tables 命令实际上等于 DatabaseMetaData.getTables() , 还有其他命令来查询JDBC metadata, 例如 !columns 和 !describe

如结果所示,该系统中共有5个table: SALES schema下的EMPS, DEPTS, HOBBIES 和系统自带的 metadata schema下的COLUMNS和TABLES.

系统table在Calcite中会一直展示,但其他表是由schema的指定实现而来,在本例中,EMPS和DEPTS表来源于target/test-classes路径下的EMPS.csv和DEPTS.csv。

通过在这些表上执行一些查询,我们可以验证Calcite提供了完整的SQL功能实现。

首先,table可以scan

image2017-9-21 12:4:0.png

同时支持JOIN和GROUP BY

image2017-9-21 12:4:24.png

最后,VALUES操作符可以聚合生成单独一行数据,我们可以通过这种简便的方法来测试表达式和SQL内嵌函数:

image2017-9-21 12:4:41.png

Calcite具有其他许多SQL特性。我们来不及在这里一一举例,使用者可以编写更多的查询来进行验证。

1.2.3 Schema发现

 

现在,我们来探索一下Calcite是如何发现这些table的。记住,最核心的Calcite不知道CSV文件的任何信息。(作为一个“没有存储层的databse”,Calcite不回去了解任何文件格式)Calcite能识别这些table事因为我们告诉它去运行calcite-example-csv工程下的代码。

运行连接中有一系列的步骤。首先,我们在一个schema 工程类中以model file的格式定义一个schema。然后schema工厂类创建一个schema,schema创建多个table,这些table都知道如何通过scan CSV文件来获取数据。最后,在Calcite解析完查询并将查询计划映射到这几个table上时,Calcite会在查询执行时触发这些table去读取数据。接下来我们更深入地解析其中的细节步骤。

在JDBC连接字符串中,我们会给出以JSON格式定义的model的路径。model具体定义如下

image2017-9-21 12:5:9.png

这个model定义了一个名为SALES的schema。这个schema是由一个plugin类支持的,org.apache.calcite.adapter.csv.CsvSchemaFactory,这个类是calcite-example-csv工程里的一部分并且实现了Calcite中的SchemaFactory接口。它的create方法将一个schema实例化了,将model file中的directory参数传递过去了。

image2017-9-21 14:1:37.png

根据model的配置,这个schema 工程类实例化了一个名为SALES的schema。这个schema是org.apache.calcite.adapter.csv.CsvSchema的一个实例,实现了Calcite中的Schema接口。

一个schema的职责是产生一系列的tables(它统一可以列举出子schema和table-function,但这些进阶的特性在calcite-example-csv中没有支持)。

这些table实现了Calcite的Table接口。

CsvSchema生成了CsvTable和它子类实例的一些tables。

下面是CsvSchema的一些相关代码,对基类AbstractSchema中的getTableMap()方法进行了重载

image2017-9-21 14:2:17.png

image2017-9-21 14:2:29.png

schema会扫描指定路径,找到所有以".csv/”结尾的文件。在本例中,指定路径是 target/test-classes/sales,路径中包含文件EMPS.csv和DEPTS.csv,这两个文件会转换成tables EMPS和DEPTS.

1.2.4 Schema中的Tables和Views

 

注意,我们不需要在model中定义任何tables;schema会自动生成这些tables。

但我们可以使用已经存在的schema中的table的属性在自动生成的table之外定义额外的自定义table。

接下来我们可以看一下如何创建一种重要且有用的table,即一个视图。

在写一个查询时,视图就相当于一个table,但它不存储数据。它通过执行查询来生成加过。在查询转换为执行计划时,视图会被展开,所以查询执行器可以执行一些优化策略,例如移除一些SELECT子句中存在但在最终结果中没有用到的表达式。

下面是定义了一个视图的schema:

image2017-9-21 14:3:41.png

上面的 type:“view”这一行将FEMALE_EMPS定义为一个视图,而不是常规表或者是自定义表。注意JSON中定义单引号需要加上转义字段\

用JSON来定义长字符串易用性不太高,因此Calcite支持了一种替代语法。如果你的视图定义中有长SQL语句,可以使用多行来定义一个长字符串。

image2017-9-21 14:4:3.png

在定义完一个视图之后,在查询时可以完全将它作为一个table使用

image2017-9-21 14:4:42.png

1.2.5 自定义Tables

 

自定义表是由用户定义的代码来实现定义的,不需要额外自定义schema。

具体例子请参考  model-with-custom-table.json

image2017-9-21 14:5:9.png

我们可以通过通用方式来对自定义表进行查询

image2017-9-21 14:5:24.png

上面的schema是通用格式,包含了一个由org.apache.calcite.adapter.csv.CsvTableFactory驱动的自定义表,这个类实现了Calcite中的TableFactory接口。它创建了一个CsvScannableTable实例方法,将model文件中的file参数传递过去

image2017-9-21 14:5:40.png

实现自定义table通常是一个比实现自定义schema更容易的替代选择。这两种方法最终都会创建类似的Table接口类的实现,但自定义表无需实现metadata discovery。(CsvTableFactory创建一个CsvScannableTable,就像CsvSchema一样,但是table实现无需扫描整个文件系统来找到.csv类型的文件)。

自定义table要求开发者在model上执行更多操作(开发者需要在model文件中显式指定每一个table和它对应的文件),同时也提供给了开发者更多的控制选项(例如,为每一个table提供不同参数)。

1.2.6 模型注释

model定义过程中可以通过/*…*/或者//符号来添加注释

image2017-9-21 14:6:14.png

Comments不是标准JSON格式,但不会造成影响。

1.2.7 使用执行计划规则优化查询

 

目前我们看到的table实现和查询都没有问题,因为table中不会包含大数据量。但如果你的自定义table有,例如,一百列,100w行,你会希望用户在每次查询过程中不要检索全量数据。你会希望Calcite通过适配器来进行衡量,并找到一个更有效的方法来访问数据。

衡量过程是一个简单的查询优化格式。Calcite支持通过添加执行器规则来实现查询优化。执行器规则在查询解析树中匹配到对应规则时生效(例如在某个项目中匹配到某种类型的table是生效),而且执行器规则是可扩展的,例如schemas和tables。因此,如果用户希望通过SQL访问某个数据集,首先需要定义一个自定义表或是schema,然后再去定义一些能使数据访问高效的规则。

为了查看效果,我们可以使用一个执行器规则来访问一个CSV文件中的某些子列集合。我们可以在两个相似的schema中执行同样的查询:

image2017-9-21 14:7:4.png

两次查询的scan方式不同,EnumerableTableScan和CsvTableScan

是什么导致了执行计划的差异?让我们来追查一下其中证据。在smart.json model 文件中,存在额外的一行:

image2017-9-21 14:7:19.png

这回让CsvSchema携带参数参数falvor = TRANSLATABLE 参数进行创建,并且它的createTable方法会创建CsvTranslatableTable,而不是CsvScannableTable.

CsvTranslatableTable实现了TranslatableTable.toRel()方法来执行CsvTableScan. Table scan操作是查询执行树中的叶子节点,默认实现方式是EnumerableTableScan,但我们构造了一种不同的的子类型来让规则生效。

下面是一个完整的规则:

image2017-9-21 14:7:35.png

构造函数声明了能使规则生效的关系表达式匹配模式。

onMatch方法生成了一个新的关系表达式,调用了RelOptRuleCall.transformTo()来表示规则已经触发成功了。

1.2.8 查询优化流程

 

关于Calcite的查询计划有多智能有许多种方法,但我们在这里不会讨论这个问题。最智能的部分是为了减轻用户负担的优化器规则设计者。

首先,Calcite不会按照规定的数据来执行瑞泽。查询优化处理过程是一个有很多分支的分支树,就像国际象棋一样会检查很多可能的子操作。如果规则A和B同时满足查询操作树的一个给定子集合,Calcite可以将它们同时执行。

其次,Calcite在执行计划树的时候会使用基于代价的优化,但基于成本的模型并不会导致规则的执行,这在短期内看起来代价会更大。

许多优化规则都有一个线性优化方案。在面对上面说的规则A和规则B的情况下,这样的优化器需要立刻进行抉择。可能会有一个策略,比如“在整棵树上先执行规则A,然后在整棵树上执行规则B”,或是执行基于代价的优化策略,执行能产生耗费更低的结果的规则。

Calcite不需要这样的妥协(哪样???)。这能让结合各种规则的操作更简单。如果你希望结合规则来识别各种携带规则的物化视图,去从CSV和JDBC源数据系统中读取数据,你需要去给Calcite所有的规则并告诉它如何去做。

Calcite使用了一个基于成本的优化模型,成本模型决定了最终使用哪个执行计划,有时候为了避免搜索空间的爆炸性增长会对搜索树进行剪枝,但它绝不对强迫用户在规则A和规则B之间进行选择。这是很重要的一点,因为它避免了在搜索空间中落入实际上不是最优的局部最优值。

同样,成本模型是可扩展的,它是基于表和查询操作的统计信息。这个问题稍后会仔细讨论。

1.2.9 JDBC适配器

 

DBC适配器将JDBC数据源中的schema映射成了Calcite的schema模式。

例如,下面这个schema是从一个“foodmart” MySQL数据库中读取出来的。

image2017-9-21 14:9:23.png

当前的限制:JDBC适配器目前仅支持下推table scan操作;其他的的操作(filtering,joins,aggregations等等)在Calcite中完成。我们的目的是将尽可能多的处理操作、语法转换、数据类型和内建函数下推到源数据系统。如果一个Calcite查询来源于单独一个JDBC数据库中的表,从原则上来说整个查询都会下推到源数据系统中。如果表来源于多个JDBC数据源,或是一个JDBC和非JDBC的混合源,Calcite会使用尽可能高效的分布式查询方法来完成本次查询。

1.2.10 克隆JDBC适配器

克隆JDBC适配器创造了一个混合源数据系统。数据来源于JDBC数据库但在它第一次读取时会读取到内存表中。Calcite基于内存表对查询进行评估,有效地实现了数据库的缓存。

例如,下面的model从一个“foodmart” MySQL database中读取表:

image2017-9-21 14:9:55.png

另外一种技术是从当前已存在的schema中构建一份clone schema。通过source属性来引用之前已经在model中定义过的schema,如下:

image2017-9-21 14:11:27.png

你可以使用这种方法建立任意类型schema的clone schema,不仅限于JDBC.

cloning adapter不是最重要的。我们计划开发更复杂的缓存策略,和更复杂更有效的内存表的实现,但目前cloning JDBC adapter体现了这种可能性,并让我们能开始尝试初始实现。

1.2.11 待完善

......

1.3 关系代数

 

关系代数是Calcite的核心。每一个查询都可以使用一棵关系操作符的树型结构来表示。你可以将SQL翻译为一个关系代数,或是直接构建树形结构。

执行计划规则通过使用保留语义的???数学恒等式来翻译表达式树。例如,如果filter没有引用其他输入的列,那么就可以将这个filter下推到inner join的子查询输入中。

Calcite通过反复讲优化规则应用到关系表达式中来进行查询优化。基于代价的模型会指引这一过程,并且执行计划引擎会生成代码更优的,和原始语义具有同样语义的表达式来提供选择。

执行计划处理过程是可扩展的,你可以添加自己的关系操作符、执行计划规则、代码模型和统计数据。

1.3.1 关系代数构建起

 

https://calcite.apache.org/apidocs/org/apache/calcite/tools/RelBuilder.html

构建关系型表达式最简单的实现规则是使用Algebra builder,RelBuilder。下面是一个具体案例构建流程

1.3.1.1 TableScan

image2017-9-21 14:12:54.png

 

上面的代码输出结果是

LogicalTableScan(table=[[scott, EMP]])

它创建了EMP table的scan过程,和下面的SQL等效

image2017-9-21 14:13:17.png

1.3.1.2 增加投影

现在,让我们来添加一个等效于下面SQL的投影

image2017-9-21 14:13:28.png

我们只需要在build方法之前增加一个对于project方法的调用

image2017-9-21 14:15:57.png

输出如下:

image2017-9-21 14:13:53.png

 

两次对builder.field的调用创建了从关系表达式输入中返回field的简单表达式,TableScan是由scan调用创建的。

Calcite可以将$7 和$1转换为field的顺序引用。

1.3.1.3 增加Filter和Aggregate

一个带有Aggregte和Filter的查询如下

image2017-9-21 14:14:13.png

相当于下面的SQL

image2017-9-21 14:16:19.png

生成的执行计划结果如下

image2017-9-21 14:14:41.png

1.3.1.4 Push和Pop

 

builder使用一个堆栈来存储每一个步骤产生的关系表达式并且将它作为下一步的输入传递过去。这样的方案允许我们通过使用产生关系表达式的方法去生成构建器。

大多数时间,你只会用到唯一一个堆栈方法build(),来获取最终的表达式,即树的根节点。

有时,堆栈嵌套得如此之深,让人感到困惑。为了保持它的可理解性,你可以从堆栈中移除一些表达式。例如,下面我们构建的bushy join

image2017-9-21 14:18:4.png

我们会分三个stage来进行构建。将中间结果以left和right变量进行存储,并在创建最终的join时使用push()方法来将中间结果放回stack中

image2017-9-21 14:18:21.png

1.3.1.5 Field名称和序数

 

用户可以通过名称或是序数来对一个field进行引用。

序数是从0开始的,每一个操作符在进行输出field时会自动生成序数。例如,Project会返回每一个标量表达式生成的field。

每一个操作符产生的field名称会保证是唯一的,但这就意味着有时候名称并不是你想要的,比如,在对两个表EMP和DEPT进行join时,其中一个输出字段会被命名为DEPTNO,另外一个field会被命名为DEPTNO_1.

一些关系表达式方法可以给你提供关于field names的更多控制权限:

 · project 允许你通过 alias(expr, fieldName)来包装表达式。它会移除包装器,但会留下包装器中建议的名称(只要它是唯一的)。

· values(String[] fieldNames, Object … values) 接收一组field name,如果数组中任意一个元素是空的,builder会自动生成一个全局唯一的名字。

如果一个表达式对一个输入field进行投影,或是转换,它会直接使用输入field的名称。

一旦唯一的field名称被确定了,这个名称就是不可变的。如果你有一个特殊的RelNode实例  ??? ,你可以依赖于不变的field name。实际上,整个关系表达式都是不可变的。

但如果一个关系表达式上已经应用了多个重写规则,结果表达式中的field name可能不会和原始表达式保持一致。从这一点来看,最好是通过序数来引用fields。

当你在构建一个接受了多个输入的关系表达式时,需要在构建field引用时将field 序数和name唯一性重命名考虑进来。通常在构建join conditions时需要考虑这样的场景。

假设你现在正在EMP上构建一个join关系,涉及到8个field [EMPNO, ENAME, JOB, MGR, HIREDATE, SAL, COMM, DEPTNO] 以及DEPT表中的三个field [DEPTNO, DNAME, LOC]。在Calcite内部,它将这11个field看做一行结合后的输入,通过偏移量来唯一确认: 左表第一个field是field #0(序数从0开始),右表的第一个field是field #8。

但通过builder api,你可以指定确定input中的确定field。为了引用field “SAL”, 即内部的field #5,可以这样进行引用builder.field(2, 0, "SAL"), builder.field(2, "EMP", "SAL"), or builder.field(2, 0, 5)。这意味着“两个input中的input#0中的field#5”(为什么它需要知道有两个input?因为输入都是在堆栈中存储;input #1是堆栈的根节点,input#0是子节点,如果我们不告诉builder有两个input,它将不知道获取input #0需要遍历到第几层)。

同样,为了引用“DNAME”,内部field #9(8+1),应该这样引用builder.field(2, 1, "DNAME"), builder.field(2, "DEPT", "DNAME"), or builder.field(2, 1, 1).

1.3.1.6 API总结

1.3.1.6.1 Relational operator

image2017-9-21 14:22:57.png

image2017-9-21 14:27:26.png

参数类型

image2017-9-21 14:26:54.png

 

builder方法会执行各种各样的优化策略,包括

  1. 当要求投影project所有 的列时,project直接返回输入

  2. filter会对condition进行打平(所以一个 and 和 or操作会有多余两个子节点)?和优化(比如将x=1 and true转换为x=1)

  3. 如果先sort再limit,结果等价于直接调用sortLimit

有一些注解方法可以支持在stack顶层的关系表达式中添加信息

image2017-9-21 14:27:5.png

1.3.1.6.2 Stack methods

image2017-9-21 14:26:36.png

1.3.1.6.3 标量表达式

 

下列方法会返回标量表达式(RexNode)

他们中的许多成员会使用stack中的内容。比如,field(“DEPTNO”)返回了一个指向stack中的关系表达式DEPTNO的引用。

image2017-9-21 14:26:8.png

image2017-9-21 14:25:59.png

image2017-9-21 14:25:47.png

image2017-9-21 14:25:41.png

1.3.1.6.3 Group key methods

下列方法返回一个RelBuilder.GroupKey

image2017-9-21 14:25:26.png

1.3.1.6.4 Aggregate call methods

下列方法返回一个RelBuilder.AggCall

image2017-9-21 14:25:20.png

 

 

二、进阶

三、Avatica

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值