原文链接: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是否可以接受 Top N 查询(ORDER BY aggFun() DESC LIMIT n)返回近似结果。
caseSensitive标识符匹配是否区分大小写。如果未指定,将会使用 lex 中的值。
conformanceSQL 一致性级别。包含如下值:DEFAULT(默认值,类似于 PRAGMATIC_2003)、LENIENTMYSQL_5ORACLE_10ORACLE_12PRAGMATIC_99PRAGMATIC_2003STRICT_92STRICT_99STRICT_2003SQL_SERVER_2008
createMaterializationsCalcite 是否应该创建物化视图。默认为 false。
defaultNullCollation如果查询中既未指定 NULLS FIRST 也未指定 NULLS LAST,应该如何对 NULL 值进行排序。默认值为 HIGH,对 NULL 值的排序与 Oracle 相同。
druidFetch执行 SELECT 查询时,Druid 适配器应当一次获取多少行记录。
forceDecorrelate优化器是否应该尽可能地尝试去除相关子查询。默认为 true。
fun内置函数和运算符的集合。有效值为 standard(默认值)、oraclespatial,并且可以使用逗号组合,例如 oracle,spatial
lex词法分析策略。有效值为 BIG_QUERYJAVAMYSQLMYSQL_ANSIORACLE(默认)、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(默认值)、JDBCCUSTOM(如果指定了 schemaFactory 则隐式设置为 CUSTOM)。如果指定了 model 则忽略该参数。
spark指定是否应使用 Spark 作为引擎来处理无法推送到源系统的处理。如果为 false(默认值),Calcite 会生成实现 Enumerable 接口的代码。
timeZone时区,例如 gmt-3。默认是 JVM 的时区。
typeSystem类型系统。实现 interface RelDataTypeSystem 并具有公共默认构造函数或 INSTANCE 常量的类的名称。
unquotedCasing如果标识符未加引号,设置如何存储标识符。值为 UNCHANGEDTO_UPPERTO_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 操作 (INSERTUPDATEDELETEMERGE),但不支持 CREATE SCHEMACREATE TABLE 等 DDL 操作。正如我们将看到的,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 参考中描述了这些命令。

要启用 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
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 子句并将记录聚集成集合的函数。内置的分组窗口函数是 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 方言

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

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

定义自定义模式

要定义自定义模式,你需要实现 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 FilterableTableinterface ProjectableFilterableTable

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

类型系统

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

关系运算符

所有关系运算符都实现 interface RelNode,并且大多数扩展了 class AbstractRelNode。最核心的运算符(被 SqlToRelConverter 使用并覆盖了常规的关系代数)是 TableScanTableModifyValuesProjectFilterAggregateJoinSortUnionIntersectMinusWindowMatch

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

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

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

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

优化规则

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

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

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

调用约定

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

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

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

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

内置 SQL 实现

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

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

统计和代价

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

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

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

写在最后

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

Calcite 从入门到精通