==
这是一个手把手式文档,教你如何构建并且连接到Calcite。我们用一个简单的适配器来将一个包含CSV文件的目录变成一个包含数据表的数据库(原文描述为schema)。Calcite可以提供一个完整的SQL接口。
Calcite-example-CSV是一个全功能适配器来使得Calcite可以读取CSV格式文件。可以通过几百行代码就能够完成一个全SQL查询功能。
CSV适配器可以作为抛砖引玉的模板套用到其他数据格式上。尽管他代码量不多,但是麻雀虽小五脏俱全,重要原理都包含其中:
-
使用
SchemaFactory和Schema interfaces来自定义schema -
使用固定格式的JSON文件来(
a model JSON file模型文件)声明数据库(schemas) -
使用固定格式的JSON文件来(
a model JSON file模型文件)声明视图(views) -
使用
Table interface来自定义表(Table) -
确定表格的记录类型
-
使用
ScannableTable interface来实现一个简单的表(Table),来枚举所有行(rows) -
进阶实现
FilterableTable,可以根据条件(simple predicates)来过滤数据 -
表的进阶实现
TranslatableTable,将执行计划翻译成关系运算(translates to relational operators using planner rules)
下载和编译
需要Java环境(1.7及以上版本,推荐1.8),git以及maven(3.2.1及以上版本)
$ git clone github.com/apache/calc…
$ cd calcite
$ mvn install -DskipTests -Dcheckstyle.skip=true
$ cd example/csv
第一个查询
现在让我们来使用sqlline来连接Calcite,sqlline是一个包含在整个Calcite项目里的SQL的命令行工具。
$ ./sqlline
sqlline> !connect jdbc:calcite:model=target/test-classes/model.json admin admin
(如果是windows操作系统,使用sqlline.bat)
执行一个元数据查询:
sqlline> !tables
+------------+--------------+-------------+---------------+----------+------+
| TABLE_CAT | TABLE_SCHEM | TABLE_NAME | TABLE_TYPE | REMARKS | TYPE |
+------------+--------------+-------------+---------------+----------+------+
| null | SALES | DEPTS | TABLE | null | null |
| null | SALES | EMPS | TABLE | null | null |
| null | SALES | HOBBIES | TABLE | null | null |
| null | metadata | COLUMNS | SYSTEM_TABLE | null | null |
| null | metadata | TABLES | SYSTEM_TABLE | null | null |
+------------+--------------+-------------+---------------+----------+------+
(*译者注:上面案例里使用的!tables命令查询元数据,但是译者在使用的时候发现这个命令不好使)
0: jdbc:calcite:model=target/test-classes/mod> !table
+-----------+-------------+------------+------------+---------+----------+------------+-----------+---------------------------+----------------+
| 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()方法,还有其他的获取元数据命令如:!columns,!describe)
(译者注:!describe需要加表名)
0: jdbc:calcite:model=target/test-classes/mod> !describe
Usage: describe <table name>
0: jdbc:calcite:model=target/test-classes/mod> !describe DEPTS
+-----------+-------------+------------+-------------+-----------+-----------+-------------+---------------+----------------+----------------+----------+---------+------------+---------------+------------------+-------------------+------------------+-------------+---------------+--------------+-------------+
| TABLE_CAT | TABLE_SCHEM | TABLE_NAME | COLUMN_NAME | DATA_TYPE | TYPE_NAME | COLUMN_SIZE | BUFFER_LENGTH | DECIMAL_DIGITS | NUM_PREC_RADIX | NULLABLE | REMARKS | COLUMN_DEF | SQL_DATA_TYPE | SQL_DATETIME_SUB | CHAR_OCTET_LENGTH | ORDINAL_POSITION | IS_NULLABLE | SCOPE_CATALOG | SCOPE_SCHEMA | SCOPE_TABLE |
+-----------+-------------+------------+-------------+-----------+-----------+-------------+---------------+----------------+----------------+----------+---------+------------+---------------+------------------+-------------------+------------------+-------------+---------------+--------------+-------------+
| | SALES | DEPTS | DEPTNO | 4 | INTEGER | -1 | null | null | 10 | 1 | | | null | null | -1 | 1 | YES | | | |
| | SALES | DEPTS | NAME | 12 | VARCHAR CHARACTER SET "ISO-8859-1" COLLATE "ISO-8859-1primary" | -1 | null | null | 10 | 1 | | | null | null | -1 | 2 |
+-----------+-------------+------------+-------------+-----------+-----------+-------------+---------------+----------------+----------------+----------+---------+------------+---------------+------------------+-------------------+------------------+-------------+---------------+--------------+-------------+
你能看到,在执行!tables的时候有5个表,表EMPS, DEPTS和HOBBIES在SALES库(schema)里,表COLUMNS和TABLES在系统元数据库(system metadata schema)里。系统表总是在Calcite里显示,但其他表是由库(schema)的实现来指定的,在本例中,EMPS和DEPTS表来源于target/test-classes路径下的EMPS.csv和DEPTS.csv。
让我们来执行一些查询,来展示Calcite的全SQL功能,首先表检索:
sqlline> SELECT * FROM emps;
+--------+--------+---------+---------+----------------+--------+-------+---+
| EMPNO | NAME | DEPTNO | GENDER | CITY | EMPID | AGE | S |
+--------+--------+---------+---------+----------------+--------+-------+---+
| 100 | Fred | 10 | | | 30 | 25 | t |
| 110 | Eric | 20 | M | San Francisco | 3 | 80 | n |
| 110 | John | 40 | M | Vancouver | 2 | null | f |
| 120 | Wilma | 20 | F | | 1 | 5 | n |
| 130 | Alice | 40 | F | Vancouver | 2 | null | f |
+--------+--------+---------+---------+----------------+--------+-------+---+
接下来是表连接和分组聚合查询:
sqlline> 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 |
+------------+---------+
最后,一个计算操作返回一个单行记录,也可以通过这种简便的方法来测试表达式和SQL函数
sqlline> VALUES CHAR_LENGTH('Hello, ' || 'world!');
+---------+
| EXPR$0 |
+---------+
| 13 |
+---------+
Calcite还包含很多SQL特性,这里就不一一列举了。
Schema探索
那么Calcite是如何发现表的呢?事实上Calcite的核心是并不能理解CSV文件的(作为一个“没有存储层的databse”,Calcite是了解任何文件格式),之所以Calcite能读取上文中的元数据,是因为在calcite-example-csv里我们撰写了相关代码。
在执行链里包含着很多步骤。首先我们定义一个可以被库工厂加载的模型文件(we define a schema based on a schema factory class in a model file.)。然后库工厂会加载成数据库并创建许多表,每一个表都需要知道自己如何加载CSV中的数据。最后Calcite解析完查询并将查询计划映射到这几个表上时,Calcite会在查询执行时触发这些表去读取数据。接下来我们更深入地解析其中的细节步骤。
举个栗子(a model in JSON format):
{
version: '1.0',
defaultSchema: 'SALES',
schemas: [
{
name: 'SALES',
type: 'custom',
factory: 'org.apache.calcite.adapter.csv.CsvSchemaFactory',
operand: {
directory: 'target/test-classes/sales'
}
}
]
}
这个模型文件定义了一个库(schema)叫SALES,这个库是由一个插件类(a plugin class)支持的,org.apache.calcite.adapter.csv.CsvSchemaFactory这个是calcite-example-csv工程里interface SchemaFactory的一个实现。它的create方法将一个schema实例化了,将model file中的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);
}
根据模型(model)描述,库工程(schema factory)实例化了一个名为'SALES'的简单库(schema)。这个库(schema)是org.apache.calcite.adapter.csv.CsvSchema的实例并且实现了Calcite里的接口Schema。
一个库(schema)的主要职责就是创建一个表(table)的列表(库的职责还包括子库列表、函数列表等,但是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);
}
}
schema会扫描指定路径,找到所有以.csv/结尾的文件。在本例中,指定路径是 target/test-classes/sales,路径中包含文件'EMPS.csv'和'DEPTS.csv',这两个文件会转换成表EMPS和DEPTS。
表和视图
值得注意的是,我们在模型文件(model)里并不需要定义任何表,schema会自动创建的。 你可以额外扩展一些表(tables),使用这个schema中其他表的属性。
让我们看看如何创建一个重要且常用的一种表——视图。
在写一个查询时,视图就相当于一个table,但它不存储数据。它通过执行查询来生成数据。在查询转换为执行计划时,视图会被展开,所以查询执行器可以执行一些优化策略,例如移除一些SELECT子句中存在但在最终结果中没有用到的表达式。
举个栗子:
{
version: '1.0',
defaultSchema: 'SALES',
schemas: [
{
name: 'SALES',
type: 'custom',
factory: 'org.apache.calcite.adapter.csv.CsvSchemaFactory',
operand: {
directory: 'target/test-classes/sales'
},
tables: [
{
name: 'FEMALE_EMPS',
type: 'view',
sql: 'SELECT * FROM emps WHERE gender = 'F''
}
]
}
]
}
栗子中type:view这一行将FEMALE_EMPS定义为一个视图,而不是常规表或者是自定义表。注意通常在JSON文件里,定义view的时候,需要对单引号进行转义。
用JSON来定义长字符串易用性不太高,因此Calcite支持了一种替代语法。如果你的视图定义中有长SQL语句,可以使用多行来定义一个长字符串:
{
name: 'FEMALE_EMPS',
type: 'view',
sql: [
'SELECT * FROM emps',
'WHERE gender = 'F''
]
}
现在我们定义了一个视图(view),我们可以再查询中使用它就像使用普通表(table)一样:
sqlline> SELECT e.name, d.name FROM female_emps AS e JOIN depts AS d on e.deptno = d.deptno;
+--------+------------+
| NAME | NAME |
+--------+------------+
| Wilma | Marketing |
+--------+------------+
自定义表
自定义表是由用户定义的代码来实现定义的,不需要额外自定义schema。
继续举个栗子model-with-custom-table.json:
{
version: '1.0',
defaultSchema: 'CUSTOM_TABLE',
schemas: [
{
name: 'CUSTOM_TABLE',
tables: [
{
name: 'EMPS',
type: 'custom',
factory: 'org.apache.calcite.adapter.csv.CsvTableFactory',
operand: {
file: 'target/test-classes/sales/EMPS.csv.gz',
flavor: "scannable"
}
}
]
}
]
}
我们可以一样来查询表数据:
sqlline> !connect jdbc:calcite:model=target/test-classes/model-with-custom-table.json admin admin
sqlline> SELECT empno, name FROM custom_table.emps;
+--------+--------+
| EMPNO | NAME |
+--------+--------+
| 100 | Fred |
| 110 | Eric |
| 110 | John |
| 120 | Wilma |
| 130 | Alice |
+--------+--------+
上面的schema是通用格式,包含了一个自定义表org.apache.calcite.adapter.csv.CsvTableFactory,这个类实现了Calcite中的TableFactory接口。它在create方法里实例化了CsvScannableTable,将model文件中的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);
}
通常做法是实现一个自定义表(a custom table)来替代实现一个自定义库(a custom schema)。两个方法最后都会创建一个Table接口的实例,但是自定义表无需重新实现元数据(metadata)获取部分。(CsvTableFactory和CsvSchema一样,都创建了CsvScannableTable,但是自定表实现就不需要实现在文件系统里检索.csv文件。)
自定义表(table)要求开发者在model上执有多操作(开发者需要在model文件中显式指定每一个table和它对应的文件),同时也提供给了开发者更多的控制选项(例如,为每一个table提供不同参数)。
模型中的注释
注释使用语法 /* ... */ 和 //:
{
version: '1.0',
/* 多行
注释 */
defaultSchema: 'CUSTOM_TABLE',
// 单行注释
schemas: [
..
]
}
(注释不是标准JSON格式,但不会造成影响。)
使用查询计划来优化查询
目前来看表(table)实现和查询都没有问题,因为我们的表中并没有大量的数据。但如果你的自定义表(table)有,例如,有100列和100万行数据,你肯定希望用户在每次查询过程中不检索全量数据。你会希望Calcite通过适配器来进行衡量,并找到一个更有效的方法来访问数据。
这个衡量过程是一个简单的查询优化格式。Calcite是通过添加执行器规则(planner rules)来支持查询优化的。执行器规则(planner rules)通过在查询解析中寻找指定模式(patterns)(例如在某个项目中匹配到某种类型的table是生效),使用实现优化后的新节点替换寻找到节点。
执行器规则(planner rules)也是可扩展的,就像schemas和tables一样。所以如果你有一些存储下来的数据希望通过SQL访问它,首先需要定义一个自定义表或是schema,然后再去定义一些能使数据访问高效的规则。
为了查看效果,我们可以使用一个执行器规则(planner rules)来访问一个CSV文件中的某些子列集合。我们可以在两个相似的schema中执行同样的查询:
sqlline> !connect jdbc:calcite:model=target/test-classes/model.json admin admin
sqlline> explain plan for select name from emps;
+-----------------------------------------------------+
| PLAN |
+-----------------------------------------------------+
| EnumerableCalcRel(expr#0..9=[{inputs}], NAME=[$t1]) |
| EnumerableTableScan(table=[[SALES, EMPS]]) |
+-----------------------------------------------------+
sqlline> !connect jdbc:calcite:model=target/test-classes/smart.json admin admin
sqlline> explain plan for select name from emps;
+-----------------------------------------------------+
| PLAN |
+-----------------------------------------------------+
| EnumerableCalcRel(expr#0..9=[{inputs}], NAME=[$t1]) |
| CsvTableScan(table=[[SALES, EMPS]]) |
+-----------------------------------------------------+
这两个计划到底有什么不同呢?通过对比可以发现,在smart.json里只多了一行:
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新