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

模式适配器

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

主要适配器

其他语言接口

引擎

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

赞助商

驱动

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

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

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

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

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

JDBC 连接字符串参数

属性描述
approximateDecimal是否可以接受 DECIMAL 类型聚合函数返回近似结果。
approximateDistinctCount是否可以接受 COUNT(DISTINCT ...) 聚合函数返回近似结果。
approximateTopN是否可以接受 Top N 查询(ORDER BY aggFun() DESC LIMIT n)返回近似结果。
caseSensitive标识符匹配是否区分大小写。如果未指定,将会使用 lex 中的值。
conformanceSQL 一致性级别。包含如下值:DEFAULT(默认值,类似于 PRAGMATIC_2003)、LENIENT、MYSQL_5、ORACLE_10、ORACLE_12、PRAGMATIC_99、PRAGMATIC_2003、STRICT_92、STRICT_99、STRICT_2003、SQL_SERVER_2008。
createMaterializationsCalcite 是否应该创建物化视图。默认为 false。
defaultNullCollation如果查询中既未指定 NULLS FIRST 也未指定 NULLS LAST,应该如何对 NULL 值进行排序。默认值为 HIGH,对 NULL 值的排序与 Oracle 相同。
druidFetch执行 SELECT 查询时,Druid 适配器应当一次获取多少行记录。
forceDecorrelate优化器是否应该尽可能地尝试去除相关子查询。默认为 true。
fun内置函数和运算符的集合。有效值为 standard(默认值)、oracle、spatial,并且可以使用逗号组合,例如 oracle,spatial。
lex词法分析策略。有效值为 BIG_QUERY、JAVA、MYSQL、MYSQL_ANSI、ORACLE(默认)、SQL_SERVER。
materializationsEnabledCalcite 是否应该使用物化视图。默认为 false。
modelJSON/YAML 模型文件的 URI 或内联的 JSON(例如:inline:{...}) 、内联的 YAML(例如: inline:...)。
parserFactory解析器工厂。实现 interface SqlParserImplFactory 并具有公共默认构造函数或 INSTANCE 常量的类的名称。
quoting如何引用标识符。值为 DOUBLE_QUOTE、BACK_TICK、BACK_TICK_BACKSLASH、BRACKET。如果未指定,则使用 lex 中的值。
quotedCasing如果标识符被引用,设置如何存储标识符。值为 UNCHANGED、TO_UPPER、TO_LOWER。如果未指定,则使用 lex 中的值。
schema初始模式的名称。
schemaFactory模式工厂。实现 interface SchemaFactory 并具有公共默认构造函数或 INSTANCE 常量的类的名称。如果指定了 model 则忽略该参数。
schemaType模式类型。值必须是 MAP(默认值)、JDBC 或 CUSTOM(如果指定了 schemaFactory 则隐式设置为 CUSTOM)。如果指定了 model 则忽略该参数。
spark指定是否应使用 Spark 作为引擎来处理无法推送到源系统的处理。如果为 false(默认值),Calcite 会生成实现 Enumerable 接口的代码。
timeZone时区,例如 gmt-3。默认是 JVM 的时区。
typeSystem类型系统。实现 interface RelDataTypeSystem 并具有公共默认构造函数或 INSTANCE 常量的类的名称。
unquotedCasing如果标识符未加引号,设置如何存储标识符。值为 UNCHANGED、TO_UPPER、TO_LOWER。如果未指定,则使用 lex 中的值。
typeCoercionsql 节点校验时,如果类型不匹配是否进行隐式类型强转,默认为 true。

要基于内置模式类型连接到单个模式,你不需要指定 model 参数。例如,通过映射到 foodmart 数据库的 JDBC 模式适配器创建一个模式,并使用这个模式创建一个数据库连接。

1
jdbc:calcite:schemaType=JDBC; schema.jdbcUser=SCOTT; schema.jdbcPassword=TIGER; schema.jdbcUrl=jdbc:hsqldb:res: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 或 CREATE TABLE 等 DDL 操作。正如我们将看到的,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;
  • CREATE 和 DROP FUNCTION;
  • CREATE 和 DROP TYPE。

SQL 参考中描述了这些命令。

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

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 中的 API 获得。

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

目前,元数据库尚未持久化。当你执行 DDL 命令时,你正在通过添加和删除可从根 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
16
17
18
19
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 )不能用作聚合函数。

另一个区别是窗口可以是相交的(non-disjoint):特定行可以出现在多个窗口中。例如,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 行动态求和(SUM)的调用顺序,其中 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 子句并将记录聚集成集合的函数。内置的分组窗口函数是 HOP、TUMBLE 和 SESSION。你可以通过实现 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.fmpp 和 parserImpls.ftl 文件,从而生成扩展解析器。

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

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

要自定义解析器应接受的 SQL 扩展,请实现 interface SqlConformance 或使用 enum SqlConformanceEnum.

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

定义自定义模式

要定义自定义模式,你需要实现 interface SchemaFactory。

在查询准备期间,Calcite 将调用此接口,来查找自定义模式包含哪些表和子模式。当查询引用了模式中的表时,Calcite 将要求自定义模式创建 interface Table。

表将被包装在 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 FilterableTable 或 interface ProjectableFilterableTable。

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

类型系统

你可以通过实现 interface RelDataTypeSystem 来自定义类型系统的某些方面。

关系运算符

所有关系运算符都实现 interface RelNode,并且大多数扩展了 class AbstractRelNode。最核心的运算符(被 SqlToRelConverter 使用并覆盖了常规的关系代数)是 TableScan, TableModify, Values, Project, Filter, Aggregate, Join, Sort, Union, Intersect, Minus, Window 和 Match。

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

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

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

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

优化规则

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

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

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

调用约定

调用约定是特定数据引擎使用的协议。例如,Cassandra 引擎有一组关系运算符,CassandraProject,CassandraFilter 等,并且这些运算符可以相互连接,而无需将数据从一种格式转换成另一种格式。

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

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

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

内置 SQL 实现

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

答案是特定的内置调用约定 EnumerableConvention。Enumerable 约定的关系表达式作为内置实现:Calcite 生成 Java 代码,对其进行编译,并在其自己的 JVM 中执行。Enumerable 约定的效率低于运行在面向列的数据文件上的分布式引擎,但它可以实现所有核心关系运算符以及所有内置 SQL 函数和运算符。如果数据源无法实现关系运算符,则可以使用枚举约定。

统计和代价

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

有许多种内置的元数据,包括:排序规则、 列来源、 列唯一性、 唯一行数、 分布、 执行计划可见性、 表达式血缘、 最大行数、 节点类型、 并行度、 原始行百分比、 总体大小、 谓词、 行数、 选择性、 大小、 表引用 和 唯一键。你也可以定义自己的元数据。

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

写在最后

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

Calcite 从入门到精通