模式适配器
模式适配器允许 Calcite 读取特定类型的数据,并将这些数据显示为模式中的表。
- Cassandra 适配器(calcite-cassandra);
- CSV 适配器(示例/csv);
- Druid 适配器(calcite-druid);
- Elasticsearch 适配器(calcite-elasticsearch);
- 文件适配器(calcite-file);
- Geode 适配器(calcite-geode);
- InnoDB 适配器(calcite-innodb);
- JDBC 适配器(calcite-core 的一部分);
- MongoDB 适配器(calcite-mongodb);
- 操作系统适配器(calcite-os);
- Pig 适配器(calcite-pig);
- Redis 适配器(calcite-redis);
- Solr cloud 适配器(solr-sql);
- Spark 适配器(calcite-spark);
- Splunk 适配器(calcite-splunk);
- Eclipse 内存分析器 (MAT) 适配器(mat-calcite-plugin);
- Apache Kafka 适配器。
其他语言接口
- Piglet(calcite-piglet)在 Pig Latin 的子集中运行查询;
引擎
许多项目和产品使用 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 中的值。 |
| 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,对 NULL 值的排序与 Oracle 相同。 |
| druidFetch | 执行 SELECT 查询时,Druid 适配器应当一次获取多少行记录。 |
| forceDecorrelate | 优化器是否应该尽可能地尝试去除相关子查询。默认为 true。 |
| fun | 内置函数和运算符的集合。有效值为 standard(默认值)、oracle、spatial,并且可以使用逗号组合,例如 oracle,spatial。 |
| lex | 词法分析策略。有效值为 BIG_QUERY、JAVA、MYSQL、MYSQL_ANSI、ORACLE(默认)、SQL_SERVER。 |
| materializationsEnabled | Calcite 是否应该使用物化视图。默认为 false。 |
| model | JSON/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 中的值。 |
| typeCoercion | sql 节点校验时,如果类型不匹配是否进行隐式类型强转,默认为 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 | { |
请注意 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 | $ ./sqlline |
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 | struct Accumulator { |
以下是计算列值为 4 和 7 的两行之和的调用序列:
1 | a = init() # a = {0} |
窗口函数
窗口函数类似于聚合函数,但它应用于由 OVER 子句而不是 GROUP BY 子句收集的一组行。每个聚合函数都可以用作窗口函数,但存在一些关键区别。窗口函数看到的行可能是有序的,并且依赖于顺序的窗口函数(例如 RANK )不能用作聚合函数。
另一个区别是窗口可以是相交的(non-disjoint):特定行可以出现在多个窗口中。例如,10:37 既可以出现在 9:00-10:00 时间段,也可以出现在 9:15-9:45 时间段。
窗口函数是动态计算的:当时钟从 10:14 跳转到 10:15 时,可能有两行进入窗口,而三行离开。为此,窗口函数有一个额外的生命周期操作:
remove从累加器中删除一个值。
它的伪代码 SUM(int) 是:
1 | Accumulator remove(Accumulator a, int x) { |
以下是计算前 2 行动态求和(SUM)的调用顺序,其中 4 行的数值为 4、7、2 和 3:
1 | a = init() # a = {0} |
分组窗口函数
分组窗口函数是操作 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 的朋友提供一些帮助。

