1.概述
最近,有同學留言關於SQL解析器方面的問題,今天筆者就為大家分享一下SQL解析器方便的一些內容。
2.內容
2.1 SQL解析器是什麼?
SQL解析與優化是屬於編輯器方面的知識,與C語言這類程式語言的解析上是類似的。SQL解析主要包含:詞法分析、語義語法分析、優化和執行程式碼生成、例如,我們非常熟悉的MySQL的一個SQL解析部分流程,如下圖所以:
這裡給大家介紹一下關於MySQL Lex和Bison生成的相關含義和具體負責的內容。
1.詞法分析
SQL解析由詞法分析和語法、語義分析兩個部分組成。詞法分析主要是把輸入轉化成若干個Token,其中Token包含key和非key。比如,一個簡單的SQL如下所示:
SELECT age FROM user
在分析之後,會得到4個Token,其中有2個key,它們分別是SELECT、FROM。
key | 非key | key | 非key |
SELECT | age | FROM | user |
通常情況下,詞法分析可以使用Flex來生成,但是我們熟悉的MySQL裡面並沒有使用該工具,而是手寫了詞法分析的部分(具體原因據說是為了效率和靈活性)。
MySQL在lex.h檔案中對key進行了定義,下面是部分的key:
{"&&", SYM(AND_AND_SYM)}, {"<", SYM(LT)}, {"<=", SYM(LE)}, {"<>", SYM(NE)}, {"!=", SYM(NE)}, {"=", SYM(EQ)}, {">", SYM(GE_SYM}, {">=", SYM(GE)}, {"<<", SYM(SHIFT_LEFT)}, {">>", SYM(SHIFT_RIGHT)}, {"<=>", SYM(EQUAL_SYM)}, {"ADD", SYM(ADD)}, {"AFTER", SYM(AFTER_SYM)}, {"AGGREGATE", SYM(AGGREGATE_SYM)}, {"ALL", SYM(ALL_SYM)},
2.語法分析
語法分析是生成語法樹的過程,這是整個解析過程中最核心、最複雜的環節。不過,這部分MySQL使用了Bison來實現,即使如此,如何設計合適的資料結構和相關演算法,以及儲存和遍歷所有的資訊,也是值得我們去研究的。
例如,如下SQL語句:
SELECT name,age from user where age > 20 and age < 25 and gender = 'F'
解析上述SQL時會生成如下語法數:
2.2 ANTLR VS Calcite ?
2.2.1 ANTLR
ANTLR 是一個功能強大的語法分析生成器,可以用來讀取、處理、執行和轉換結構化文字或者二進位制檔案。在大資料的一些SQL框架裡面有廣泛的應用,比如Hive的詞法檔案是ANTLR3寫的,Presto詞法檔案也是ANTLR4實現的,SparkSQL Lambda詞法檔案也是用Presto的詞法檔案改寫的,另外還有HBase的SQL工具Phoenix也是用ANTLR工具進行SQL解析的。
使用ANTLR來實現一條SQL,執行或者實現的過程大致如下:
- 實現詞法檔案(g4);
- 生成詞法分析器和語法分析器;
- 生成抽象語法數(AST);
- 遍歷AST;
- 生成語義樹;
- 優化生成邏輯執行計劃;
- 生成物理執行計劃再執行。
例項程式碼如下所示:
assign : ID '=' expr ';' ;
解析器的程式碼類似如下:
void assign(){ match(ID); match('='); expr(); match(); }
1.Parser
Parser是用來識別語言的程式,其本身包含兩個部分:詞法分析器和語法分析器。詞法分析階段主要解決的問題是key以及各種symbols,比如INT或者ID。語法分析主要是基於詞法分析的結果構造一顆語法分析樹,如下圖所示:
因此,為了讓詞法分析和語法能夠正常工作,在使用ANTLR4的時候,需要定義Grammar。
我們可以把CharStream轉換成一顆AST,CharStream經過詞法分析後會變成Token,TokenStream再最終組成一顆AST,其中包含TerminalNode和RuleNode,具體如下所示:
2.Grammar
ANTLR官方提供了很多常用的語言的語法檔案,可以進行膝蓋後直接進行使用:
https://github.com/antlr/grammars-v4
在使用語法的時候,需要注意以下事項:
- 語法名稱和檔名要一致;
- 語法分析器規則以小寫字母開始;
- 詞法分析器規則以大寫字母開始;
- 用'string'單引號引出字串;
- 不需要指定開始字元;
- 規則以分號結束;
- ...
3.例項分析
這裡我們使用IDEA來進行編寫,使用IDEA中的ANTLR4相關外掛來實現。然後建立一個Maven工程,在pom.xml檔案中新增如下依賴:
<dependency> <groupId>org.antlr</groupId> <artifactId>antlr4</artifactId> <version>4.9.3</version> </dependency>
然後,建立一個語法檔案,內容如下所示:
grammar Expr; prog : stat+; stat: expr NEWLINE # printExpr | ID '=' expr NEWLINE # assign | NEWLINE # blank ; expr: expr op=('*'|'/') expr # MulDiv | expr op=('+'|'-') expr # AddSub | INT # int | ID # id | '(' expr ')' # parens ; MUL : '*' ; DIV : '/' ; ADD : '+' ; SUB : '-' ; ID : [a-zA-Z]+ ; INT : [0-9]+ ; NEWLINE:'\r'? '\n' ; WS : [ \t]+ -> skip;
上述語法檔案很簡單,本質含義就是一個遞迴下降,即定義一個表示式(expr),可以迴圈呼叫,也可以直接呼叫其他表示式,但是最終肯定會有一個最核心的表示式不能再繼續往下呼叫了。以上語法檔案在真正執行的時候會生成一顆AST,然後在IDEA中執行“Test Rule ...”,並在執行後的測試框中輸入表示式“((1 + 2 ) + 3 - 4 * 5 ) / 6”,就會生成一顆AST了。AST如下圖所示:
整個語法檔案的目的是為了讓ANTLR生成相關的JAVA程式碼,我們設定生成visitor,然後,它們會生成如下檔案:
- ExprParser;
- ExprLexer;
- ExprBaseVisitor;
- ExprVisitor。
ExprLexer是詞法分析器,ExprParser是語法分析器。一個語言的解析過程一般是從詞法分析到語法分析。這是ANTLR4為我們生成的框架程式碼,而我們需要做的事情就是實現一個Visitor,一般從ExprBaseVisitor來繼承即可。生成的檔案如下所示:
然後,我編寫一個自定義的實現計算類,程式碼如下所示:
public class ExprCalcVistor extends ExprBaseVisitor{ public Integer visitAssign(ExprParser.AssignContext ctx) { String id = ctx.ID().getText(); Integer value = (Integer) visit(ctx.expr()); return value; } @Override public Integer visitInt(ExprParser.IntContext ctx) { return Integer.valueOf(ctx.INT().getText()); } @Override public Integer visitMulDiv(ExprParser.MulDivContext ctx) { Integer left = (Integer) visit(ctx.expr(0)); Integer right = (Integer) visit(ctx.expr(1)); if (ctx.op.getType() == ExprParser.MUL){ return left * right; }else{ return left / right; } } }
最後,執行主函式,程式碼如下所示:
public class ExprMain { public static void main(String[] args) throws IOException { ANTLRInputStream inputStream = new ANTLRInputStream("1 + 2 * 3"); ExprLexer lexer = new ExprLexer(inputStream); CommonTokenStream tokenStream = new CommonTokenStream(lexer); ExprParser parser = new ExprParser(tokenStream); ParseTree parseTree = parser.prog(); ExprCalcVistor visitor = new ExprCalcVistor(); Integer rtn = (Integer) visitor.visit(parseTree); System.out.println("result: " + rtn); } }
2.2.2 Calcite
上述ANTLR內容演示了詞法分析和語法分析的簡單流程,但是由於ANTLR要實現SQL查詢,需要自己定義詞法和語法相關檔案,然後再使用ANTLR的外掛對檔案進行編譯,然後再生成程式碼。
而Apache Calcite的出現,大大簡化了這些複雜工程,Calcite可以讓使用者很方便的給自己的系統套上一個SQL的外殼,並且提供足夠高效的查詢效能優化。
- query language
- query optimization
- query execution
- data management
- data storage
上述這五個功能,通常是資料庫系統包含的常用功能。Calcite在設計的時候就確定了自己只關注綠色的三個部分,而把下面資料管理和資料儲存留給了外部的儲存或者計算引擎。
資料管理和資料儲存,尤其是資料儲存是很複雜的,也會由於資料本身的特性導致實現上的多樣性。Calcite棄用這2部分的設計,而是專注於上層更加通用的模組,使得自己能夠足夠的輕量化,系統複雜性得到控制,開發人員的專注點不會耗費太多時間。
同時,Calcite也沒有去重複造輪子,能複用的東西,Calcite都會直接拿來複用。這也是讓開發者能夠去接受使用Calcite的原因之一,比如,如下例子:
- 示例1:作為一個SQL解析器,關鍵的SQL解析,Calcite沒有重複造輪子,而是直接使用了開源的JavaCC,來將SQL語句轉化為Java程式碼,然後進一步轉成AST以供下一階段使用;
- 示例2:為了支援後面會提到的靈活的後設資料功能,Calcite需要支援執行時編譯Java程式碼,預設的JavaC太重了,需要一個更加輕量級的編譯器,Calcite同樣沒有選擇造輪子,而是使用了開源的Janino方案。
上面的圖是Calcite官網給出的架構圖,從圖中我們可以知道,一方面印證了我們上面提到的,Calcite足夠的簡單,沒有做自己不改做的事情;另一方面,也是更重要的,Calcite被設計的足夠模組化和可插拔。
- JDBC Client:這個模組用來支援使用JDBC Client的應用
- SQL Parser and Validator:該模組用來做SQL解析和校驗
- Expressions Builder:用來支援自己做SQL解析和校驗的框架對接
- Operator Expressions:該模組用來處理關係表示式
- Metadata Provider:該模組用來支援外部自定義後設資料
- Pluggable Rules:該模組用來定義優化規則
- Query Optimizer:最核心的模組,專注於查詢優化
功能模組的規劃足夠合理,也足夠獨立,使得不用完整的整合,而是可以只選擇其中的一部分使用,而基本上每個模組都支援自定義,也使得使用者能夠更多的定製系統,如下表所示:
System | Query Language | JDBC Driver | SQL Parser and Validator | Execution Engine |
Apache Flink | Streaming SQL | √ | √ | Native |
Apache Hive | SQL+extensions | √ | √ | Tez, Spark |
Apache Drill | SQL+extensions | √ | √ | Native |
Apache Phoenix | SQL | √ | √ | HBase |
Apache Kylin | SQL | √ | √ | HBase |
... | ... | ... | ... | ... |
上面列舉的這些大資料常用的元件中Calcite均有整合,可以看到Hive就是自己做了SQL解析,只使用了Calcite的查詢優化功能,而像Flink則是從解析到優化都直接使用了Calcite。
上面介紹的Calcite整合方法,都是把Calcite的模組當作庫來使用,如果覺得太重量級,可以選擇更簡單的介面卡功能。通過類似Spark這些框架來自定義的Source或Sink方式,來實現和外部系統的資料互動操作。
Adapter | Target Language |
Cassandra | CQL |
Pig | Pig Latin |
Spark | RDD |
Kafka | Java |
... | ... |
上圖就是比較典型的介面卡用法,比如通過Kafka的介面卡就能直接在應用層通過SQL,而底層自動轉換成Java和Kafka進行資料互動。
1.pom依賴
<dependency> <groupId>org.smartloli</groupId> <artifactId>jsql-client</artifactId> <version>1.0.2</version> </dependency>
2.例項
public static void main(String[] args) throws Exception { JSONObject tabSchema = new JSONObject(); tabSchema.put("id", "integer"); tabSchema.put("name", "varchar"); tabSchema.put("age", "integer"); String tableName = "stu"; List<JSONArray> preRusult = new ArrayList<>(); JSONArray dataSets = new JSONArray(); for (int i = 0; i < 5000; i++) { JSONObject object = new JSONObject(); object.put("id", i); object.put("name", "aa" + i); object.put("age", 10 + i); dataSets.add(object); } preRusult.add(dataSets); String sql = "select count(*) as cnt from stu"; JSONObject result = JSqlUtils.query(tabSchema, tableName, preRusult, sql); System.out.println(result); }
3.Calcite實現KSQL查詢Kafka
Kafka Eagle實現了SQL查詢Kafka Topic中的資料,SQL操作Topic如下所示:
select * from efak_cluster_006 where `partition` in (0) limit 10
執行上圖SQL語句,截圖如下所示:
感興趣的同學,可以關注Kafka Eagle官網,或者原始碼。
4.結束語
這篇部落格就和大家分享到這裡,如果大家在研究學習的過程當中有什麼問題,可以加群進行討論或傳送郵件給我,我會盡我所能為您解答,與君共勉!
另外,博主出書了《Kafka並不難學》和《Hadoop大資料探勘從入門到進階實戰》,喜歡的朋友或同學, 可以在公告欄那裡點選購買連結購買博主的書進行學習,在此感謝大家的支援。關注下面公眾號,根據提示,可免費獲取書籍的教學視訊。