Calcite基础入门(一)

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

$ git clone https://github.com/apache/calcite.git
$ cd calcite/example/csv
$ ./sqlline

使用sqlline连接calcite

$ ./sqlline
sqlline> !connect jdbc:calcite:model=src/test/resources/model.json admin admin

元数据查询

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 |         |          |            |           |                           |                |
+-----------+-------------+------------+--------------+---------+----------+------------+-----------+---------------------------+----------------+

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

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)

group by和join操作

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)

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

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

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

Schema discovery

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

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

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

{
  version: '1.0',
  defaultSchema: 'SALES',
  schemas: [
    {
      name: 'SALES',
      type: 'custom',
      factory: 'org.apache.calcite.adapter.csv.CsvSchemaFactory',
      operand: {
        directory: 'sales'
      }
    }
  ]
}

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

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

模式的工作是生成一个系列的表。(它也可以列出子模式和表函数,但这些是高级特性,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);
  }
}

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

模式中的表和视图

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

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

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

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

下面是定义视图的模式

{
  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\''
        }
      ]
    }
  ]
}

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

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

{
  name: 'FEMALE_EMPS',
  type: 'view',
  sql: [
    'SELECT * FROM emps',
    'WHERE gender = \'F\''
  ]
}

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

sqlline> SELECT e.name, d.name FROM female_emps AS e JOIN depts AS d on e.deptno = d.deptno;
+--------+------------+
|  NAME  |    NAME    |
+--------+------------+
| Wilma  | Marketing  |
+--------+------------+

该模式是一个常规模式,包含一个由org.apache.calcite.adapter.csv.CsvTableFactory提供的自定义表。该表实现了calcite接口TableFactory。它的create方法实例化了一个CsvScannableTable,从模型文件中传入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);
}

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

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

模型中的注释

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

{
  version: '1.0',
  /* Multi-line
     comment. */
  defaultSchema: 'CUSTOM_TABLE',
  // Single-line comment.
  schemas: [
    ..
  ]
}

使用规划器规则优化查询

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

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

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

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

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)

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

flavor: "translatable"

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

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

以下是整个规则:

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);
    }
}

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

public abstract class CsvRules {
  public static final CsvProjectTableScanRule PROJECT_SCAN =
      CsvProjectTableScanRule.Config.DEFAULT.toRule();
}

对默认配置(接口配置中的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 之间进行选择。 这很重要,因为它避免陷入局部最小值 在实际上不是最佳的搜索空间中。

此外(你猜对了)成本模型是可插入的,它所基于的表和查询运算符统计也是如此。 但这可能是以后的主题。

0 0 投票数
文章评分

本文为从大数据到人工智能博主「xiaozhch5」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://lrting.top/backend/351/

(0)
上一篇 2021-11-09 09:50
下一篇 2021-11-09 09:59

相关推荐

订阅评论
提醒
guest

0 评论
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x