Apache頂級專案ShardingSphere — SQL Parser的設計與實現

京東數科技術說發表於2020-12-16

導語:SQL作為現代計算機行業的資料處理事實標準,是目前最重要的資料處理介面之一,從傳統的DBMS(如MySQL、Oracle),到主流的計算框架(如spark,flink)都提供了SQL的解析引擎,因此想對sql進行精細化的操作,一定離不開SQL Parser。Apache ShardingSphere 是一套開源的分散式資料庫中介軟體解決方案組成的生態圈,需要對SQL進行精細化的操作,如改寫,加密等,因此也實現了SQL Parser,並提供獨立的Parser引擎。

先來認識一下傳統資料庫中一條SQL處理流程是怎樣的,接受網路包-》資料庫協議解析網路包的到sql-》SQL語法解析為抽象語法樹-》把語法樹轉換成關係代數表示式樹(邏輯執行計劃)-》再轉換成物理運算元樹(物理執行計劃)-》遍歷物理運算元樹執行相應運算元的實現獲取資料並返回。

一、工作原理

SQL Parser 的功能是把一條SQL解析為抽象語法樹(AST),SQL Parser需要編譯原理相關的知識,簡單介紹一下。

SQL Parser 包含詞法解析(Lexer)和語法解析(Parser),詞法解析的作用是把一個sql 分割成一個一個不可分割的單元,例子:

原始sql: select id,name from table1 where name=“xxx”;
詞法解析器輸入是原始sql,並且暴露一個介面nextToken(),每次呼叫nextToken()都會返回一個Token(表示上面所說的不可分割的元素),虛擬碼如下:

String originSql = "select id,name from table1 where name='xxx'";
Lexer lexer = new Lexer(originSql);
while(!lexer._hitEOF){// 判斷是否結束
  System.out.printLn(lexer.nextToken())
}
// 輸出如下:
select
id
,
name
from
table1
where
name
=
'xxx'

語法分析器的作用是使用Lexer的輸出(呼叫nextToken()),構造出AST,以上面的sql為例,解析器得出的語法樹如下:
在這裡插入圖片描述

1、Lexer(詞法分析)原理

Lexer 也稱為分詞,從左向右掃描SQL,將其分割成一個個的toke(不可分割的,具有獨立意義的單元,類似英語中的單詞)。

Lexer的實現一般都是構造DFA(確定性有限狀態自動機)來實現的,以一個例子說明。
狀態轉移圖如下,這是一個能夠識別識別符號,數字和一般運算子的詞法解析器。
在這裡插入圖片描述
程式碼實現可以使用傳統的while case的模板實現,虛擬碼如下:

Class Token{...}
Enum State {
    BEGIN,OPERATER,IDENTIFIER,NUMBER
}

Class Lexer{
    String input = "...";
    State state = BEGIN;
    int index = 0;
    Token nextToken() {
        while(index < input.length) {
            Char char = input.charAt(index);
            switch (state) {
                case BEGIN: 
                    switch (char){
                        case a-zA-Z_ :
                            index++;
                            state = IDENTIFIER;
                        case +-*/ :
                            state = BEGIN
                            return Token(OPERATER)
                        case 0-9:
                            state = NUMBER;
                            index++;
                        default:
                            return null;
                    }
                case IDENTIFIER:
                    switch (char){
                        case a-zA-Z_0-9 :
                            index++;
                            state = IDENTIFIER;
                        default :
                            state = BEGIN;
                            index--;
                            return Token(IDENTIFIER)
                    }
                case NUMBER:
                    switch (char){
                        case 0-9 :
                            index++;
                            state = NUMBER;
                        default :
                            state = BEGIN;
                            index--;
                            return Token(NUMBER)
                    }
                
                default:
                    return null;
            }
        }
    }
}

2、Parser(語法解析) 原理

Parser階段有兩種型別方法來實現,一種是自頂向下分析法,另一種是自底向上分析法,簡單介紹一下兩種型別分析法的處理思路。

首先給出上下文無關語法和相關術語的定義:

  • 終結符集合 T (terminal set)
    一個有限集合,其元素稱為 終結符(terminal)
  • 非終結符集合 N (non-terminal set)
    一個有限集合,與 T 無公共元素,其元素稱為 非終結符(non-terminal)
  • 符號集合 V (alphabet)
    T 和 N 的並集,其元素稱為符號(symbol) 。因此終結符和非終結符都是符號。符號可用字母:A, B, C, X, Y, Z, a, b, c 等表示。
  • 符號串(a string of symbols)
    一串符號,如 X1 X2 … Xn 。只有終結符的符號串稱為 句子(sentence)。空串 (不含任何符號)也是一個符號串,用 ε 表示。符號串一般用小寫字母 u, v, w 表示。
  • 產生式(production)
    一個描述符號串如何轉換的規則。對於上下文字無關語法,其固定形式為:A -> u ,其中 A 為非終結符, u 為一個符號串。
  • 產生式集合 P (production set)
    一個由有限個產生式組成的集合
  • 展開(expand)
    一個動作:將一個產生式 A -> u 應用到一個含有 A 的符號串 vAw 上,用 u 代替該符號串中的 A ,得到一個新的符號串 vuw。
  • 摺疊(reduce)
    一個動作:將一個產生式 A -> u 應用到一個含有 u 的符號串 vuw 上,用 A 代替該符號串中的 u ,得到一個新的符號串 vAw。
  • 起始符號 S (start symbol)
    N 中的一個特定的元素
  • 推導(derivate)
    一個過程:從一個符號串 u 開始,應用一系列的產生式,展開到另一個的符號串 v。若 v 可以由 u 推導得到,則可寫成:u => v 。
  • 上下文字無關語法 G (context-free grammar, CFG)
    一個 4 元組:(T, N, P, S) ,其中 T 為終結符集合, N 為非終結符集合, P 為產生式集合, S 為起始符號。一個句子如果能從語法 G 的 S 推導得到,可以直接稱此句子由語法 G 推導得到,也可稱此句子符合這個語法,或者說此句子屬於 G 語言。G 語言( G language) 就是語法 G 推匯出來的所有句子的集合,有時也用 G 代表這個集合。
  • 解析(parse)
    也稱為分析,是一個過程:給定一個句子 s 和語法 G ,判斷 s 是否屬於 G ,如果是,則找出從起始符號推導得到 s 的全過程。推導過程中的任何符號串(包括起始符號和最終的句子)都稱為 中間句子(working string)。

2.1 自頂向下分析法 LL(1)

LL(1) 是自頂向下分析法的一種,第一個L代表從左向右掃描待解析文字,第二個L代表從左向右展開,(1)代表每次讀取一個Token。

以簡單表示式為例子:

// 語法規則, 整個是一個產生式,左邊expr為非終結符,NUM是一
//個Token,為終結符
expr: NUM | NUM expr
// 待解析文字 
1 2 23 45

解析過程如下:
在這裡插入圖片描述
我們的目標是將起始符號expr展開成句子 1 2 23 45

  1. 對比expr和NUM(1),只能選擇expr -> NUM expr,才可以和NUM(1)匹配,展開後得NUM expr
  2. 忽略已匹配的NUM(1), 再次讀入NUM(2),只能選擇expr -> NUM expr,展開後得NUM expr
  3. 忽略已匹配的NUM(2),再次讀入NUM(23),只能選擇expr -> NUM expr,展開後得NUM expr
  4. 忽略已匹配的NUM(23),再次讀入NUM(45),只能選擇expr -> NUM,展開後得NUM
  5. 得到最終句子 NUM(1)NUM(2) NUM(23) NUM(45) 可接受,解析完成

注意點:為什麼1,2,3只能選擇expr -> NUM expr, Lexer中會有介面判斷是否得到末尾,ShardingSphere中介面是lexer._hitEOF。

2.1 自底向上分析法 LR(1)

自底向上分析的順序和自頂向下分析的順序相反,從給定的句子開始,不斷的挑選出合適的產生式,將中間句子中的子串摺疊為非終結符,最終摺疊到起始符號。

LR(1) 是自底向上分析法的一種,第一個L代表從左向右掃描待解析文字,第二個R代表從右向坐摺疊,(1)代表每次讀取一個Token。
以簡單表示式為例:

// 語法規則, 整個是一個產生式,左邊expr為非終結符,NUM是一
//個Token,為終結符
expr: NUM | expr NUM
// 待解析文字 
1 2 23 45

解析過程是如下:
在這裡插入圖片描述
我們的目標是把 1 2 23 45 摺疊為expr

  1. 讀入NUM(1),發現只能選擇expr -> NUM, 摺疊得expr
  2. 讀入NUM(2),只能選擇expr -> expr NUM,摺疊得expr
  3. 讀入NUM(23),只能選擇expr -> expr NUM, 摺疊得expr
  4. 讀入NUM(45),只能選擇expr-> expr NUM, 摺疊得expr 可接受,解析完成

二、ShardingSphere Parser 實現

實現Parser的方式一般分為兩種,一種是寫程式碼實現狀態機來進行解析,另一種是通過解析器生成器根據定義的語法規則生成解析器,ShardingSphere使用第二種方式,這是由於衡量了效能,擴充套件性和容易維護因素最終決定的。

以ShardingSphere中的MySQL 解析引擎為例,模組shardingsphere-sql-parser-mysql,語法定義路徑src/main/antlr。

在這裡插入圖片描述

功能點:

  • 提供獨立的SQL解析引擎
  • 可以方便的對語法規則進行擴充和修改
  • 提供SQL 變數引數化功能
  • 提供SQL 格式化功能
  • 支援多種方言
資料庫支援狀態
MySQL支援,完善
PostgreSQL支援,完善
SQLServer支援
Oracle支援
SQL92支援

使用方法:

//maven 依賴
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-sql-parser-engine</artifactId>
    <version>${project.version}</version>
</dependency>
// 根據需要引入指定方言的解析模組(以MySQL為例),可以新增所有支援的方言,也可以只新增使用到的
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-sql-parser-mysql</artifactId>
    <version>${project.version}</version>
</dependency>
//獲取語法樹
/**
 * databaseType type:String 可能值 MySQL,Oracle,PostgreSQL,SQL92,SQLServer
 * sql type:String 解析的SQL
 * useCache type:boolean 是否使用快取
 * @return parse tree
 */
ParseTree tree = new SQLParserEngine(databaseType).parse(sql, useCache); 
// 獲取SQLStatement
/**
 * databaseType type:String 可能值 MySQL,Oracle,PostgreSQL,SQL92,SQLServer
 * useCache type:boolean 是否使用快取
 * @return SQLStatement
 */
ParseTree tree = new SQLParserEngine(databaseType).parse(sql, useCache); 
SQLVisitorEngine sqlVisitorEngine = new SQLVisitorEngine(databaseType, "STATEMENT");
SQLStatement sqlStatement = sqlVisitorEngine.visit(tree);

三、常見解析器

1、MySQL Parser

詞法解析通過手寫程式碼的方式實現,語法解析通過定義語法規則,使用bison生成語法解析程式碼。

2、PostgreSQL Parser

通過定義語法規則,使用flex/bison生成詞法,語法解析程式碼。

3、TIDB Parser(能夠單獨使用, golang)

詞法解析通過手寫程式碼的方式實現,語法解析通過定義語法規則,使用goyacc生成語法解析程式碼。

4、ShardingSphere Parser(能夠單獨使用, 多種目標語言c,c++,java, golang,python)

  • 通過定義語法規則,使用ANTLR生成詞法,語法解析程式碼
  • 可以方便自定義語法
  • 提供快取機制
  • SQL 格式化,SQL引數化
  • 支援多種DB: Mysql, PostgreSQL, SQLServer, Oracle, SQL92
  • 支援自定義visitor

5、alibaba druid(能夠單獨使用,java)

  • 通過程式碼的方式實現詞法解析器和語法解析器
  • 支援多種DB: Mysql, PostgreSQL, SQLServer, Oracle, odps, db2, hive, SQL92
  • SQL 格式化,SQL引數化
  • 支援自定義visitor

6、Jsqlparser(能夠單獨使用, java)

  • 通過javacc定義語法解析規則實現
  • 不侷限於某一個DB

注意:

  1. 基本都是通過定義語法來實現的,詞法解析都是通過定義正則語言,構造有限狀態自動機實現,區別是LL(自頂向下)語法和LR(自底向上)語法。
  2. antlr使用的是LL(自頂向下)的語法規則 改進的LL(*)演算法,Bison和goyacc使用LR(自底向上)的語法規則,LALR演算法,Jsqlparser是LL(k), druid目測類似LL(k)。
  3. LR相比LL表達能力更強,典型的例子是antlr不支援相互左遞迴,bison支援。

四、AST(語法樹)應用

  • 轉化為邏輯執行計劃,邏輯執行計劃再轉換為物理執行計劃,物理執行計劃用於儲存引擎具體執行。

eg: select * from table1, table2 where table1.id=1 轉化為邏輯執行計劃為:

在這裡插入圖片描述

  • 通過遍歷語法樹,對SQL進行格式化,引數化等

參考文獻
[1] LL(*):https://www.antlr.org/papers/LL-star-PLDI11.pdf
[2] LALR:https://suif.stanford.edu/dragonbook/lecture-notes/Stanford-CS143/11-LALR-Parsing.pdf;http://www.cs.ecu.edu/karl/5220/spr16/Notes/Bottom-up/lalr.html
[3] TiDB優化器設計:https://pingcap.com/blog-cn/tidb-cascades-planner
[4] MySQL優化器設計:https://dev.mysql.com/doc/refman/8.0/en/cost-model.html

本文作者
陸敬尚,京東數科軟體工程師,Apache ShardingSphere Committer,熱愛開源,現專注於ShardingSphere 的開源建設和開發工作。


招聘資訊
京東數科長期招聘Apache ShardingSphere的開源工程師(點選閱讀原文可查詢崗位詳情),歡迎優秀的開源人才加入我們,共同打造出色的Apache頂級專案!簡歷投遞郵箱:zhangliang@apache.org。


往期好文推薦:
2020 ICDM 知識圖譜競賽獲獎技術方案
一文讀懂聯邦學習的前世今生(建議收藏)
突破DevOps瓶頸:京東數科自動化測試平臺建設實踐
京東數科七層負載 | HTTPS硬體加速 (Freescale加速卡篇)
京東數科mPaaS:深度解讀京東金融App(Android)的秒開優化實踐
在這裡插入圖片描述

相關文章