Apache Calcite官方文檔中文版- 概覽-2. 教程

第一部分 概覽

2. 教程

  本章針對Calcite的連接建立提供了循序漸進的教程,使用一個簡單的適配器來將一個CSV文件目錄以包含Schema信息的tables形式呈現,並提供了一個完全SQL接口。
  Calcite-example-CSV是一個Calcite中的一個功能完備的適配器,它可以讀取CSV格式(以逗號分隔)的文本文件。值得稱讚的是,幾百行的java代碼就足夠提供完全的SQL查詢功能。
  CSV適配器同樣作爲一個其他數據格式的適配器構建參考模板。儘管代碼量不大,但它覆蓋了一些重要的概念:
1) 用戶通過使用SchemaFactory和Schema interfaces來自定義schema
2) 以JSON模型文件聲明schemas
3) 以JSON模型文件聲明視圖views
4) 通過Table interface自定義table
5) 定義table的record類型
6) 使用Scannable Table interface作爲Table的簡單實現,來直接枚舉所有的rows
7) 進階實現FilterableTable,來根據簡單的謂詞predicates過濾rows
8) 以Translatable Table進階實現Table,將關係型算子翻譯爲執行計劃規則

2.1 源碼搭建

版本依賴:Java (1.7 or higher; 1.8 preferred), git and maven (3.2.1 or later).

$ git clone https://github.com/apache/calcite.git
$ cd calcite
$ mvn install -DskipTests -Dcheckstyle.skip=true
$ cd example/csv

2.2 查詢測試

  可以通過工程內置的sqlline腳本來連接到Calcite

$ ./sqlline
sqlline> !connect jdbc:calcite:model=target/test-classes/model.json admin admin

(如果使用Windows操作系統,命令爲sqlline.bat)
執行一個metadata 查詢

sqlline> !tables
+------------+--------------+-------------+---------------+----------+------+
| TABLE_CAT  | TABLE_SCHEM  | TABLE_NAME  |  TABLE_TYPE   | REMARKS  | TYPE |
+------------+--------------+-------------+---------------+----------+------+
| null       | SALES        | DEPTS       | TABLE         | null     | null |
| null       | SALES        | EMPS        | TABLE         | null     | null |
| null       | SALES        | HOBBIES     | TABLE         | null     | null |
| null       | metadata     | COLUMNS     | SYSTEM_TABLE  | null     | null |
| null       | metadata     | TABLES      | SYSTEM_TABLE  | null     | null |
+------------+--------------+-------------+---------------+----------+------+

  JDBC提示:sqlline中的 !tables 命令實際上等於執行 DatabaseMetaData.getTables() , 還有其他命令來查詢JDBC metadata, 例如 !columns 和 !describe。
Apache Calcite官方文檔中文版- 概覽-2. 教程
Apache Calcite官方文檔中文版- 概覽-2. 教程
Apache Calcite官方文檔中文版- 概覽-2. 教程
Apache Calcite官方文檔中文版- 概覽-2. 教程Apache Calcite官方文檔中文版- 概覽-2. 教程
  如結果所示,該系統中共有5個table: SALES schema下的EMPS, DEPTS, HOBBIES 和系統自帶的 metadata schema下的COLUMNS和TABLES。系統table在Calcite中會一直展示,但其他表是由schema的指定實現而來,在本例中,EMPS和DEPTS表來源於target/test-classes路徑下的EMPS.csv和DEPTS.csv文件。
  通過在這些表上執行一些查詢,我們可以驗證Calcite提供了完整的SQL功能實現。
首先,scan table:

sqlline> SELECT * FROM emps;
+--------+--------+---------+---------+----------------+--------+-------+---+
| EMPNO  |  NAME  | DEPTNO  | GENDER  |      CITY      | EMPID  |  AGE  | S |
+--------+--------+---------+---------+----------------+--------+-------+---+
| 100    | Fred   | 10      |         |                | 30     | 25    | t |
| 110    | Eric   | 20      | M       | San Francisco  | 3      | 80    | n |
| 110    | John   | 40      | M       | Vancouver      | 2      | null  | f |
| 120    | Wilma  | 20      | F       |                | 1      | 5     | n |
| 130    | Alice  | 40      | F       | Vancouver      | 2      | null  | f |
+--------+--------+---------+---------+----------------+--------+-------+---+

Apache Calcite官方文檔中文版- 概覽-2. 教程
同時支持JOIN和GROUP BY

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

Apache Calcite官方文檔中文版- 概覽-2. 教程
  最後,VALUES操作符可以聚合生成單獨一行數據,我們可以通過這種簡便的方法來測試表達式和SQL內嵌函數:

sqlline> VALUES CHAR_LENGTH('Hello, ' || 'world!');
+---------+
| EXPR$0  |
+---------+
| 13      |
+---------+

Apache Calcite官方文檔中文版- 概覽-2. 教程
  Calcite具有其他許多SQL特性。我們來不及在這裏一一舉例,使用者可以編寫更多的查詢來進行驗證。

2.3 Schema發現

  現在,我們來探索一下Calcite是如何發現這些table的。記住,最核心的Calcite不知道CSV文件的任何信息。(像一個“沒有存儲層的databse”一樣,Calcite不會去了解任何文件格式)Calcite能識別這些table是因爲我們告訴它去運行calcite-example-csv工程下的代碼。
  運行鏈中有一系列的步驟。首先,我們在一個schema 工程類中以model file的格式定義一個schema。然後schema工廠類創建一個schema,schema創建多個table,這些table都知道如何通過scan CSV文件來獲取數據。最後,在Calcite解析完查詢並將查詢計劃映射到這幾個table上時,Calcite會在查詢執行時觸發這些table去讀取數據。接下來我們更深入地解析其中的細節步驟。
  在JDBC連接字符串中,我們會給出以JSON格式定義的model的路徑。model具體定義如下

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

Apache Calcite官方文檔中文版- 概覽-2. 教程
  這個model定義了一個名爲SALES的schema。這個schema是由一個plugin類支持的,org.apache.calcite.adapter.csv.CsvSchemaFactory,這個類是calcite-example-csv工程裏的一部分並且實現了Calcite中的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 工程類實例化了一個名爲SALES的schema。這個schema是org.apache.calcite.adapter.csv.CsvSchema的一個實例,實現了Calcite中的Schema接口。
  一個schema的職責是產生一系列的tables(它也可以列舉出子schema和table-function,但這些高級的特性在calcite-example-csv中沒有支持)。這些table實現了Calcite的Table接口。CsvSchema生成了一些tables,它們是CsvTable以及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,這兩個文件會轉換成EMPS和DEPTS表。

{
  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中定義單引號需要加上轉義字段“\”。使用JSON定義長字符串易用性不太高,因此Calcite支持一種替代語法。如果視圖定義中有長SQL語句,可以使用多行來定義一個長字符串。

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

  在定義完一個視圖之後,在查詢時可以完全將它作爲一個table使用

sqlline> !connect jdbc:calcite:model=target/test-classes/model-with-view.json admin admin
sqlline> SELECT e.name, d.name FROM female_emps AS e JOIN depts AS d on e.deptno = d.deptno;
+--------+------------+
|  NAME  |    NAME    |
+--------+------------+
| Wilma  | Marketing  |
+--------+------------+

2.5 自定義Tables

  自定義表是由用戶定義的代碼來實現定義的,不需要額外自定義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接口。它創建了一個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);
}

  實現自定義table通常是一個比實現自定義schema更容易的替代方法。這兩種方法最終都會創建類似的Table接口類的實現,但自定義表無需實現metadata discovery。(CsvTableFactory創建一個CsvScannableTable,就像CsvSchema一樣,但是table實現無需掃描整個文件系統來找到.csv類型的文件)。
  自定義table要求開發者在model上執行更多操作(開發者需要在model文件中顯式指定每一個table和它對應的文件),同時也提供給了開發者更多的控制選項(例如,爲每一個table提供不同參數)。

2.6 模型註釋

  model定義過程中可以通過//或者//符號來添加註釋

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

  Comments不是標準JSON格式,但不會造成影響。

2.7 使用執行計劃規則優化查詢

  目前我們看到的table實現和查詢都沒有問題,因爲table中不會包含大數據量。但如果自定義table數據量大,例如,一百列,100w行,你會希望用戶在每次查詢過程中不要檢索全量數據。你會希望Calcite通過適配器來進行衡量,並找到一個更有效的方法來訪問數據。
  衡量過程是一個簡單的查詢優化格式。Calcite支持通過添加計劃器規則來實現查詢優化。計劃器規則在查詢解析樹中匹配到對應規則時生效(例如在某個項目中匹配到某種類型的table時生效),而且計劃器規則是可擴展的,例如schemas和tables。因此,如果用戶希望通過SQL訪問某個數據集,首先需要定義一個自定義表或是schema,然後再去定義一些能使數據訪問高效的規則。
  爲了查看效果,我們可以使用一個計劃器規則來訪問一個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]])               |
+-----------------------------------------------------+

  兩次查詢的scan方式不同,EnumerableTableScan和CsvTableScan。
  是什麼導致了執行計劃的差異?讓我們來追查一下其中證據。在smart.json model 文件中,存在額外的一行:

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()來表示規則已經觸發成功了。

2.8 查詢優化流程

  關於Calcite的查詢計劃有多智能有許多種方法,但我們在這裏不會討論這個問題。最智能的部分是爲了減輕用戶負擔的優化器規則設計者。
  首先,Calcite不會按照規定的順序來執行規則。查詢優化處理過程是一個有很多分支的分支樹,就像國際象棋一樣會檢查很多可能的子操作(移動)。如果規則A和B同時滿足查詢操作樹的一個給定子集合,Calcite可以將它們同時執行。
  其次,Calcite在執行計劃樹的時候會使用基於代價的優化,但基於成本的模型並不會導致規則的執行,這在短期內看起來代價會更大。
  許多優化規則都有一個線性優化方案。在面對上面說的規則A和規則B的情況下,這樣的優化器需要立刻進行抉擇。可能會有一個策略,比如“在整棵樹上先執行規則A,然後在整棵樹上執行規則B”,或是執行基於代價的優化策略,執行能產生耗費更低的結果的規則。
  Calcite不需要這樣的妥協(哪樣???)。這能讓結合各種規則的操作更簡單。如果你希望結合規則來識別各種攜帶規則的物化視圖,去從CSV和JDBC源數據系統中讀取數據,需要給Calcite所有的規則並告訴它如何去做。
  Calcite使用了一個基於成本的優化模型,成本模型決定了最終使用哪個執行計劃,有時候爲了避免搜索空間的爆炸性增長會對搜索樹進行剪枝,但它絕不對強迫用戶在規則A和規則B之間進行選擇。這是很重要的一點,因爲它避免了在搜索空間中落入實際上不是最優的局部最優值。
  同樣,成本模型是可插拔(擴展)的,它是基於表和查詢操作的統計信息。這個問題稍後會仔細討論。

2.9 JDBC適配器

  JDBC適配器將JDBC數據源中的schema映射成了Calcite的schema模式。
例如,下面這個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'
      }
    }
  ]
}

(The FoodMart database will be familiar to those of you who have used the Mondrian OLAP engine, because it is Mondrian’s main test data set. To load the data set, follow Mondrian’s installation instructions.)

  當前的限制:JDBC適配器目前僅支持下推table scan操作;其他的的操作(filtering,joins,aggregations等等)在Calcite中完成。我們的目的是將儘可能多的處理操作、語法轉換、數據類型和內建函數下推到源數據系統。如果一個Calcite查詢來源於單獨一個JDBC數據庫中的表,從原則上來說整個查詢都會下推到源數據系統中。如果表來源於多個JDBC數據源,或是一個JDBC和非JDBC的混合源,Calcite會使用儘可能高效的分佈式查詢方法來完成本次查詢。

2.10 克隆JDBC適配器

  克隆JDBC適配器創造了一個混合源數據系統。數據來源於JDBC數據庫但在它第一次讀取時會讀取到內存表中。Calcite基於內存表對查詢進行評估,有效地實現了數據庫的緩存。
例如,下面的model從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'
      }
    }
  ]
}

  另外一種技術是從當前已存在的schema中構建一份clone schema。通過source屬性來引用之前已經在model中定義過的schema,如下:

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

  可以使用這種方法建立任意類型schema的clone schema,不僅限於JDBC.
  cloning adapter不是最重要的。我們計劃開發更復雜的緩存策略,和更復雜更有效的內存表的實現,但目前cloning JDBC adapter僅體現了這種可能性,並讓我們能開始嘗試初始實現。

2.11 更多主題

  There are many other ways to extend Calcite not yet described in this tutorial. The adapter specification describes the APIs involved.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章