这是一个循序渐进的教程,展示了如何构建和连接Calcite。它使用一个简单的适配器,使CSV文件的目录看起来是一个包含表的模式。Calcite完成了其余的工作,并提供了完整的SQL接口。
Calcite-example-CSV是一个功能齐全的Calcite适配器,读取CSV(逗号分隔值)格式的文本文件。值得注意的是,几百行Java代码就足以提供完整的SQL查询功能。
CSV还可以作为构建其他数据格式的适配器的模板。尽管代码行数不多,但它涵盖了几个重要的概念:
-
使用SchemaFactory和schema接口的用户定义模式;
-
在模型JSON文件中声明模式;
-
在模型JSON文件中声明视图;
-
使用table接口的用户定义表;
-
确定表的记录类型;
-
Table的简单实现,使用scanabletable接口,直接枚举所有行;
-
一个更高级的实现,它实现了FilterableTable,可以根据简单的谓词过滤出行;
-
Table的高级实现,使用TranslatableTable,转换为使用规划器规则的关系操作符。
开始尝试calcite
首先你应该安装Java 8、Java 9或Java 10,以及Git
$ git clone https://github.com/apache/calcite.git
$ cd calcite/example/csv
$ ./sqlline
使用sqlline连接calcite
$ ./sqlline
sqlline> !connect jdbc:calcite:model=src/test/resources/model.json admin admin
元数据查询
0: jdbc:calcite:model=src/test/resources/mode> !tables
+-----------+-------------+------------+--------------+---------+----------+------------+-----------+---------------------------+----------------+
| TABLE_CAT | TABLE_SCHEM | TABLE_NAME | TABLE_TYPE | REMARKS | TYPE_CAT | TYPE_SCHEM | TYPE_NAME | SELF_REFERENCING_COL_NAME | REF_GENERATION |
+-----------+-------------+------------+--------------+---------+----------+------------+-----------+---------------------------+----------------+
| | SALES | DEPTS | TABLE | | | | | | |
| | SALES | EMPS | TABLE | | | | | | |
| | SALES | SDEPTS | TABLE | | | | | | |
| | metadata | COLUMNS | SYSTEM TABLE | | | | | | |
| | metadata | TABLES | SYSTEM TABLE | | | | | | |
+-----------+-------------+------------+--------------+---------+----------+------------+-----------+---------------------------+----------------+
从JDBC角度,sqlline的!tables命令只是在后台执行DatabaseMetaData.getTables()。它还有其他查询JDBC元数据的命令,比如!column和!description
如您所见,系统中有5个表:当前SALES模式中的表EMPS、DEPTS和HOBBIES,以及系统元数据模式中的COLUMNS和TABLES。系统表总是出现在calcite中,但其他表是由模式的具体实现提供的;在本例中,EMPS和DEPTS表基于resources/sales目录中的EMPS.csv和DEPTS.csv文件。
让我们在这些表上执行一些查询,以说明calcite提供了SQL的完整实现。首先,表扫描:
0: jdbc:calcite:model=src/test/resources/mode> select * from EMPS;
+-------+-------+--------+--------+---------------+-------+------+---------+---------+------------+
| EMPNO | NAME | DEPTNO | GENDER | CITY | EMPID | AGE | SLACKER | MANAGER | JOINEDAT |
+-------+-------+--------+--------+---------------+-------+------+---------+---------+------------+
| 100 | Fred | 10 | | | 30 | 25 | true | false | 1996-08-03 |
| 110 | Eric | 20 | M | San Francisco | 3 | 80 | | false | 2001-01-01 |
| 110 | John | 40 | M | Vancouver | 2 | null | false | true | 2002-05-03 |
| 120 | Wilma | 20 | F | | 1 | 5 | | true | 2005-09-07 |
| 130 | Alice | 40 | F | Vancouver | 2 | null | false | true | 2007-01-01 |
+-------+-------+--------+--------+---------------+-------+------+---------+---------+------------+
5 rows selected (0.054 seconds)
group by和join操作
0: jdbc:calcite:model=src/test/resources/mode> SELECT d.name, COUNT(*) FROM emps AS e JOIN depts AS d ON e.deptno = d.deptno GROUP BY d.name;
+-----------+--------+
| NAME | EXPR$1 |
+-----------+--------+
| Sales | 1 |
| Marketing | 2 |
+-----------+--------+
2 rows selected (0.318 seconds)
最后,VALUES操作符生成一行,这是测试表达式和SQL内置函数的一种方便的方法:
0: jdbc:calcite:model=src/test/resources/mode> VALUES CHAR_LENGTH('Hello, ' || 'world!');
+--------+
| EXPR$0 |
+--------+
| 13 |
+--------+
1 row selected (0.067 seconds)
calcite还有许多其他SQL特性。我们这里没时间讲了。你可以编写更多的查询来进行试验。
Schema discovery
Calcite是怎么找到这些表格的?记住,核心Calcite不知道任何关于CSV文件。(作为一个“没有存储层的数据库”,Calcite不知道任何文件格式。)Calcite知道这些表,因为我们告诉它运行Calcite-example-csv项目中的代码。
这个流程有几个步骤。首先,我们基于模型文件中的模式工厂类定义一个模式。然后模式工厂创建一个模式,该模式创建几个表,每个表都知道如何通过扫描CSV文件获取数据。最后,在Calcite解析了查询并计划使用这些表之后,Calcite在执行查询时调用这些表来读取数据。现在让我们更详细地看看这些步骤。
在JDBC连接字符串上,我们以JSON格式给出了模型的路径。模型如下:
{
version: '1.0',
defaultSchema: 'SALES',
schemas: [
{
name: 'SALES',
type: 'custom',
factory: 'org.apache.calcite.adapter.csv.CsvSchemaFactory',
operand: {
directory: 'sales'
}
}
]
}
该模型定义了一个名为“SALES”的模式。该模式由插件类org.apache.calcite.adapter.csv.CsvSchemaFactory提供支持。该插件是calcite-example-csv项目的一部分,并实现Calcite接口SchemaFactory。它的create方法实例化一个模式,传入模型文件中的directory参数:
public Schema create(SchemaPlus parentSchema, String name,
Map<String, Object> operand) {
String directory = (String) operand.get("directory");
String flavorName = (String) operand.get("flavor");
CsvTable.Flavor flavor;
if (flavorName == null) {
flavor = CsvTable.Flavor.SCANNABLE;
} else {
flavor = CsvTable.Flavor.valueOf(flavorName.toUpperCase());
}
return new CsvSchema(
new File(directory),
flavor);
}
在模型的驱动下,模式工厂实例化一个名为“SALES”的模式。该模式是org.apache.calcite.adapter.csv.CsvSchema的一个实例,实现了Calcite接口schema。
模式的工作是生成一个系列的表。(它也可以列出子模式和表函数,但这些是高级特性,calcite-example-csv不支持它们。)这些表实现了calcite的Table接口。CsvSchema生成的表是CsvTable及其子类的实例。
下面是来自CsvSchema的相关代码,它覆盖AbstractSchema基类中的getTableMap()方法。
protected Map<String, Table> getTableMap() {
// Look for files in the directory ending in ".csv", ".csv.gz", ".json",
// ".json.gz".
File[] files = directoryFile.listFiles(
new FilenameFilter() {
public boolean accept(File dir, String name) {
final String nameSansGz = trim(name, ".gz");
return nameSansGz.endsWith(".csv")
|| nameSansGz.endsWith(".json");
}
});
if (files == null) {
System.out.println("directory " + directoryFile + " not found");
files = new File[0];
}
// Build a map from table name to table; each file becomes a table.
final ImmutableMap.Builder<String, Table> builder = ImmutableMap.builder();
for (File file : files) {
String tableName = trim(file.getName(), ".gz");
final String tableNameSansJson = trimOrNull(tableName, ".json");
if (tableNameSansJson != null) {
JsonTable table = new JsonTable(file);
builder.put(tableNameSansJson, table);
continue;
}
tableName = trim(tableName, ".csv");
final Table table = createTable(file);
builder.put(tableName, table);
}
return builder.build();
}
/** Creates different sub-type of table based on the "flavor" attribute. */
private Table createTable(File file) {
switch (flavor) {
case TRANSLATABLE:
return new CsvTranslatableTable(file, null);
case SCANNABLE:
return new CsvScannableTable(file, null);
case FILTERABLE:
return new CsvFilterableTable(file, null);
default:
throw new AssertionError("Unknown flavor " + flavor);
}
}
模式扫描目录并查找所有文件名以“.csv”结尾的文件,并为它们创建表。在本例中,目录是sales并包含文件EMPS.csv和DEPTS.csv,这些文件成为表EMPS和DEPTS。
模式中的表和视图
注意,我们不需要在模型中定义任何表;模式自动生成表。
除了自动创建的表之外,还可以使用模式的tables属性定义额外的表。
让我们看看如何创建一个重要和有用的表类型,即视图。
当您编写查询时,视图看起来像一个表,但它不存储数据。它通过执行查询来获得结果。在规划查询时,视图会展开,因此查询规划器通常可以执行优化,比如从SELECT子句中删除最终结果中没有使用的表达式。
下面是定义视图的模式
{
version: '1.0',
defaultSchema: 'SALES',
schemas: [
{
name: 'SALES',
type: 'custom',
factory: 'org.apache.calcite.adapter.csv.CsvSchemaFactory',
operand: {
directory: 'sales'
},
tables: [
{
name: 'FEMALE_EMPS',
type: 'view',
sql: 'SELECT * FROM emps WHERE gender = \'F\''
}
]
}
]
}
行类型:’view’将FEMALE_EMPS标记为视图,而不是常规表或自定义表。注意,视图定义中的单引号使用反斜杠进行转义,这是JSON的正常方式。
JSON并不容易生成长字符串,所以Calcite支持另一种语法。如果你的视图有一个很长的SQL语句,你可以提供一个行列表而不是一个字符串:
{
name: 'FEMALE_EMPS',
type: 'view',
sql: [
'SELECT * FROM emps',
'WHERE gender = \'F\''
]
}
现在我们已经定义了一个视图,我们可以在查询中使用它,就像它是一个表一样:
sqlline> SELECT e.name, d.name FROM female_emps AS e JOIN depts AS d on e.deptno = d.deptno;
+--------+------------+
| NAME | NAME |
+--------+------------+
| Wilma | Marketing |
+--------+------------+
该模式是一个常规模式,包含一个由org.apache.calcite.adapter.csv.CsvTableFactory提供的自定义表。该表实现了calcite接口TableFactory。它的create方法实例化了一个CsvScannableTable,从模型文件中传入file参数:
public CsvTable create(SchemaPlus schema, String name,
Map<String, Object> map, RelDataType rowType) {
String fileName = (String) map.get("file");
final File file = new File(fileName);
final RelProtoDataType protoRowType =
rowType != null ? RelDataTypeImpl.proto(rowType) : null;
return new CsvScannableTable(file, protoRowType);
}
实现自定义表通常比实现自定义模式更简单。这两种方法最终可能会创建一个类似的Table接口实现,但是对于自定义表,您不需要实现元数据发现。(CsvTableFactory创建了一个CsvScannableTable,就像CsvSchema一样,但是表实现并不扫描文件系统中的.csv文件。)
定制表需要模型的作者做更多的工作(作者需要显式地指定每个表及其文件),但也给作者更多的控制(比如,为每个表提供不同的参数)。
模型中的注释
包括在/ … / 或者//中,例如:
{
version: '1.0',
/* Multi-line
comment. */
defaultSchema: 'CUSTOM_TABLE',
// Single-line comment.
schemas: [
..
]
}
使用规划器规则优化查询
到目前为止,我们看到的表实现都很好,只要表不包含大量的数据。但是,如果您的客户表有100列和100万行,您宁愿系统不为每个查询检索所有数据。您希望calcite与适配器协商,并找到更有效的访问数据的方法。
这种协商是查询优化的一种简单形式。calcite通过添加规划器规则支持查询优化。Planner规则的操作方法是在查询解析树中寻找模式(例如某种表上的项目),并用一组实现优化的新节点替换树中匹配的节点。
规划器规则也是可扩展的,就像模式和表一样。因此,如果您有一个希望通过SQL访问的数据存储,您首先需要定义一个自定义表或模式,然后定义一些规则以使访问更加有效。
要查看实际效果,让我们使用规划器规则访问CSV文件中的列子集。让我们对两个非常相似的模式运行相同的查询:
sqlline> !connect jdbc:calcite:model=src/test/resources/model.json admin admin
0: jdbc:calcite:model=src/test/resources/mode> explain plan for select name from emps;
+--------------------------------------------------+
| PLAN |
+--------------------------------------------------+
| EnumerableCalc(expr#0..9=[{inputs}], NAME=[$t1])
EnumerableTableScan(table=[[SALES, EMPS]])
|
+--------------------------------------------------+
1 row selected (0.056 seconds)
sqlline> !connect jdbc:calcite:model=src/test/resources/smart.json admin admin
0: jdbc:calcite:model=src/test/resources/smar> explain plan for select name from emps;
+---------------------------------------------------+
| PLAN |
+---------------------------------------------------+
| CsvTableScan(table=[[SALES, EMPS]], fields=[[1]])
|
+---------------------------------------------------+
1 row selected (1.337 seconds)
是什么导致了计划上的差异?让我们跟着证据的线索走。smart.json模型文件,有一个额外的行:
flavor: "translatable"
这将导致使用flavor = TRANSLATABLE创建一个CsvSchema,它的createTable方法将创建CsvTranslatableTable的实例,而不是CsvScannableTable。
CsvTranslatableTable实现了TranslatableTable.toRel()方法来创建CsvTableScan。表扫描是查询操作符树的叶子。通常的实现是EnumerableTableScan,但是我们创建了一个独特的子类型,它将触发规则。
以下是整个规则:
public class CsvProjectTableScanRule
extends RelRule<CsvProjectTableScanRule.Config> {
/** Creates a CsvProjectTableScanRule. */
protected CsvProjectTableScanRule(Config config) {
super(config);
}
@Override public void onMatch(RelOptRuleCall call) {
final LogicalProject project = call.rel(0);
final CsvTableScan scan = call.rel(1);
int[] fields = getProjectFields(project.getProjects());
if (fields == null) {
// Project contains expressions more complex than just field references.
return;
}
call.transformTo(
new CsvTableScan(
scan.getCluster(),
scan.getTable(),
scan.csvTable,
fields));
}
private int[] getProjectFields(List<RexNode> exps) {
final int[] fields = new int[exps.size()];
for (int i = 0; i < exps.size(); i++) {
final RexNode exp = exps.get(i);
if (exp instanceof RexInputRef) {
fields[i] = ((RexInputRef) exp).getIndex();
} else {
return null; // not a simple projection
}
}
return fields;
}
/** Rule configuration. */
public interface Config extends RelRule.Config {
Config DEFAULT = EMPTY
.withOperandSupplier(b0 ->
b0.operand(LogicalProject.class).oneInput(b1 ->
b1.operand(CsvTableScan.class).noInputs()))
.as(Config.class);
@Override default CsvProjectTableScanRule toRule() {
return new CsvProjectTableScanRule(this);
}
}
规则的默认实例驻留在CsvRules的持有者类中:
public abstract class CsvRules {
public static final CsvProjectTableScanRule PROJECT_SCAN =
CsvProjectTableScanRule.Config.DEFAULT.toRule();
}
对默认配置(接口配置中的default字段)中的withOperandSupplier方法的调用声明了将导致规则触发的关系表达式模式。如果规划器看到LogicalProject的唯一输入是没有输入的CsvTableScan,它将调用该规则。
规则的变体是可能的。例如,不同的规则实例可能会匹配CsvTableScan上的EnumerableProject。
onMatch方法生成一个新的关系表达式,并调用RelOptRuleCall.transformTo()来指示规则已成功触发。
查询优化过程
关于calcite的查询规划器有多聪明有很多话要说,但我们在这里不说。这种聪明的设计是为了减轻你作为计划规则的作者的负担。
首先,calcite不会按照规定的顺序执行规则。查询优化过程遵循分支树的许多分支,就像下棋程序检查许多可能的走法序列一样。如果规则A和B都匹配查询操作符树的给定部分,则calcite可以同时触发两者。
其次,Calcite 在计划之间进行选择时使用成本,但成本模型并不能阻止规则的触发,这在短期内似乎更昂贵。
许多优化器都有一个线性优化方案。 如上所述,面对规则 A 和规则 B 之间的选择,这样的优化器需要立即选择。 它可能有诸如“将规则 A 应用于整棵树,然后将规则 B 应用于整棵树”之类的策略,或者应用基于成本的策略,应用产生更便宜结果的规则。
Calcite 不需要这种妥协。 这使得组合各种规则集变得简单。 如果,假设您想将识别物化视图的规则与从 CSV 和 JDBC 源系统读取的规则结合起来,您只需将所有规则的集合提供给 Calcite 并告诉它执行它。
Calcite 确实使用了成本模型。 成本模型决定最终使用哪个计划,有时会修剪搜索树以防止搜索空间爆炸,但它从不强迫您在规则 A 和规则 B 之间进行选择。 这很重要,因为它避免陷入局部最小值 在实际上不是最佳的搜索空间中。
此外(你猜对了)成本模型是可插入的,它所基于的表和查询运算符统计也是如此。 但这可能是以后的主题。
本文为从大数据到人工智能博主「xiaozhch5」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://lrting.top/backend/351/