阿卡姆大数据科普报告——Calcite,2024大数据开发开发社招面试解答之性能优化

42 阅读2分钟

==

这是一个手把手式文档,教你如何构建并且连接到Calcite。我们用一个简单的适配器来将一个包含CSV文件的目录变成一个包含数据表的数据库(原文描述为schema)。Calcite可以提供一个完整的SQL接口。

Calcite-example-CSV是一个全功能适配器来使得Calcite可以读取CSV格式文件。可以通过几百行代码就能够完成一个全SQL查询功能。

CSV适配器可以作为抛砖引玉的模板套用到其他数据格式上。尽管他代码量不多,但是麻雀虽小五脏俱全,重要原理都包含其中:

  1. 使用SchemaFactorySchema interfaces来自定义schema

  2. 使用固定格式的JSON文件来(a model JSON file模型文件)声明数据库(schemas)

  3. 使用固定格式的JSON文件来(a model JSON file模型文件)声明视图(views)

  4. 使用Table interface来自定义表(Table)

  5. 确定表格的记录类型

  6. 使用ScannableTable interface来实现一个简单的表(Table),来枚举所有行(rows)

  7. 进阶实现FilterableTable,可以根据条件(simple predicates)来过滤数据

  8. 表的进阶实现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来连接Calcitesqlline是一个包含在整个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-1enUSen_USprimary" | -1          | null          | null           | 10             | 1        |         |            | null          | null             | -1                | 2               |

+-----------+-------------+------------+-------------+-----------+-----------+-------------+---------------+----------------+----------------+----------+---------+------------+---------------+------------------+-------------------+------------------+-------------+---------------+--------------+-------------+

你能看到,在执行!tables的时候有5个表,表EMPSDEPTSHOBBIESSALES库(schema)里,表COLUMNSTABLES在系统元数据库(system metadata schema)里。系统表总是在Calcite里显示,但其他表是由库(schema)的实现来指定的,在本例中,EMPSDEPTS表来源于target/test-classes路径下的EMPS.csvDEPTS.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',这两个文件会转换成表EMPSDEPTS

表和视图


值得注意的是,我们在模型文件(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)获取部分。(CsvTableFactoryCsvSchema一样,都创建了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)也是可扩展的,就像schemastables一样。所以如果你有一些存储下来的数据希望通过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里只多了一行:

img img img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取