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
里只多了一行:
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’
}
}
]
}
(foodmart
这个库对于使用 Mondrian OLAP的人再熟悉不过了,这是Mondrain的重要测试集之一,不了解的请点击传送门)
当前的一些限制:JDBC适配器(adapter
)目前仅支持下推表扫描(table scan
)操作;其他的的操作(filtering,joins,aggregations等等)在Calcite
中完成。我们的目的是将尽可能多的处理操作、语法转换、数据类型和内建函数下推到源数据系统。如果一个Calcite
查询来源于单独一个JDBC数据库中的表,从原则上来说整个查询都会下推到源数据系统中。如果表来源于多个JDBC数据源,或是一个JDBC和非JDBC的混合源,Calcite
会使用尽可能高效的分布式查询方法来完成本次查询。
(*译者注:从15年开始,我们设计的一块数据分析产品,就像达到类似的功能,但是最终以失败告终,整体的完成度远不及Calcite,而Calcite的历史库最远仅可以追溯到14年,感叹一下人家的开发水准,自叹弗如!!!)
克隆JDBC适配器(adapter
)
克隆JDBC适配器(adapter
)创造了一个混合数据系统。数据来源于JDBC数据库但在它第一次读取时会读取到内存表中。Calcite
基于内存表对查询进行评估,有效地实现了数据库的缓存。
例如:下面的模型(model
)就是从mysql的“footmart”库中读取信息的:
{
version: ‘1.0’,
defaultSchema: ‘FOODMART_CLONE’,
schemas: [
{
name: ‘FOODMART_CLONE’,
type: ‘custom’,
factory: ‘org.apache.calcite.adapter.clone.CloneSchema$Factory’,
operand: {
jdbcDriver: ‘com.mysql.jdbc.Driver’,
jdbcUrl: ‘jdbc:mysql://localhost/foodmart’,
jdbcUser: ‘foodmart’,
jdbcPassword: ‘foodmart’
}
}
]
}
另外一种技术是从当前已存在的schema
中构建一份克隆schema
。通过source
属性来引用之前已经在model
中定义过的schema
,如下:
{
version: ‘1.0’,
defaultSchema: ‘FOODMART_CLONE’,
schemas: [
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
org.apache.calcite.adapter.clone.CloneSchema$Factory’,
operand: {
jdbcDriver: ‘com.mysql.jdbc.Driver’,
jdbcUrl: ‘jdbc:mysql://localhost/foodmart’,
jdbcUser: ‘foodmart’,
jdbcPassword: ‘foodmart’
}
}
]
}
另外一种技术是从当前已存在的schema
中构建一份克隆schema
。通过source
属性来引用之前已经在model
中定义过的schema
,如下:
{
version: ‘1.0’,
defaultSchema: ‘FOODMART_CLONE’,
schemas: [
[外链图片转存中…(img-n9nx50u3-1714550941404)]
[外链图片转存中…(img-03qvpfAb-1714550941405)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!