Calcite官网教程学习笔记(未完待续)

一、构建

  1. 官网教程学习链接
  2. 环境:macos,包管理工具gradle 6.3
  3. 根据这篇文章,https://blog.csdn.net/weixin_44112790/article/details/114284434?ops_request_misc=&request_id=&biz_id=102&utm_term=calcite%E5%AE%98%E6%96%B9%E6%95%99%E7%A8%8B&utm_medium=distribute.pc_search_result.none-task-blog-2blogsobaiduweb~default-0-114284434.185v2control&spm=1018.2226.3001.4450,选择代码版本,git reset --hard 68b02df ,而非选择官网版本
  4. 在此基础上构建成功,可以使用sqlline命令行

这是一个分步教程,展示了如何构建和连接到 Calcite。它使用一个简单的适配器,使一个包含若干CSV 文件的目录,看起来像是一个包含tables的schema。在这之后, Calcite 完成其余的工作,并提供完整的 SQL 接口。

Calcite-example-CSV 是 Calcite 的功能齐全的适配器,可读取 CSV格式的文本文件。值得注意的是,几百行 Java 代码足以提供完整的 SQL 查询能力。

这个CSV适配器还用作构建其他数据格式的适配器的模板。尽管代码行数不多,但它涵盖了几个重要概念:

  • 使用 SchemaFactory 和 Schema 接口实现的的用户定义schema;
  • 在模型 JSON 文件中声明Schema;
  • 在模型 JSON 文件中声明视图views;
  • 使用 Table 接口的用户定义表;
  • 确定表的记录类型;
  • Table 的简单实现,使用 ScannableTable 接口,直接枚举所有行;
  • 一个更高级的实现,实现了FilterableTable,并且可以根据简单的谓词过滤掉行;
  • Table 的高级实现,使用 TranslatableTable,使用规划器规则转换为关系运算符。

构建教程略

研究Schema
Calcite是如何找到这些表格(在某个文件中)的?请记住,核心 Calcite 对 CSV 文件一无所知。 (作为“没有存储层的数据库”,Calcite 不知道任何文件格式。)Calcite 知道这些表,因为我们告诉它运行 calcite-example-csv 项目中的代码。

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

在 JDBC 连接字符串(即!connect jdbc:calcite:model=src/test/resources/model.json admin admin)上,我们以 JSON 格式给出了模型的路径(笔者:jdbc是java数据库连接,比如连接mysql需要描述数据库地址信息,这里的model可以类比连接数据库需要的信息)。这是模型。

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

该模型定义了一个名为“SALES”的schema。该模式由插件类 org.apache.calcite.adapter.csv.CsvSchemaFactory 提供支持,它是 calcite-example-csv 项目的一部分,实现了 Calcite 接口 SchemaFactory(笔者,Schema工厂方法,提供了各种数据格式的Schema实现方式)。它的 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);
} // 笔者:注意,direction定义了schema的地址,和table的实现方式SCANNABLE

所以根据模型定义,schema工厂实例化了一个名为“SALES”的模式。该模式是 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.
  // 构建映射,从表名到JsonTable
  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. */
// flavor指明了表的创建方式,这里给出了多种创建表的方式
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扫描目录,找到具有适当扩展名的所有文件,并为它们创建表。在本例中,目录是 sales,包含文件 EMPS.csv.gz、DEPTS.csv 和 SDEPTS.csv,这些文件成为 EMPS、DEPTS 和 SDEPTS 表。

schemas里的表和视图

注意我们不需要在模型中定义任何表(笔者:模型中只定义了schema名称、目录和CsvSchemaFactory);Schema自动生成表。

除了自动创建的表之外,您还可以使用schema的 tables 属性定义其他类型的表。 让我们看看如何创建一个重要且有用的表类型,即视图views。

当您编写查询时,视图就像一张表,但它不存储数据。它通过执行查询得出其结果(它可以看作一个查询的中间结果)。在计划查询时会扩展视图,因此查询计划程序通常可以执行优化,例如从 SELECT 子句中删除最终结果中未使用的表达式。

这是一个定义了view的schema:

{
  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\'' // 视图SQL
        }
      ]
    }
  ]
}

这就可以直接在查询中使用视图名称了,就像存在这张表一样

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: 'sales/EMPS.csv.gz', // 这里指定了表的地址,而不是像之前指出文件夹地址,然后适配器扫描生成表
            flavor: "scannable" // 扫描
          }
        }
      ]
    }
  ]
}

在这样自定义申明的表里面可以照常查询。

sqlline> !connect jdbc:calcite:model=src/test/resources/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是一个常规schema,包含一个由 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 文件。)

自定义表需要模型的作者做更多的工作(作者需要明确指定每个表及其文件),但也给了作者更多的控制权(例如,为每个表提供不同的参数)。

模型定义中的注释略

使用执行计划优化查询
到目前为止,只要表不包含大量数据,我们看到的表实现就很好。但是,如果您的客户表有一百列和一百万行,您肯定希望系统不要去检索每个查询的所有数据。您希望 Calcite 与适配器协商并找到更有效的数据访问方式。
这种协商就是查询优化的一种简单形式。 Calcite 通过添加规划器规则来支持查询优化。规划器规则通过在查询解析树中查找模式(例如,在某类表之上的项目),并用一组实现优化了的新节点替换树中的匹配节点。
规划器规则也是可扩展的,就像模式和表。因此,如果您有一个想要通过 SQL 访问的数据存储,您首先定义一个自定义表或模式,然后定义一些规则以提高访问效率。
要查看实际情况,让我们使用规划器规则来访问 CSV 文件中的列子集。让我们对两个非常相似的模式运行相同的查询:

sqlline> !connect jdbc:calcite:model=src/test/resources/model.json admin admin
sqlline> explain plan for select name from emps;
+-----------------------------------------------------+
| PLAN                                                |
+-----------------------------------------------------+
| EnumerableCalc(expr#0..9=[{inputs}], NAME=[$t1])    |
|   EnumerableTableScan(table=[[SALES, EMPS]])        |
+-----------------------------------------------------+
sqlline> !connect jdbc:calcite:model=src/test/resources/smart.json admin admin
sqlline> explain plan for select name from emps;
+-----------------------------------------------------+
| PLAN                                                |
+-----------------------------------------------------+
| CsvTableScan(table=[[SALES, EMPS]], fields=[[1]])   |
+-----------------------------------------------------+

是什么导致了计划的差异?让我们跟随证据的踪迹。在 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();
}

在默认配置(interface Config 中的 DEFAULT 字段)中调用 withOperandSupplier 方法声明了关系表达式模式,将导致规则执行。如果规划器看到一个唯一输入是没有输入的 CsvTableScan 的 LogicalProject,它将调用该规则。 规则的变体是可能的。例如,不同的规则实例可能会匹配 CsvTableScan 上的 EnumerableProject。 onMatch 方法生成一个新的关系表达式并调用 RelOptRuleCall.transformTo() 以指示规则已成功触发。

查询优化过程

关于 Calcite 的查询计划器有多聪明,有很多话要说,但我们不会在这里说。聪明的设计是为了减轻你的负担。
首先,calcite不会以规定的顺序触发规则。查询优化过程遵循分支树的许多分支,就像下棋程序检查许多可能的移动序列一样。如果规则 A 和 B 都匹配查询运算符树的给定部分,则 Calcite 可以同时触发。

其次,calcite在选择计划时考虑成本,但成本模型并不能阻止触发在短期内似乎代价更大的规则。

许多优化器具有线性优化方案。面对规则 A 和规则 B 之间的选择,如上所述,这样的优化器需要立即进行选择。它可能有一个策略,例如“将规则 A 应用于整棵树,然后将规则 B 应用于整棵树”,或者应用基于成本的策略,应用产生更便宜结果的规则。

calcite不需要这样的协调。这使得组合各种规则集变得简单。如果,假设您想将识别物化视图的规则与从 CSV 和 JDBC 源系统读取的规则结合起来,您只需为 Calcite 提供所有规则集并告诉它执行。

Calcite 确实使用成本模型。成本模型决定最终使用哪个计划,有时会修剪搜索树以防止搜索空间爆炸,但它从不强迫您在规则 A 和规则 B 之间进行选择。这很重要,因为它避免陷入局部最小值在实际上不是最优的搜索空间中。

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

JDBC适配器
JDBC适配器将JDBC数据源(笔者:我们在model里定义)的schema映射作为calcite的schema。

以下时一个读取了MySQL数据库的schema:

{
  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 引擎的人来说应该很熟悉,因为它是 Mondrian 的主要测试数据集。要加载数据集,请按照 Mondrian 的安装说明进行操作。)

当前限制:JDBC适配器目前只下推表扫描操作(我理解是扫描元数据?);所有其他处理(过滤、连接、聚合等)都在 Calcite 中进行。我们的目标是将尽可能多的处理下推到源系统,同时翻译语法、数据类型和内置函数。如果 Calcite 查询基于单个 JDBC 数据库中的表,则原则上整个查询都应该转到该数据库。如果表来自多个 JDBC 源,或者 JDBC 和非 JDBC 的混合,Calcite 将使用它可以使用的最有效的分布式查询方法。

克隆JDBC适配器
克隆 JDBC 适配器创建一个混合数据库。数据来自 JDBC 数据库,但在第一次访问每个表时被读入内存表。 Calcite 根据这些内存表评估查询,实际上是数据库的缓存。 例如,以下模型从 MySQL “foodmart”数据库中读取表:

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

另一种技术是在现有模式之上构建克隆模式。您可以使用 source 属性来引用模型中之前定义的模式,如下所示:

{
  version: '1.0',
  defaultSchema: 'FOODMART_CLONE',
  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'
      }
    },
    {
      name: 'FOODMART_CLONE',
      type: 'custom',
      factory: 'org.apache.calcite.adapter.clone.CloneSchema$Factory',
      operand: {
        source: 'FOODMART'
      }
    }
  ]
 }

您可以使用这种方法在任何类型的模式上创建克隆模式,而不仅仅是 JDBC。 克隆适配器并不是万能的。我们计划开发更复杂的缓存策略,以及更完整和更高效的内存表实现,但现在克隆 JDBC 适配器展示了什么是可能的,并允许我们尝试我们的初始实现。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值