【Flink原理和應用】:Calcite應用指南

翻譯原文:http://calcite.apache.org/docs/tutorial.html

1.引言

Calcite作爲一個強大的SQL計算引擎,在Flink內部的SQL引擎模塊就是基於Calcite。但是目前Calcite的相關學習文檔還是太少了,沒有系統的學習資料。

2. 教程

這是一個循序漸進的教程,演示如何建立和連接Calcite。它使用一個簡單的適配器,使CSV文件的目錄看起來像一個包含表的schema。Calcite完成剩下的工作,並提供完整的SQL接口。

Calcite-example-CSV是Calcite的完全功能適配器,用於讀取CSV(逗號分隔值)格式的文本文件。值得注意的是,幾百行Java代碼足以提供完整的SQL查詢能力。

CSV 還可以用作構建其他數據格式適配器的模板。儘管代碼行不多,但它涵蓋了幾個重要概念:

  • 使用SchemaFactory和模式接口的用戶自定義Schema
  • 在模型JSON文件中聲明Schemas;
  • 在模型JSON文件中聲明視圖
  • 利用Table接口來自定義表
  • 確定表的記錄類型
  • 表的簡單實現,使用ScannableTable 接口,直接枚舉所有行
  • 一種更高級的實現,實現FilterableTable,並可以根據簡單的謂詞篩選出行
  • 使用TranslatableTable,該表使用計劃器規則轉換爲關係運算符

3. 下載和編譯

您需要Java(版本8, 9或10)和Git:

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

4. 首次查詢

現在讓我們使用sqlline連接到Calcite,這是一個包含在這個項目中的sql shell。

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

(如果運行的是Windows,則命令爲sqlline.bat)

執行元數據查詢:

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元數據的命令,比如 !columns!describe。)

如您所見,系統中有5個表:表EMPS, DEPTS和HOBBIES是在當前SALES schema當中; COLUMNS和TABLES是在系統元數據schema中。系統表始終存在於Calcite中,但其他表由schema的具體實現提供。在這種情況下,EMPS和DEPTS表基於target/test-classes目錄中的EMPS.csv和DEPTS.csv文件。

讓我們對這些表執行一些查詢,以顯示Calcite提供了SQL的完整實現。首先,表掃描:

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

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

最後,values運算符生成一行,是測試表達式和SQL內置函數的方便方法:

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

Calcite還有許多其他SQL特徵。我們沒有時間在這裏報道他們。再寫一些查詢進行實驗。

5. Schema發現

Calcite是怎麼找到這些tables的呢?記住,Calcite core對csv文件一無所知。作爲“沒有存儲層的數據庫”,Calcite不知道任何文件格式。Calcite知道這些表,因爲我們告訴它在calcite-example-csv項目中運行代碼。

在那條鏈上有幾個步驟。首先,我們基於schema文件中的schema工廠類定義schema。然後schema工廠創建一個schema,schema創建幾個表,每個表都知道如何通過掃描csv文件來獲取數據。最後,在Calcite解析了查詢並計劃使用這些表之後,Calcite在執行查詢時調用這些表來讀取數據。現在讓我們更詳細地看看這些步驟。

在jdbc connect字符串中,我們給出了json格式的模型路徑。模型如下:

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

該模型定義了一個名爲“SALES”的schema。該schema由org.apache.calcite.adapter.csv.CsvSchemaFactory類提供支持,該插件類是calcite-example-csv項目的一部分,實現了Calcite 接口SchemaFactory。它的create方法實例化一個schema,從模型文件傳入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);
}

在模型的驅動下,schema工廠實例化一個稱爲“SALES”的單一模式。該模式是org.apache.calcite.adapter.csv.CsvSchema的一個實例,實現了Calcite interface Schema。

schema的工作是生成表的列表。(它也可以列出子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);
  }
}

schema掃描directory並且發現那些.csv文件,並且爲它們創建表。在本案例中,directory是 target/test-classes/sales,然後包含了EMPS.csv和DEPTS.csv,這成爲了表EMPS和DEPTS。

6. schemas中的表和視圖

注意我們不需要在schema中定義任何表;schema會自動生成表。您可以使用schema的tables屬性定義額外的表,而不是自動創建的表。讓我們看看如何創建一個重要且有用的表類型,即視圖。

在編寫查詢時,視圖看起來像一個表,但它不存儲數據。它通過執行查詢來獲得結果。在計劃查詢的同時展開視圖,因此查詢規劃器通常可以執行優化,例如從select子句中刪除最終結果中未使用的表達式。

下面是定義了視圖的schema:

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

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

7. 自定義表

自定義表是由用戶自定義的代碼驅動其實現的表。它們不需要活在自定義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是常規schema,包含由org.apache.calcite.adapter.csv.CsvTableFactory,提供支持的自定義表,該表實現了Calcite接口TableFactory。其create方法實例化一個CsvScannableTable,從模型文件傳入文件參數:

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

實現自定義表通常是實現自定義schema的簡單替代方案。這兩種方法最終可能會創建一個類似的Table接口實現,但是對於自定義表,您不需要實現元數據發現。(CsvTableFactory與 CsvSchema一樣創建CsvScannableTable,但表實現不會掃描文件系統中的.csv文件。)

自定義表需要爲模型做更多的工作(作者需要顯式地指定每個表及其文件),但也給作者更多的控制(例如,爲每個表提供不同的參數)。

8. models裏的註釋

模型的註釋可以用/* ... *///語義:

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

(註釋不是標準JSON,但是是無害的擴展。)

9. 使用計劃器規則優化查詢

到目前爲止,只要表不包含大量數據,我們看到的表實現就可以了。但是,如果您的自定義表有一百列和一百萬行,那麼您希望系統不會檢索每個查詢的所有數據。您希望Calcite與適配器協商,並找到一種更有效的訪問數據的方法。

此協商是一種簡單的查詢優化形式。Calcite通過添加計劃規則支持查詢優化。規劃器規則通過在查詢解析樹中查找模式(例如某個表頂部的項目)來操作,並將樹中匹配的節點替換爲一組新的節點來實現優化。

規劃器規則也可以擴展,比如schemas和表。因此,如果您有一個要通過SQL訪問的數據存儲,那麼首先要定義一個自定義表或schema,然後定義一些規則來提高訪問效率。

要在實際操作中看到這一點,讓我們使用計劃器規則從csv文件訪問列的子集。讓我們針對兩個非常相似的schemas運行相同的查詢:

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"

這將導致使用flavor = TRANSLATABLE來創建CsvSchema ,其createtable方法創建 CsvTranslatableTable實例,而不是CsvScannableTable實例。

CsvTranslatableTable實現了TranslatableTable.toRel()方法來創建CsvTableScan。表掃描是查詢運算符樹的葉。通常的實現是 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()以指示規則已成功激發。

10. 查詢優化過程

關於Calcite的查詢計劃有多聰明,有很多話要說,但這裏我們不說。

首先,Calcite不會按規定的順序觸發規則。查詢優化過程遵循分支樹的許多分支,就像下棋程序檢查許多可能的移動序列一樣。如果規則A和B都匹配查詢運算符樹的給定部分,則Calcite可以同時觸發這兩個部分。

第二,Calcite使用成本模型來選擇計劃,但成本模型並不能阻止規則在短期內觸發似乎更昂貴的計劃。

許多優化器都有一個線性優化方案。面對規則A和規則B之間的選擇,如上所述,這樣的優化器需要立即進行選擇。它可能有一個策略,例如“將規則A應用於整棵樹,然後將規則B應用於整棵樹”,或者應用基於成本的策略,應用產生更便宜結果的規則。

Calcite不需要這樣的妥協。這使得組合各種規則集變得簡單。如果,假設您想結合規則來識別物化視圖(materialized views )和從CSV及JDBC源系統中讀取的規則,你只要給Calcite一套規則,讓它去做。

Calcite確實使用成本模型。成本模型決定最終使用哪一個計劃,有時會修剪搜索樹以防止搜索空間爆炸,但它從不強制您在規則A和規則B之間進行選擇。這一點很重要,因爲它可以避免陷入搜索空間中實際不是最佳的局部極小值。

另外(您猜對了),成本模型是可插拔的,它所基於的表和查詢操作符統計也是可插拔的。但這可能是以後的主題。

11. JDBC適配器

JDBC適配器將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'
      }
    }
  ]
}

當前限制:JDBC適配器當前只向下推表掃描操作;所有其他處理(過濾、聯接、聚合等)都發生在Calcite中。我們的目標是將儘可能多的處理推送到源系統,並在執行過程中轉換語法、數據類型和內置函數。如果Calcite查詢基於單個JDBC數據庫中的表,那麼原則上,整個查詢應該轉到該數據庫。如果表來自多個JDBC源,或者是JDBC和非JDBC的混合,Calcite將使用最有效的分佈式查詢方法。

12. 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'
      }
    }
  ]
}

另一種技術是在現有schema的基礎上構建克隆schema。使用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適配器展示了什麼是可能的,並允許我們嘗試最初的實現。

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