前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Calcite基础入门(一)

Calcite基础入门(一)

作者头像
从大数据到人工智能
发布2022-01-18 21:55:00
2.2K0
发布2022-01-18 21:55:00
举报
文章被收录于专栏:大数据-BigData

这是一个循序渐进的教程,展示了如何构建和连接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

代码语言:javascript
复制
$ git clone https://github.com/apache/calcite.git
$ cd calcite/example/csv
$ ./sqllineCopy

使用sqlline连接calcite

代码语言:javascript
复制
$ ./sqlline
sqlline> !connect jdbc:calcite:model=src/test/resources/model.json admin adminCopy

元数据查询

代码语言:javascript
复制
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 |         |          |            |           |                           |                |
+-----------+-------------+------------+--------------+---------+----------+------------+-----------+---------------------------+----------------+Copy

从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的完整实现。首先,表扫描:

代码语言:javascript
复制
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)Copy

group by和join操作

代码语言:javascript
复制
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)Copy

最后,VALUES操作符生成一行,这是测试表达式和SQL内置函数的一种方便的方法:

代码语言:javascript
复制
0: jdbc:calcite:model=src/test/resources/mode> VALUES CHAR_LENGTH('Hello, ' || 'world!');
+--------+
| EXPR$0 |
+--------+
| 13     |
+--------+
1 row selected (0.067 seconds)Copy

calcite还有许多其他SQL特性。我们这里没时间讲了。你可以编写更多的查询来进行试验。

Schema discovery

Calcite是怎么找到这些表格的?记住,核心Calcite不知道任何关于CSV文件。(作为一个“没有存储层的数据库”,Calcite不知道任何文件格式。)Calcite知道这些表,因为我们告诉它运行Calcite-example-csv项目中的代码。

这个流程有几个步骤。首先,我们基于模型文件中的模式工厂类定义一个模式。然后模式工厂创建一个模式,该模式创建几个表,每个表都知道如何通过扫描CSV文件获取数据。最后,在Calcite解析了查询并计划使用这些表之后,Calcite在执行查询时调用这些表来读取数据。现在让我们更详细地看看这些步骤。

在JDBC连接字符串上,我们以JSON格式给出了模型的路径。模型如下:

代码语言:javascript
复制
{
  version: '1.0',
  defaultSchema: 'SALES',
  schemas: [
    {
      name: 'SALES',
      type: 'custom',
      factory: 'org.apache.calcite.adapter.csv.CsvSchemaFactory',
      operand: {
        directory: 'sales'
      }
    }
  ]
}Copy

该模型定义了一个名为“SALES”的模式。该模式由插件类org.apache.calcite.adapter.csv.CsvSchemaFactory提供支持。该插件是calcite-example-csv项目的一部分,并实现Calcite接口SchemaFactory。它的create方法实例化一个模式,传入模型文件中的directory参数:

代码语言:javascript
复制
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);
}Copy

在模型的驱动下,模式工厂实例化一个名为“SALES”的模式。该模式是org.apache.calcite.adapter.csv.CsvSchema的一个实例,实现了Calcite接口schema。

模式的工作是生成一个系列的表。(它也可以列出子模式和表函数,但这些是高级特性,calcite-example-csv不支持它们。)这些表实现了calcite的Table接口。CsvSchema生成的表是CsvTable及其子类的实例。

下面是来自CsvSchema的相关代码,它覆盖AbstractSchema基类中的getTableMap()方法。

代码语言:javascript
复制
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);
  }
}Copy

模式扫描目录并查找所有文件名以“.csv”结尾的文件,并为它们创建表。在本例中,目录是sales并包含文件EMPS.csv和DEPTS.csv,这些文件成为表EMPS和DEPTS。

模式中的表和视图

注意,我们不需要在模型中定义任何表;模式自动生成表。

除了自动创建的表之外,还可以使用模式的tables属性定义额外的表。

让我们看看如何创建一个重要和有用的表类型,即视图。

当您编写查询时,视图看起来像一个表,但它不存储数据。它通过执行查询来获得结果。在规划查询时,视图会展开,因此查询规划器通常可以执行优化,比如从SELECT子句中删除最终结果中没有使用的表达式。

下面是定义视图的模式

代码语言:javascript
复制
{
  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\''
        }
      ]
    }
  ]
}Copy

行类型:’view’将FEMALE_EMPS标记为视图,而不是常规表或自定义表。注意,视图定义中的单引号使用反斜杠进行转义,这是JSON的正常方式。

JSON并不容易生成长字符串,所以Calcite支持另一种语法。如果你的视图有一个很长的SQL语句,你可以提供一个行列表而不是一个字符串:

代码语言:javascript
复制
{
  name: 'FEMALE_EMPS',
  type: 'view',
  sql: [
    'SELECT * FROM emps',
    'WHERE gender = \'F\''
  ]
}Copy

现在我们已经定义了一个视图,我们可以在查询中使用它,就像它是一个表一样:

代码语言:javascript
复制
sqlline> SELECT e.name, d.name FROM female_emps AS e JOIN depts AS d on e.deptno = d.deptno;
+--------+------------+
|  NAME  |    NAME    |
+--------+------------+
| Wilma  | Marketing  |
+--------+------------+Copy

该模式是一个常规模式,包含一个由org.apache.calcite.adapter.csv.CsvTableFactory提供的自定义表。该表实现了calcite接口TableFactory。它的create方法实例化了一个CsvScannableTable,从模型文件中传入file参数:

代码语言:javascript
复制
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);
}Copy

实现自定义表通常比实现自定义模式更简单。这两种方法最终可能会创建一个类似的Table接口实现,但是对于自定义表,您不需要实现元数据发现。(CsvTableFactory创建了一个CsvScannableTable,就像CsvSchema一样,但是表实现并不扫描文件系统中的.csv文件。)

定制表需要模型的作者做更多的工作(作者需要显式地指定每个表及其文件),但也给作者更多的控制(比如,为每个表提供不同的参数)。

模型中的注释

包括在/ / 或者//中,例如:

代码语言:javascript
复制
{
  version: '1.0',
  /* Multi-line
     comment. */
  defaultSchema: 'CUSTOM_TABLE',
  // Single-line comment.
  schemas: [
    ..
  ]
}Copy

使用规划器规则优化查询

到目前为止,我们看到的表实现都很好,只要表不包含大量的数据。但是,如果您的客户表有100列和100万行,您宁愿系统不为每个查询检索所有数据。您希望calcite与适配器协商,并找到更有效的访问数据的方法。

这种协商是查询优化的一种简单形式。calcite通过添加规划器规则支持查询优化。Planner规则的操作方法是在查询解析树中寻找模式(例如某种表上的项目),并用一组实现优化的新节点替换树中匹配的节点。

规划器规则也是可扩展的,就像模式和表一样。因此,如果您有一个希望通过SQL访问的数据存储,您首先需要定义一个自定义表或模式,然后定义一些规则以使访问更加有效。

要查看实际效果,让我们使用规划器规则访问CSV文件中的列子集。让我们对两个非常相似的模式运行相同的查询:

代码语言:javascript
复制
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)
Copy

是什么导致了计划上的差异?让我们跟着证据的线索走。smart.json模型文件,有一个额外的行:

代码语言:javascript
复制
flavor: "translatable"Copy

这将导致使用flavor = TRANSLATABLE创建一个CsvSchema,它的createTable方法将创建CsvTranslatableTable的实例,而不是CsvScannableTable。

CsvTranslatableTable实现了TranslatableTable.toRel()方法来创建CsvTableScan。表扫描是查询操作符树的叶子。通常的实现是EnumerableTableScan,但是我们创建了一个独特的子类型,它将触发规则。

以下是整个规则:

代码语言:javascript
复制
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);
    }
}
Copy

规则的默认实例驻留在CsvRules的持有者类中:

代码语言:javascript
复制
public abstract class CsvRules {
  public static final CsvProjectTableScanRule PROJECT_SCAN =
      CsvProjectTableScanRule.Config.DEFAULT.toRule();
}Copy

对默认配置(接口配置中的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://cloud.tencent.com/developer/article/1936476

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-11-,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 开始尝试calcite
  • Schema discovery
  • 模式中的表和视图
  • 模型中的注释
  • 使用规划器规则优化查询
  • 查询优化过程
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档