SQL解析器詳解

哥不是小蘿莉發表於2022-01-31

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,執行或者實現的過程大致如下:

  1. 實現詞法檔案(g4);
  2. 生成詞法分析器和語法分析器;
  3. 生成抽象語法數(AST);
  4. 遍歷AST;
  5. 生成語義樹;
  6. 優化生成邏輯執行計劃;
  7. 生成物理執行計劃再執行。

例項程式碼如下所示:

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大資料探勘從入門到進階實戰》,喜歡的朋友或同學, 可以在公告欄那裡點選購買連結購買博主的書進行學習,在此感謝大家的支援。關注下面公眾號,根據提示,可免費獲取書籍的教學視訊。

相關文章