使用 Calcite 實現一個簡單的資料庫
Calcite
說到Calcite你可能有些陌生,但提及Hive、Kylin、Apache Drill、Flink等一定不會陌生,這些都是在我們日常工作中經常用到的。如上這些都是基於Calcite實現查詢引擎,還有Druid和Storm也是使用它來實現SQL功能。按照官方的說法,Calcite是動態資料管理框架,這個解釋理解起來有點抽象,通俗一點講,要使用Calcite實現資料庫,只需要關注儲存引擎以及元資料管理,其他都交給Calcite。可能這個說法有些不嚴謹, http://calcite.apache.org/docs/tutorial.html 中還提到了Calcite不提供處理資料的演算法,但Calcite-core和Calcite-linq都提供了一些運算元的實現,對於一個簡單的資料庫足夠了。
對於Calcite的詳細介紹推薦大家看一篇 文章(下方原文連結) ,本文主要介紹Calcite如何使用。例如,已經有一種資料格式的檔案儲存,如何利用Calcite快速實現SQL查詢。我看過kylin、druid的Calcite應用,也是各不相同,這大概也是Calcite的魅力吧。
全表掃描的資料庫
Calcite文件有一個指南,介紹使用CSV File作為資料儲存格式實現SQL查詢,掌握了以後我們可以照貓畫虎造出一個其他資料格式的資料庫,或者對學習kylin、druid的原始碼有幫助,概括地講,在這個例子使用以下技巧:
-
自定義Schema
-
自定義Table
-
決定Table的欄位型別
-
使用ScannableTable實現簡單的全表掃描
-
更高階一點的技巧,使用Filterable Table實現謂詞下推
-
更酷一點的技巧,基於TranslateTable使用Rule實現邏輯表示式的轉換
前四點是構建一個簡單的、採用全表掃描的方式實現查詢。5和6屬於進階內容,在案例中,使用Rule轉換的方式實現了Project下推,和5實現的謂詞下推是常用的SQL優化方式。下面由淺到深介紹這幾項技巧。先來看前四項,完成一個簡單的只能全表掃描的資料庫。
開始吧
一、儲存格式&元資料
首先 在GitHub上下載Calcite的原始碼,看calcite-example-csv工程,在src/test/CSVTest中有各種場景的測試用例,例如
-
testFilterableWhere是測試謂詞下推
-
testPushDownProject是Project下推
-
testSelect是最簡單的全表掃描
可以先跑一下測試用例感受一下Calcite的魅力,Calcite實現一個數據庫,只需要關注儲存引擎以及元資料管理。儲存格式採用csv,一個CSV檔案會對映成一個Table,需要注意的是CSV檔案的第一行是Table的元資料資訊,採用 “FieldName1:FieldType,FieldNameN:FieldType” 這樣的格式儲存,類似excel中的表頭資訊。以下是sales/SALES.csv的示例。
DEPTNO:int,NAME:string 10,"Sales" 20,"Marketing" 30,"Accounts"
二、搭建步驟
說在前面
建立一個json格式的mode檔案,描述瞭如何建立Schema,可以參照test/resource目錄下的model.json,
{ "version": "1.0", "defaultSchema": "SALES", "schemas": [ { "name": "SALES", "type": "custom", "factory": "org.apache.calcite.adapter.csv.CsvSchemaFactory", "operand": { "directory": "sales" } } ] }
在分析model檔案之前,先了解幾個重要的概念:
-
Schema,是table和function的名稱空間,它是一個可巢狀的結構,Schema還可以有subSchema,理論上可以無限巢狀,但一般不會這麼做。Schema可以理解成Database,Database下面有table,這樣就和傳統資料庫的概念聯絡起來了,在Calcite中,頂層的Schema是root,自定義的Schema是root的subSchema,同時還可以設定defaultSchema,類似我們使用資料庫時,使用use database命令以後就不用再輸入database名字字首。
-
Table,就很好理解了,就是資料庫中的表。在table描述了欄位名以及相應的型別、表的統計資訊,例如表有多少條記錄等等,這裡先不展開講。另外重要的是資料檔案的儲存以及如何掃描讀取資料檔案。
再來看這份model檔案,就比較清晰了。它描述了在資料庫中有多少個Schema、每個Schema如何建立以及預設的Schema,這裡的Schema可以理解成database。defaultSchema屬性設定預設Schema,schemas是陣列型別,每一項代表一個Schema描述資訊,在描述資訊中有一個關鍵的屬性factory,它是建立Schema的工廠類,在這個例子中factory是org.apache.calcite.adapter.csv.CsvSchemaFactory,它實現了SchemaFactory介面。
正題
要自實現只有全表掃描功能的簡單資料庫需要做如下幾步:
-
自定義 SchemaFactory
-
自定義 Schema
-
自定義 Table
-
自定義 Enumerator
① SchemaFactory 介面,它只有一個方法:
Schema create( SchemaPlus parentSchema, String name, Map<String, Object> operand);
create用於建立 Schema ,其引數說明如下:
-
parentSchema,他的父節點,一般為root
-
name schema的名字,它在model中定義的
-
operand,也是在mode中定義的,是Map型別,用於傳入自定義引數。
在這個Model中,CSVSchemaFactory建立一個叫“SALES”的CSVSchema,它會把src/test/resources/sales下所有CSV檔案構建成table。所以operand只許設定了一個引數directory,即讀取CSV檔案的根目錄。CSVSchemaFactory的實現比較簡單所以就不在展開分析,需要注意是的原始碼中flavor引數的處理,這個引數涉及優化進階相關,這裡先不用管,預設為SCANNABLE。
② 自定義Schema 需要實現Schema介面,前面提過Schema是table和function的名稱空間,其主要方法如下:
-
Table getTable(String name),根據表名獲取Table
-
Set <String> getTableNames(),獲取Schema下的所有表名集合
-
Collection <Function> getFunctions(String name),根據函式名獲取函式列表,和table不同,這裡返回的是集合型別。
-
Set <String> getFunctionNames(),或者所有的函式名集合。
CsvSchema->AbstractSchema->Schema,AbstractSchema重新設計了一個getTableMap方法,使用tableName->Table的Map結構儲存所有table。這樣設計的好處是getTable()能夠快速查詢。CSVSchema的實現也比較簡單,遍歷讀取根目錄下的每個檔案建立成表,因為上面的model.json中flavor沒有設定,採用預設值SCANNABLE,建立成CsvScannableTable。
③ 自定義Table 是本文中最複雜的,先看下圖:
如圖可知,CSVScannableTable主要實現了兩個介面ScannableTable和Table。右邊部分,CSVTable實現了Table介面,它的作用是定義Table的欄位以及欄位型別,左側的ScannableTable是實現如何遍歷讀取CSV檔案的資料。Table介面有如下三個方法:
-
RelDataType getRowType(RelDataTypeFactory typeFactory); 這個方法就是定義Table行記錄的欄位以及欄位型別。
-
Statistic getStatistic(); 獲取統計資訊
-
Schema.TableType getJdbcTableType(); table的型別,table的型別有很多種,例如table和view。
AbstractTable預設實現了getStatistic和getJdbcTableType,所以我們只需要實現getRowType方法。首先需要定義type,規範我們這個資料庫支援的資料型別。例如字串是採用String還是VarChar,具體實現在CsvFieldType列舉類,它內部維護了一個Map結構用來儲存type的
STRING(String.class, "string"), BOOLEAN(Primitive.BOOLEAN), BYTE(Primitive.BYTE), CHAR(Primitive.CHAR), //只列舉部分型別
由如上程式碼可知,type並不都是標準的SQL Type,例如String。Calcite中設計了RelDataTypeFactory,不僅支援標準的SQL TYPE,也支援java型別以及Array、Map等集合型別。該例項中,RowType是一個StructType,是集合型別,類似c語言中的struct,非常適合儲存行記錄中欄位名以及型別,這和Hive的方式是一樣的。例如SALES檔案中的
DEPTNO:int,NAME:string
則Type為
struct<DEPTNO:int,NAME:string>
在這個例子中通過讀取csv檔案的第一行來獲取fieldName以及fieldType的,具體實現在CsvEnumerator的deduceRowType()方法。在calcite中一般有兩種執行模型,解釋和編譯,這一點類似Java。編譯模式更好理解一些,會把邏輯執行計劃通過位元組碼技術生成java code然後編譯執行。解釋模式則省掉生成程式碼編譯的過程。 關於解釋執行 。我看過一些基於Calcite的應用,大部分還是採用編譯模式的,所以你看完這篇文章以後再去看其他使用calicite的專案,可能找不到熟悉的身影,如果table實現瞭如下三個介面之一,Calcite則會使用解釋模式執行
-
ScannableTable
-
FilterableTable
-
ProjectableFilterableTable
ScannableTable用於簡單的全表掃描,FilterableTable用於謂詞下推,ProjectableFilterableTable更酷一些既能支援謂詞下推又能支援project下推。他們都有一個scan,但是引數不同
-
ScannableTable
Enumerable<Object[]> scan(DataContext root);
-
FilterableTable
Enumerable<Object[]> scan(DataContext root, List<RexNode> filters);
因為要做謂詞下推,比ScannableTable多了filters。filters是where語句中的filter。
-
ProjectableFilterableTable
Enumerable<Object[]> scan(DataContext root, List<RexNode> filters, int[] projects);
又增加了projects,投影欄位順序的陣列。
④ Enumerable 支援linq和java的迭代器
//返回java的迭代器 Iterator<T> it = enumerable.iterator(); //LINQ風格的迭代器 Enumerator<T> enumerator =enumerable.enumerator();
要使用這兩種迭代器之前,必須要實現它!AbstractEnumerable藉助Linq4j實現了enumerator和iterator的轉換
public Iterator<T> iterator() { return Linq4j.enumeratorIterator(enumerator()); }
所以我們僅需實現enumerator方法。
Enumerator是Linq風格的迭代器,它有4個方法:
-
current()
-
moveNext()
-
reset()
-
close()
current返回遊標所指的當前記錄,需要注意的是current並不會改變遊標的位置,這一點和iterator是不同的,在iterator相對應的是next方法,每一次呼叫都會將遊標移動到下一條記錄,current則不會,Enumerator是在呼叫moveNext方法時才會移動遊標。moveNext方法將遊標指向下一條記錄,並獲取當前記錄供current方法呼叫,如果沒有下一條記錄則返回false。
CsvEnumerator是讀取csv檔案的迭代器,它還得需要一個RowConverter,因為csv中都是String型別,使用RowConverter轉化成相應的型別。在moreNext方法中,有Stream和謂詞下推filter部分的實現,在本文只關注如下幾行程式碼:
final String[] strings = reader.readNext(); if (strings == null) { current = null; return false; } current = rowConverter.convertRow(strings); return true;
至此,我們完成了使用csv檔案儲存的資料庫全部工作,你可以在CsvTest中使用所有的名為“model”的模型進行測試,
checkSql("model", "select * from EMPS"); //smart模型的會在後續的文中介紹 checkSql("smart", "select name from EMPS");
總結一下
-
建立模型,model.json
-
自定義SchemaFactory,CsvSchemaFactory
-
自定義Schema,CsvSchema
-
自定義Table,CsvTable、CsvScannableTable
-
自定義Enumerator,CsvEnumerator
分享的過程也是學習的過程,在寫本文過程,也瞭解了不少以前自以為懂了的細節,但也有可能還存在不正確的認識,歡迎指正交流。
參照資料:
-
http://calcite.apache.org/docs/tutorial.html
-
http://www.infoq.com/cn/articles/new-big-data-hadoop-query-engine-apache-calcite
-
http://events.linuxfoundation.org/sites/events/files/slides/ApacheCon2016ChristianTzolov.v4.pdf
你還可能感 興趣的 文 章