阿卡姆大数据科普报告——Calcite

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里只多了一行:

flavor: “translatable”

这会让CsvSchema携带参数参数falvor = TRANSLATABLE 参数进行创建,并且它的createTable方法会创建CsvTranslatableTable,而不是CsvScannableTable.

CsvTranslatableTable实现了TranslatableTable.toRel()方法来创建CsvTableScan. 扫描表(Table scan)操作是查询执行树中的叶子节点,默认实现方式是EnumerableTableScan,但我们构造了一种不同的的子类型来让规则生效。

下面是完整的代码:

public class CsvProjectTableScanRule extends RelOptRule {

public static final CsvProjectTableScanRule INSTANCE =

new CsvProjectTableScanRule();

private CsvProjectTableScanRule() {

super(

operand(Project.class,

operand(CsvTableScan.class, none())),

“CsvProjectTableScanRule”);

}

@Override

public void onMatch(RelOptRuleCall call) {

final Project 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;

}

}

构造函数声明了能使规则生效的关系表达式匹配模式。

onMatch方法创了一个新的表达式并且执行RelOptRuleCall.transformTo()这个方法来通知规则执行成功。

查询优化流程


关于Calcite的查询计划有多智能有很多可以说的,但我们在这里不会讨论这个问题。最聪明的做法是为执行器规划的作者减轻负担( The cleverness is designed to take the burden off you, the writer of planner rules.)。

首先,Calcite不会按照规定的数据来执行.查询优化处理过程是一个有很多分支的分支树,就像国际象棋一样会检查很多可能的子操作。如果规则A和B同时满足查询操作树的一个给定子集合,Calcite可以将它们同时执行。

其次,Calcite在执行计划树的时候会使用基于代价的优化,但代价模型并不会阻止一些看起来短期代价更高的规则执行(Second, Calcite uses cost in choosing between plans, but the cost model doesn’t prevent rules from firing which may seem to be more expensive in the short term.)。

许多优化规则都有一个线性优化方案。在面对A或B的选择上,需要立刻做出决定。就好像有一个策略,比如“在整棵树上先执行规则A,然后在整棵树上执行规则B”,或是执行基于代价的优化策略,执行能产生耗费更低的结果的规则。

Calcite并不需要做出上述的妥协。这使得在处理多组合规则的情况更简单了。如果你希望结合规则来识别物化视图,去从CSV和JDBC源中读取数据,你只需要给Calcite所有的规则并告诉它如何去做。

Calcite使用了一个基于成本的优化模型,成本模型决定了最终使用哪个执行计划,有时候为了避免搜索空间的爆炸性增长会对搜索树进行剪枝,但它绝不对强迫用户在规则A和规则B之间进行选择。这是很重要的一点,因为它避免了在搜索空间中落入实际上不是最优的局部最优值。

同样,成本模型是可扩展的,它是基于表和查询操作的统计信息。这个问题稍后会仔细讨论。

JDBC适配器(adapter)


JDBC适配器(adapter)可以吧一个jdbc库(schema)映射成Calcite的库(schema)。

举个栗子,这是MySQL的一个经典库“foodmart”:

{

version: ‘1.0’,

defaultSchema: ‘FOODMART’,

schemas: [

{

name: ‘FOODMART’,

type: ‘custom’,

factory: ‘org.apache.calcite.adapter.jdbc.JdbcSchema$Factory’,

operand: {

jdbcDriver: ‘com.mysql.jdbc.Driver’,

jdbcUrl: ‘jdbc:mysql://localhost/foodmart’,

jdbcUser: ‘foodmart’,

jdbcPassword: ‘foodmart’

}

}

]

}

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

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

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

举个栗子,这是MySQL的一个经典库“foodmart”:

{

version: ‘1.0’,

defaultSchema: ‘FOODMART’,

schemas: [

{

name: ‘FOODMART’,

type: ‘custom’,

factory: ‘org.apache.calcite.adapter.jdbc.JdbcSchema$Factory’,

operand: {

jdbcDriver: ‘com.mysql.jdbc.Driver’,

jdbcUrl: ‘jdbc:mysql://localhost/foodmart’,

jdbcUser: ‘foodmart’,

jdbcPassword: ‘foodmart’

}

}

]

}

[外链图片转存中…(img-n5wDEtD7-1714340877514)]
[外链图片转存中…(img-DtKbcmaT-1714340877515)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

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

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 14
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值