模式适配器
模式适配器允许 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 的朋友提供一些帮助。