适配器

原文链接:https://calcite.apache.org/docs/adapter.html

模式适配器

模式适配器允许 Calcite 读取特定类型的数据,并将数据表现为模式中的表。

其他语言接口

引擎

许多项目和产品使用 Apache Calcite 进行 SQL 解析查询优化数据虚拟化数据联合查询物化视图重写。他们中的一些列在 由 Calcite 提供支持 页面上。

驱动

驱动允许你从应用程序连接到 Calcite。

JDBC 驱动由 Avatica 提供支持。连接可以是本地连接或远程连接(基于 HTTP 传输的 JSONProtobuf)。

JDBC 连接字符串的基本格式如下:

1
jdbc:calcite:property=value;property2=value2

其中 propertyproperty2 是下面描述的这些属性。连接字符串符合 OLE DB 连接字符串语法,由 Avatica 的 ConnectStringParser 实现。

JDBC 连接字符串参数

属性 描述
approximateDecimal DECIMAL 类型聚合函数的近似结果是否可以接受。
approximateDistinctCount COUNT(DISTINCT ...) 聚合函数的近似结果是否可以接受。
approximateTopN 前 N 个查询(ORDER BY aggFun() DESC LIMIT n)的近似结果是否可以接受。
caseSensitive 标识符是否区分大小写。如果未指定,将会使用 lex 中的值。
conformance SQL 的一致性级别。包含如下值:DEFAULT(默认值,类似于 PRAGMATIC_2003)、LENIENTMYSQL_5ORACLE_10ORACLE_12PRAGMATIC_99PRAGMATIC_2003STRICT_92STRICT_99STRICT_2003SQL_8SERVER_200
createMaterializations Calcite 是否应该创建物化实体。默认为 false。
defaultNullCollation 如果查询中既未指定 NULLS FIRST 也未指定 NULLS LAST,应该如何对 NULL 值进行排序。默认值为 HIGH,对 NULL 值的排序与 Oracle 相同。
德鲁伊获取 执行 SELECT 查询时,德鲁伊适配器应一次获取多少行。
强制去相关 规划者是否应该尽可能地尝试去相关。默认为真。
乐趣 内置函数和运算符的集合。有效值为“standard”(默认值)、“oracle”、“spatial”,并且可以使用逗号组合,例如“oracle,spatial”。
莱克斯 词汇政策。值为 BIG_QUERY、JAVA、MYSQL、MYSQL_ANSI、ORACLE(默认)、SQL_SERVER。
物化已启用 方解石是否应该使用物化。默认为假。
模型 JSON/YAML 模型文件的 URI 或内联(如inline:{...}JSON 和inline:...YAML)。
解析器工厂 解析器工厂。实现interface SqlParserImplFactory并具有公共默认构造函数或INSTANCE常量的类的名称。
引用 如何引用标识符。值为 DOUBLE_QUOTE、BACK_QUOTE、BRACKET。如果未指定,lex则使用值 from 。
引用大小写 如果标识符被引用,则如何存储标识符。值为 UNCHANGED、TO_UPPER、TO_LOWER。如果未指定,lex则使用值 from 。
模式 初始模式的名称。
模式工厂 模式工厂。实现interface SchemaFactory并具有公共默认构造函数或INSTANCE常量的类的名称。如果model指定则忽略。
模式类型 架构类型。值必须是“MAP”(默认值)、“JDBC”或“CUSTOM”(如果schemaFactory指定则为隐式)。如果model指定则忽略。
火花 指定是否应将 Spark 用作处理无法推送到源系统的引擎。如果为 false(默认值),Calcite 会生成实现 Enumerable 接口的代码。
时区 时区,例如“gmt-3”。默认是 JVM 的时区。
类型系统 类型系统。实现interface RelDataTypeSystem并具有公共默认构造函数或INSTANCE常量的类的名称。
不带引号的大小写 如果没有引用标识符,它们是如何存储的。值为 UNCHANGED、TO_UPPER、TO_LOWER。如果未指定,lex则使用值 from 。
类型强制 sql节点验证时类型不匹配时是否进行隐式类型强制,默认为true。

要根据内置架构类型连接到单个架构,你无需指定模型。例如,

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

使用通过 JDBC 模式适配器映射到 foodmart 数据库的模式创建连接。

同样,你可以基于用户定义的架构适配器连接到单个架构。例如,

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

与 Cassandra 适配器建立连接,相当于编写以下模型文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"version": "1.0",
"defaultSchema": "foodmart",
"schemas": [
{
type: 'custom',
name: 'twissandra',
factory: 'org.apache.calcite.adapter.cassandra.CassandraSchemaFactory',
operand: {
host: 'localhost',
keyspace: 'twissandra'
}
}
]
}

请注意该operand部分中的每个键如何schema.在连接字符串中带有前缀。

服务器

Calcite 的核心模块 ( calcite-core) 支持 SQL 查询 ( SELECT) 和 DML 操作 ( INSERT, UPDATE, DELETE, MERGE),但不支持CREATE SCHEMA或等 DDL 操作CREATE TABLE。正如我们将看到的,DDL 使存储库的状态模型复杂化并使解析器更难以扩展,因此我们将 DDL 排除在核心之外。

服务器模块 ( calcite-server) 为 Calcite 添加了 DDL 支持。它扩展了 SQL 解析器, 使用与子项目相同的机制,添加了一些 DDL 命令:

  • CREATEDROP SCHEMA
  • CREATEDROP FOREIGN SCHEMA
  • CREATEDROP TABLE(包括CREATE TABLE ... AS SELECT
  • CREATEDROP MATERIALIZED VIEW
  • CREATEDROP VIEW
  • CREATEDROP FUNCTION
  • CREATEDROP TYPE

SQL 参考中描述了命令。

要启用,请包含calcite-server.jar在你的类路径中,并添加 parserFactory=org.apache.calcite.sql.parser.ddl.SqlDdlParserImpl#FACTORY 到 JDBC 连接字符串(请参阅连接字符串属性 parserFactory)。这是一个使用sqllineshell的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ ./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

calcite-server模块是可选的。它的目标之一是使用你可以从 SQL 命令行尝试的简洁示例来展示 Calcite 的功能(例如物化视图、外部表和生成的列)。使用的所有功能calcite-server都可以通过 calcite-core.

如果你是子项目的作者,你的语法扩展不太可能与 中的匹配calcite-server,因此我们建议你通过扩展核心解析器来添加 SQL 语法扩展;如果你需要 DDL 命令,你可以将其复制粘贴calcite-server 到你的项目中。

目前,存储库未持久化。当你执行 DDL 命令时,你正在通过添加和删除可从 root 访问的对象来修改内存存储库 Schema。同一 SQL 会话中的所有命令都会看到这些对象。你可以通过执行 SQL 命令的相同脚本在以后的会话中创建相同的对象。

Calcite 还可以充当数据虚拟化或联合服务器:Calcite 管理多个外部模式中的数据,但对于客户端而言,这些数据似乎都在同一个地方。Calcite 选择应在何处进行处理,以及是否创建数据副本以提高效率。该calcite-server模块是朝着该目标迈出的一步;行业实力的解决方案需要进一步的包装(使 Calcite 可作为服务运行)、存储库持久性、授权和安全性。

可扩展性

还有许多其他 API 允许你扩展 Calcite 的功能。

在本节中,我们将简要介绍这些 API,让你了解哪些是可能的。要充分使用这些 API,你需要阅读其他文档,例如接口的 javadoc,并可能查找我们为它们编写的测试。

函数和运算符

有多种方法可以向 Calcite 添加运算符或函数。我们将首先描述最简单的(也是最不强大的)。

用户定义的函数是最简单的(但最不强大的)。它们编写起来很简单(你只需编写一个 Java 类并将其注册到你的模式中),但在参数的数量和类型、解析重载函数或派生返回类型方面没有提供很大的灵活性。

如果你想要这种灵活性,你可能需要编写一个 用户定义的运算符 (请参阅 参考资料interface SqlOperator)。

如果你的运算符不遵守标准 SQL 函数语法“ f(arg1, arg2, ...)”,那么你需要 扩展解析器

测试中有很多很好的例子: class UdfTest 测试用户定义的函数和用户定义的聚合函数。

聚合函数

用户定义的聚合函数类似于用户定义的函数,但每个函数都有几个对应的 Java 方法,一个用于聚合生命周期中的每个阶段:

  • init 创建一个累加器;
  • add 将一行的值添加到累加器;
  • merge 将两个累加器合二为一;
  • result 完成累加器并将其转换为结果。

例如,方法(伪代码)SUM(int)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 a.sum;
}

以下是计算列值为 4 和 7 的两行之和的调用序列:

1
2
3
4
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例如)不能用作聚合函数。

另一个区别是窗口是不相交的:特定行可以出现在多个窗口中。例如,10:37 出现在 9:00-10:00 和 9:15-9:45 小时。

窗口函数是递增计算的:当时钟从 10:14 到 10:15 滴答作响时,可能有两行进入窗口,而三行离开。为此,窗口函数有一个额外的生命周期操作:

  • remove 从累加器中删除一个值。

它的伪代码SUM(int)是:

1
2
3
Accumulator remove(Accumulator a, int x) {
return new Accumulator(a.sum - x);
}

以下是计算前 2 行的移动总和的调用序列,其中 4 行的值为 4、7、2 和 3:

1
2
3
4
5
6
7
8
9
10
11
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子句将记录聚集成集合的函数。内置的分组窗口函数是HOPTUMBLESESSION。你可以通过实现来定义附加功能 interface SqlGroupedWindowFunction

表函数和表宏

用户定义表函数 的定义方式与常规“标量”用户定义函数类似,但用于FROM查询子句中。以下查询使用名为 的表函数Ramp

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

用户定义的表宏使用与表函数相同的 SQL 语法,但定义不同。它们不是生成数据,而是生成关系表达式。在查询准备期间调用表宏,然后可以优化它们生成的关系表达式。(Calcite 的视图实现使用表宏。)

class TableFunctionTest 测试表函数并包含几个有用的示例。

扩展解析器

假设你需要以与将来对语法的更改兼容的方式扩展 Calcite 的 SQL 语法。Parser.jj在你的项目中复制语法文件 将是愚蠢的,因为语法经常被编辑。

幸运的是,Parser.jj实际上是一个 Apache FreeMarker 模板,其中包含可以替换的变量。解析器calcite-core使用变量的默认值(通常为空)实例化模板,但你可以覆盖。如果你的项目需要不同的解析器,你可以提供自己的config.fmppparserImpls.ftl文件,从而生成扩展解析器。

calcite-server模块在 [ CALCITE-707 ] 中创建并添加了 DDL 语句,例如CREATE TABLE,是你可以遵循的示例。另见 class ExtensionSqlParserTest

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

要自定义解析器应接受、实现 interface SqlConformance 或使用 enum SqlConformanceEnum.

要控制如何为外部数据库生成 SQL(通常通过 JDBC 适配器),请使用 class SqlDialect. 方言还描述了引擎的功能,例如它是否支持OFFSETFETCH子句。

定义自定义架构

要定义自定义架构,你需要实现 interface SchemaFactory.

在查询准备期间,Calcite 将调用此接口以找出你的架构包含哪些表和子架构。当查询中引用架构中的表时,Calcite 将要求你的架构创建 interface Table.

该表将被包装在 a 中 TableScan ,并将进行查询优化过程。

反思模式

反射模式 ( class ReflectiveSchema) 是一种包装 Java 对象以使其显示为模式的方法。其集合值字段将显示为表格。

它不是一个模式工厂,而是一个实际的模式;你必须创建对象并通过调用 API 将其包装在架构中。

class ReflectiveSchemaTest

定义自定义表

要定义自定义表,你需要实现 interface TableFactory. 模式工厂是一组命名表,而表工厂在绑定到具有特定名称(以及可选的一组额外操作数)的模式时会生成单个表。

修改数据

如果你的表要支持 DML 操作(INSERT、UPDATE、DELETE、MERGE),则你的实现interface Table必须实现 interface ModifiableTable.

流媒体

如果你的表支持流式查询,则你的实现interface Table必须实现 interface StreamableTable.

参见 class StreamTest 示例。

将操作推到你的桌子上

如果你希望将处理下推到自定义表的源系统,请考虑实现 interface FilterableTableinterface ProjectableFilterableTable

如果你想要更多的控制,你应该写一个计划规则。这将允许你下推表达式,做出关于是否下推处理的基于成本的决定,以及下推更复杂的操作,例如连接、聚合和排序。

类型系统

你可以通过实现 interface RelDataTypeSystem.

关系运算符

所有关系运算符都实现 interface RelNode 并扩展了 class AbstractRelNode. 核心运营商(使用 SqlToRelConverter 和覆盖常规关系代数)是 TableScanTableModifyValuesProjectFilterAggregateJoinSortUnionIntersectMinusWindowMatch

其中每一个都有一个“纯”逻辑子类, LogicalProject 依此类推。任何给定的适配器都有对应的引擎可以有效实现的操作;例如,Cassandra 适配器有 CassandraProject 但没有CassandraJoin.

你可以定义自己的子类RelNode以添加新运算符,或在特定引擎中实现现有运算符。

为了使运算符有用且强大,你需要 规划器规则将其与现有运算符相结合。(并且还提供元数据,见下文)。这是代数,效果是组合的:你编写一些规则,但它们组合起来处理指数数量的查询模式。

如果可能,让你的运营商成为现有运营商的子类;那么你就可以重新使用或调整其规则。更好的是,如果你的运算符是一个可以根据现有运算符重写(再次通过规划器规则)的逻辑运算,那么你应该这样做。你将无需额外工作即可重复使用这些运算符的规则、元数据和实现。

计划规则

规划器规则 ( class RelOptRule) 将关系表达式转换为等效的关系表达式。

规划器引擎注册了许多规划器规则并触发它们以将输入查询转换为更有效的内容。因此,规划器规则是优化过程的核心,但令人惊讶的是,每个规划器规则本身并不关心成本。计划引擎负责按顺序触发规则以产生最佳计划,但每个单独的规则只关心自己的正确性。

Calcite 有两个内置的规划器引擎: class VolcanoPlanner 使用动态规划,适用于穷举搜索,而 class HepPlanner 以更固定的顺序触发一系列规则。

调用约定

调用约定是特定数据引擎使用的协议。例如,卡桑德拉发动机具有关系运算符的集合, CassandraProjectCassandraFilter等等,并且这些操作符可以被相互连接,而无需从一个格式转换成另一种的数据。

如果数据需要从一种调用约定转换为另一种调用约定,Calcite 使用称为转换器的特殊关系表达式子类(请参阅 参考资料interface Converter)。但是当然转换数据有运行时成本。

在规划使用多个引擎的查询时,Calcite 根据其调用约定为关系表达式树的区域“着色”。规划器通过触发规则将操作推送到数据源中。如果引擎不支持特定操作,则不会触发规则。有时一项操作可能会发生在多个地方,最终会根据成本选择最佳方案。

调用约定是一个实现 的类 interface Convention、一个辅助接口(例如 interface CassandraRel),以及class RelNode 为核心关系运算符(ProjectFilterAggregate等)实现该接口的一组子类 。

内置 SQL 实现

如果适配器没有实现所有核心关系运算符,Calcite 如何实现 SQL?

答案是特定的内置调用约定 EnumerableConvention. 可枚举约定的关系表达式被实现为“内置”:Calcite 生成 Java 代码,编译它,并在它自己的 JVM 中执行。Enumerable 约定不如运行在面向列的数据文件上的分布式引擎那么有效,但它可以实现所有核心关系运算符和所有内置 SQL 函数和运算符。如果数据源无法实现关系运算符,则可枚举约定是一种后备。

统计和成本

Calcite 有一个元数据系统,允许你定义有关关系运算符的成本函数和统计信息,统称为元数据。每种元数据都有一个接口(通常)一个方法。例如,选择性由class RelMdSelectivity 和 方法 定义 getSelectivity(RelNode rel, RexNode predicate)

有许多内置的元数据,包括 排序规则列起源列唯一性不同行数分布解释可见性表达式谱系最大行数节点类型并行度原始行百分比人口大小谓词行计数选择性大小表引用唯一键;你也可以定义你自己的。

然后,你可以提供一个元数据提供程序,该提供程序RelNode. 元数据提供程序可以处理内置和扩展元数据类型,以及内置和扩展RelNode类型。在准备查询时,Calcite 结合了所有适用的元数据提供者并维护一个缓存,以便给定的元数据(例如x > 10特定Filter运算符中条件的选择性)仅计算一次。

写在最后

笔者因为工作原因接触到 Calcite,前期学习过程中,深感 Calcite 学习资料之匮乏,因此创建了Calcite 从入门到精通知识星球,希望能够将学习过程中的资料和经验沉淀下来,为更多想要学习 Calcite 的朋友提供一些帮助。

目前星球刚创建,内部积累的资料还很有限,因此暂时不收费,感兴趣的同学可以联系我,免费邀请进入星球。

Calcite 从入门到精通
Calcite 从入门到精通