第二部分 进阶(Advanced)

1. 适配器(Adapters)

1.1 Schema adapters

  一个schema adapter允许Calcite去读取特定类型的数据,将这些数据以一种table格式schema展示出来。
• Cassandra adapter (calcite-cassandra)
• CSV adapter (example/csv)
• Druid adapter (calcite-druid)
• Elasticsearch adapter(calcite-elasticsearch2 and calcite-elasticsearch5)
• File adapter (calcite-file)
• JDBC adapter (part of calcite-core)
• MongoDB adapter (calcite-mongodb)
• OS adapter (calcite-os)
• Pig adapter (calcite-pig)
• Solr cloud adapter (solr-sql)
• Spark adapter (calcite-spark)
• Splunk adapter (calcite-splunk)
• Eclipse Memory Analyzer (MAT) adapter (mat-calcite-plugin)

1.3 其他语言接口

• Piglet (calcite-piglet) runs queries in a subset of Pig Latin

1.4 引擎

  许多项目和产品使用Apache Calcite进行SQL解析,查询优化,数据虚拟化/联邦以及物化视图重写。其中一些在“powered by Calcite”页面。

1.5 驱动

  驱动程序允许您从应用程序连接到Calcite。
JDBC Driver(Java Doc)
  JDBC驱动程序由Avatica提供 支持。连接可以是本地或远程的(HTTP上的JSON或HTTP上的Protobuf)。

JDBC连接字符串参数

属性 描述
approximateDecimal 是否可以接受DECIMAL类型上的聚合函数的近似结果
approximateDistinctCount 是否可以接受聚合函数COUNT(DISTINCT …)的近似结果
approximateTopN 是否可以接受”TopN”查询(ORDER BY aggFun() DESC LIMIT n)的近似结果
caseSensitive 标识符是否区分大小写。如果未指定,则使用来自于lex的值。
conformance SQL一致性级别。值:DEFAULT(默认,类似于PRAGMATIC_2003),LENIENT,MYSQL_5,ORACLE_10,ORACLE_12,PRAGMATIC_99,PRAGMATIC_2003,STRICT_92,STRICT_99,STRICT_2003,SQL_SERVER_2008。
createMaterializations Calcite是否创建物化的东西。默认false
defaultNullCollation 如果在查询中既不指定NULLS FIRST也不指定NULLS LAST,应如何对NULL值进行排序。缺省值为HIGH,与Oracle相同,对NULL值进行排序。
druidFetch 执行一次SELECT查询时,Druid适配器应该读取多少行。
forceDecorrelate 计划器是否应该尽可能地相互关联。默认为true。
fun 内置函数和运算符的集合。有效值是“标准”(默认),“oracle”,“空间”,并可以使用逗号连接组合,例如“oracle,spatial”。
lex 词汇(关键字)策略。值是ORACLE(默认),MYSQL,MYSQL_ANSI,SQL_SERVER,JAVA。
materializationsEnabled Calcite是否使用物化的东西。默认false
model JSON模型文件的URI
parserFactory 解析器工厂。实现SqlParserImplFactory 接口的类的名称,并且有一个公有的默认构造方法或者INSTANCE常量
quoting 如何引用标识符。值是DOUBLE_QUOTE,BACK_QUOTE,BRACKET。如果未指定,则使用来自于lex的值。
quotedCasing 如何存储使用了引号的标识符。值是UNCHANGED, TO_UPPER, TO_LOWER。如果未指定,则使用来自于lex的值。
schema 初始化schema名称
schemaFactory Schema工厂。实现SchemaFactory接口的类的名称,并且有一个公有的默认构造方法或者INSTANCE常量。如果指定了model,此属性会被忽略。
schemaType Schema类型。值必须是“MAP”(默认值),“JDBC”或“CUSTOM”。(如果schemaFactory指定则隐式指定为CUSTOM类型)如果指定了model,此属性会被忽略。
spark 指定是否应将Spark用作无法推送到源系统的处理引擎。如果为false(默认值),则Calcite将生成实现了Enumerable接口的代码。
timeZone 时区,例如“gmt-3”。默认是JVM的时区。
typeSystem 类型系统。实现了接口RelDataTypeSystem的类的名称,并具有公有的默认构造函数或INSTANCE常量。
unquotedCasing 如果标识符未被引用,如何存储。有效值是UNCHANGED,TO_UPPER,TO_LOWER。如果未指定,则使用来自于lex的值。

  如果仅仅连接到一个基于内置shema类型的模型,不需要指定model。例如:

jdbc:calcite:schemaType=JDBC; schema.jdbcUser=SCOTT; schema.jdbcPassword=TIGER; schema.jdbcUrl=jdbc:hsqldb:res:foodmart

  通过JDBC Schema适配器创建一个映射到foodmart数据库的连接。
  相似的,可以连接到基于用户定义的schema适配器的单一schema。例如:

jdbc:calcite:schemaFactory=org.apache.calcite.adapter.cassandra.CassandraSchemaFactory; schema.host=localhost; schema.keyspace=twissandra

  创建一个连接到Cassandra适配器连接,与如下的模型文件等效:

{
  "version": "1.0",
  "defaultSchema": "foodmart",
  "schemas": [
    {
      type: 'custom',
      name: 'twissandra',
      factory: 'org.apache.calcite.adapter.cassandra.CassandraSchemaFactory',
      operand: {
        host: 'localhost',
        keyspace: 'twissandra'
      }
    }
  ]
}

注意:
  operand部分的每个key,在连接字符串都以shema.为前缀。

1.6 服务器

  Calcite的核心模块(calcite-core)支持SQL查询(SELECT)和DML操作(INSERT,UPDATE,DELETE,MERGE),但不支持DDL操作,如CREATE SCHEMA或CREATE TABLE。正如我们将要看到的,DDL使存储库的状态模型变得复杂,并且使得解析器更难以扩展,因此我们将DDL留在了核心之外。
  服务器模块(calcite-server)向Calcite添加DDL支持。它扩展了SQL解析器,使用与子项目相同的机制,添加了一些DDL命令:

  • CREATE 和 DROP SCHEMA
  • CREATE 和 DROP FOREIGN SCHEMA
  • CREATE和DROP TABLE(含CREATE TABLE ... AS SELECT)
  • CREATE 和 DROP MATERIALIZED VIEW
  • CREATE 和 DROP VIEW
    命令在SQL引用一章中详细描述。
      启用此功能,需要将calcite-server.jar添加到classpath,并且将parserFactory=org.apache.calcite.sql.parser.ddl.SqlDdlParserImpl#FACTORY添加到JDBC的连接字符串,如下是使用sqlline shell的简单示例:
    $ ./sqlline
    sqlline version 1.3.0
    > !connect jdbc:calcite:parserFactory=org.apache.calcite.sql.parser.ddl.SqlDdlParserImpl#FACTORY sa ""
    > CREATE TABLE t (i INTEGER, j VARCHAR(10));
    No rows affected (0.293 seconds)
    > INSERT INTO t VALUES (1, 'a'), (2, 'bc');
    2 rows affected (0.873 seconds)
    > CREATE VIEW v AS SELECT * FROM t WHERE i > 1;
    No rows affected (0.072 seconds)
    > SELECT count(*) FROM v;
    +---------------------+
    |       EXPR$0        |
    +---------------------+
    | 1                   |
    +---------------------+
    1 row selected (0.148 seconds)
    > !quit

    Linux系统测试通过,Windows尚不支持。
    Apache Calcite官方文档中文版- 进阶-1. 适配器_第1张图片
      calcite-server模块是可选的。其目标之一是使用简洁的示例来展示Calcite的功能(例如物化视图,外部表和生成列),可以从SQL命令行尝试这些示例。所有calcite-server使用的功能都可以通过calcite-core中的API。
      如果您是子项目的作者,那么您的语法扩展名不太可能与calcite-server匹配,所以我们建议您通过扩展核心解析器来添加SQL语法扩展; 如果您需要DDL命令,则可以将calcite-server 复制粘贴到您的项目中。
      目前,存储库不会被持久化。在执行DDL命令时,通过从根Schema添加和删除可访问的对象来修改内存中的存储库 。同一SQL会话中的所有命令都将看到这些对象。您可以通过执行相同的SQL命令脚本,在将来的会话中创建相同的对象。
      Calcite也可以作为数据虚拟化或联邦服务器:Calcite管理多个外部Schema中的数据,但是对于客户端来说数据似乎都在同一个地方。Calcite选择应该在哪里处理,以及是否为了提高效率创建数据副本。calcite-server模块是实现这一目标的一个步骤; 一个行业垂直的解决方案将需要进一步的封装扩展(使Calcite作为服务可运行),存储库持久性,授权和安全性。

1.7 扩展性

  还有许多其他的API可以扩展Calcite的功能。
  在本节中,我们将简要介绍这些API,为您提供一个对可以实现功能的思路。要充分使用这些API,您需要阅读其他文档,例如接口的javadoc,并且可能需要找我们出为它们编写的测试。

1.8 函数和操作符

  有几种方法可以将运算符或函数添加到Calcite中。我们将首先描述最简单(也是最不强大的)。
  用户自定义的函数是最简单的(但功能最少)。它们很容易编写(你只需编写一个Java类并将其注册到Schema中),但是在参数的数量和类型,解析重载函数或派生返回类型方面不提供很大的灵活性。
  如果需要这种灵活性,可能需要写一个用户自定义的操作符 (参见SqlOperator接口)。
如果操作符不遵守标准的SQL函数语法“ f(arg1, arg2, ...)”,那么需要扩展解析器。
在测试中有很多很好的例子:UdfTest类测试用户自定义的函数和用户自定义的聚合函数。
  聚合函数
  用户自定义的聚合函数与用户自定义的函数相似,但每个函数都有几个对应的Java方法,一个方法用于聚合生命周期中的每一个阶段:

  • init 创建一个累加器;
  • add 将一行的值添加到累加器;
  • merge 将两个累加器组合成一个;
  • result 完成累加器并将其转换为结果。
      例如,用于SUM(int)函数的方法如下所示(伪代码):
    struct Accumulator {
    final int sum;
    }
    Accumulator init() {
    return new Accumulator(0);
    }
    Accumulator add(Accumulator a, int x) {
    return new Accumulator(a.sum + x);
    }
    Accumulator merge(Accumulator a, Accumulator a2) {
    return new Accumulator(a.sum + a2.sum);
    }
    int result(Accumulator a) {
    return new Accumulator(a.sum + x);
    }

    下面是计算列值为4和7的两行总和的顺序调用序列:

    a = init()    # a = {0}
    a = add(a, 4) # a = {4}
    a = add(a, 7) # a = {11}
    return result(a) # returns 11

    窗口函数
      窗口函数与聚合函数类似,但是它应用于由OVER子句而不是由GROUP BY子句收集的一组行。每个聚合函数都可以用作窗口函数,但是有一些关键的区别。窗口函数看到的行可能是有序的,依赖于顺序的窗口函数(RANK例如)不能用作聚合函数。
      另一个区别是,窗口是不相交的( non-disjoint):一个特定的行可以出现在多个窗口中。例如,10:37出现在9:00-10:00以及9:15-9:45两个窗口。
      窗口函数是递增计算的:当时钟从10:14转动到10:15时,可能有两行进入窗口,三行离开。为此,窗口函数有一个额外的生命周期操作:

  • remove 从累加器中删除一个值。
      SUM(int) 的伪代码将是:
    Accumulator remove(Accumulator a, int x) {
        return new Accumulator(a.sum - x);
    }

      下面是计算移动总和的顺序调用序列,在前两行中,4行中的值分别为4,7,2和3:

    a = init()       # a = {0}
    a = add(a, 4)    # a = {4}
    emit result(a)   # emits 4
    a = add(a, 7)    # a = {11}
    emit result(a)   # emits 11
    a = remove(a, 4) # a = {7}
    a = add(a, 2)    # a = {9}
    emit result(a)   # emits 9
    a = remove(a, 7) # a = {2}
    a = add(a, 3)    # a = {5}
    emit result(a)   # emits 5

    分组窗口函数
      分组窗口函数是操作GROUP BY子句将记录集合在一起的函数。内置的分组窗口函数有HOP,TUMBLE和SESSION。可以通过实现接口SqlGroupedWindowFunction来定义其他函数 。

表函数和table macros
  用户自定义的表函数 的定义方式与常规的用户自定义“标量”函数类似,但它是在查询的FROM子句中使用。以下查询使用一个名为Ramp的表函数:

SELECT * FROM TABLE(Ramp(3, 4))

  用户定义的table macros使用与表函数相同的SQL语法,但定义不同。它们不会生成数据,而是生成一个关系表达式。在查询准备过程中调用table macros,然后可以优化它们生成的关系表达式。(Calcite使用table macros实现视图)。
  TableFunctionTest类测试表函数并包含几个有用的例子。

1.9 扩展解析器

  假设你需要扩展Calcite的SQL语法,这样才能兼容未来语法的变化。在项目中复制Parser.jj语法文件的副本是愚蠢的,因为语法是经常编辑的。
  幸运的是,Parser.jj实际上是一个 Apache FreeMarker 模板,其中包含可以被替换的变量。calcite-core中的解析器使用变量的默认值实例化模板,通常为空,但可以覆盖。如果项目使用不同的解析器,可以提供自己的config.fmpp和parserImpls.ftl文件,并因此产生扩展的解析器。
  calcite-server模块是在[ CALCITE-707 ]中创建的,并添加了DDL语句如CREATE TABLE,这是可以参考遵循的一个示例。另外可参照类ExtensionSqlParserTest。

1.10 自定义SQL方言接受和生成

  要定制解析器应该接受的SQL扩展,请实现 SqlConformance接口或使用枚举SqlConformanceEnum中的某个内置值 。
  要控制如何为外部数据库(通常通过JDBC适配器)生成SQL,请使用SqlDialect类。方言也描述了引擎的能力,比如是否支持OFFSET和使用FETCH。

1.11 自定义Schema

  要自定义Schema,需要实现SchemaFactory接口。
  在查询准备期间,Calcite会调用这个接口来找出你自定义的Schema包含的表和子Schema。在查询中引用你自定义schema中的表时,Calcite将要求你的schema创建Table接口的实例 。
  该表将被包装在 TableScan中 ,并将进行查询优化过程。

1.12 Reflective schema

  反射(Reflective)schema(ReflectiveSchema类)是一种封装Java对象的方式,以便它作为Schema出现。其拥有集合值的字段将显示为表格。
  它不是一个Schema工厂,而是一个真正的模式; 必须通过调用API来创建对象并将其包装在Schema中。
  请参阅ReflectiveSchemaTest类。

1.13 自定义表格

  要自定义一个表格,你需要实现TableFactory接口。尽管Schema工厂生产一组命名表格,但是当绑定到具有特定名称的Schema(以及可选的一组额外操作数operands)时,表格工厂会生成单个表格。
修改数据
  如果你的自定义表格支持DML操作(INSERT,UPDATE,DELETE,MERGE),实现Table接口的同时也必须实现ModifiableTable接口。

  如果你的自定义表格支持流式查询,实现Table接口的同时必须实现 StreamableTable接口。
  参见 StreamTest 类的例子。
操作下推到自定义表格
  如果您希望将处理推到自定义表格的源系统上,请考虑实现 FilterableTable 接口或 ProjectableFilterableTable接口。
  如果想要更多的控制,应该写一个计划器的规则。这将允许您下推表达式,根据成本决定是否下推处理,是否下推更复杂的操作,如关联,聚合和排序。

1.14 类型系统

  可以通过实现RelDataTypeSystem接口来自定义类型系统的某些方面。
关系运算符
  所有关系运算符都实现了RelNode接口, 并且大部分继承自AbstractRelNode类。TableScan, TableModify, Values, ProjectFilter, Aggregate, Join, Sort, Union, Intersect, Minus, Window和 Match等核心运算符(由 SqlToRelConverter使用 ,覆盖了常规传统的关系代数) 。
  上述每一个都有一个“纯”的逻辑子类,LogicalProject 等等。任何给定的适配器都会有引擎可以有效执行操作的对应部分; 例如,Cassandra适配器有 CassandraProject ,但是没有CassandraJoin。
  可以定义你自己的RelNode子类来添加一个新的操作符,或者在一个特定的引擎中实现一个现有的运算符。
  为了使运算符有用且功能强大,您需要计划器规则将其与现有运算符结合使用。(并提供元数据,见下文)。这是代数,效果是组合的:你写了一些规则,但它们结合起来可以处理指数数量的查询模式。
  如果可能的话,让您的运算符成为现有运算符的子类; 那么可以重用或适应其规则。更好的是,如果你的运算符是一个逻辑操作,你可以用现有的运算符来重写(通过计划器规则),尽量应该这样做。您将能够重复使用这些运算符的规则,元数据和实现,而无需额外的工作。
计划器规则
  计划器规则(RelOptRule类)可以将关系表达式转换为等效的关系表达式。
  计划器引擎有许多计划器规则注册并触发它们将输入查询转换为更高效的计划。因此,计划器规则是优化过程的核心,但令人惊讶的是每个计划器规则并不关心成本。计划器引擎负责按顺序发射规则,产生最优计划,但是每个规则只关心它的正确性。
  Calcite有两个内置的计划器引擎: VolcanoPlanner 类使用动态编程,对穷举搜索很有效,而 HepPlanner类则以更加固定的顺序触发一系列规则。
调用约定
  调用约定是特定数据引擎使用的协议。例如,Cassandra 引擎具有关系运算符的集合, CassandraProject,CassandraFilter等等,并且这些操作符可以被相互连接,而无需从一个格式转换成另一种格式的数据。
  如果需要将数据从一个调用约定转换为另一个调用约定,则Calcite使用一个称为转换器的特殊关系表达式子类(参见Converter类)。但是,当然,转换数据需要运行成本。
在计划使用多个引擎的查询时,Calcite根据其调用约定对关系表达式树的区域进行“着色”。计划器通过触发规则将操作推入数据源。如果引擎不支持特定操作,则规则不会触发。有时一个操作可能发生在多个地方,最终根据成本选择最佳方案。
  调用约定是实现了Convention接口,辅助接口(例如CassandraRel接口)和RelNode类的一组子类, 用于实现核心关系运算符(Project,Filter, Aggregate等等)的一个类。
内置SQL实现
  如果一个适配器没有实现所有核心关系运算符,Calcite如何实现SQL?
  答案是一个特定的内置调用约定EnumerableConvention。可枚举约定的关系表达式被实现为“内置”:Calcite生成Java代码,编译并在其自己的JVM中执行它。可枚举约定比运行在列式数据文件上的分布式引擎效率低,但可以实现所有核心关系运算符以及所有内置的SQL函数和运算符。如果一个数据源不能实现一个关系运算符,则枚举约定是一个回退。

1.15 统计和成本

  Calcite有一个元数据系统,允许你定义成本函数和有关关系运算符的统计信息,统称为元数据。每种元数据都有(通常)一种方法的接口。例如,selectivity 由RelMdSelectivity类和方法 getSelectivity(RelNode rel,RexNode predicate)定义。
  有许多内置的元数据类型,包括collation, column origins, column uniqueness, distinct row count, distribution, explain visibility, expression lineage, max row count, node types, parallelism, percentage original rows, population size, predicates, row count, selectivity, size, table references, unique keys; 也可以自定义。
  然后,您可以提供一个元数据提供者(Provider)来计算RelNode的特定子类的元数据。元数据提供者可以处理内置和扩展的元数据类型,以及内置和扩展RelNode类型。在准备查询时,Calcite将所有适用的元数据提供者组合起来,并维护一个缓存,以便只计算一次给定的元数据(例如在特定Filter运算符中x > 10条件的selectivity )。