探究Presto SQL引擎(1)-巧用Antlr
一、背景
自2014年大數(shù)據(jù)首次寫(xiě)入政府工作報(bào)告,大數(shù)據(jù)已經(jīng)發(fā)展7年。大數(shù)據(jù)的類型也從交易數(shù)據(jù)延伸到交互數(shù)據(jù)與傳感數(shù)據(jù)。數(shù)據(jù)規(guī)模也到達(dá)了PB級(jí)別。
大數(shù)據(jù)的規(guī)模大到對(duì)數(shù)據(jù)的獲取、存儲(chǔ)、管理、分析超出了傳統(tǒng)數(shù)據(jù)庫(kù)軟件工具能力范圍。在這個(gè)背景下,各種大數(shù)據(jù)相關(guān)工具相繼出現(xiàn),用于應(yīng)對(duì)各種業(yè)務(wù)場(chǎng)景需求。從Hadoop生態(tài)的Hive, Spark, Presto, Kylin, Druid到非Hadoop生態(tài)的ClickHouse, Elasticsearch,不一而足...
這些大數(shù)據(jù)處理工具特性不同,應(yīng)用場(chǎng)景不同,但是對(duì)外提供的接口或者說(shuō)操作語(yǔ)言都是相似的,即各個(gè)組件都是支持SQL語(yǔ)言。只是基于不同的應(yīng)用場(chǎng)景和特性,實(shí)現(xiàn)了各自的SQL方言。這就要求相關(guān)開(kāi)源項(xiàng)目自行實(shí)現(xiàn)SQL解析。在這個(gè)背景下,誕生于1989年的語(yǔ)法解析器生成器ANTLR迎來(lái)了黃金時(shí)代。
二、簡(jiǎn)介
ANTLR是開(kāi)源的語(yǔ)法解析器生成器,距今已有30多年的歷史。是一個(gè)經(jīng)歷了時(shí)間考驗(yàn)的開(kāi)源項(xiàng)目。一個(gè)程序從源代碼到機(jī)器可執(zhí)行,基本需要3個(gè)階段:編寫(xiě)、編譯、執(zhí)行。
在編譯階段,需要進(jìn)行詞法和語(yǔ)法的分析。ANTLR聚焦的問(wèn)題就是把源碼進(jìn)行詞法和句法分析,產(chǎn)生一個(gè)樹(shù)狀的分析器。ANTLR幾乎支持對(duì)所有主流編程語(yǔ)言的解析。從antlr/grammars-v4可以看到,ANTLR支持Java,C, Python, SQL等數(shù)十種編程語(yǔ)言。通常我們沒(méi)有擴(kuò)展編程語(yǔ)言的需求,所以大部分情況下這些語(yǔ)言編譯支持更多是供學(xué)習(xí)研究使用,或者用在各種開(kāi)發(fā)工具(NetBeans、Intellij)中用于校驗(yàn)語(yǔ)法正確性、和格式化代碼。
對(duì)于SQL語(yǔ)言,ANTLR的應(yīng)用廣度和深度會(huì)更大,這是由于Hive, Presto, SparkSQL等由于需要對(duì)SQL的執(zhí)行進(jìn)行定制化開(kāi)發(fā),比如實(shí)現(xiàn)分布式查詢引擎、實(shí)現(xiàn)各種大數(shù)據(jù)場(chǎng)景下獨(dú)有的特性等。
三、基于ANTLR4實(shí)現(xiàn)四則運(yùn)算
當(dāng)前我們主要使用的是ANTLR4。在《The Definitive ANTLR4 Reference》一書(shū)中,介紹了基于ANTLR4的各種有趣的應(yīng)用場(chǎng)景。比如:實(shí)現(xiàn)一個(gè)支持四則運(yùn)算的計(jì)算器;實(shí)現(xiàn)JSON等格式化文本的解析和提取;
將JSON轉(zhuǎn)換成XML;從Java源碼中提取接口等。本節(jié)以實(shí)現(xiàn)四則運(yùn)算計(jì)算器為例,介紹Antlr4的簡(jiǎn)單應(yīng)用,為后面實(shí)現(xiàn)基于ANTLR4解析SQL鋪平道路。實(shí)際上,支持?jǐn)?shù)字運(yùn)算也是各個(gè)編程語(yǔ)言必須具備的基本能力。
3.1 自行編碼實(shí)現(xiàn)
在沒(méi)有ANTLR4時(shí),我們想實(shí)現(xiàn)四則運(yùn)算該怎么處理呢?有一種思路是基于棧實(shí)現(xiàn)。例如,在不考慮異常處理的情況下,自行實(shí)現(xiàn)簡(jiǎn)單的四則運(yùn)算代碼如下:
代碼量不大,用到了數(shù)據(jù)結(jié)構(gòu)-棧的特性,需要自行控制運(yùn)算符優(yōu)先級(jí),特性上沒(méi)有支持括號(hào)表達(dá)式,也沒(méi)有支持表達(dá)式賦值。接下來(lái)看看使用ANTLR4實(shí)現(xiàn)。
3.2 基于ANTLR4實(shí)現(xiàn)
使用ANTLR4編程的基本流程是固定的,通常分為如下三步:
- 基于需求按照ANTLR4的規(guī)則編寫(xiě)自定義語(yǔ)法的語(yǔ)義規(guī)則, 保存成以g4為后綴的文件。
- 使用ANTLR4工具處理g4文件,生成詞法分析器、句法分析器代碼、詞典文件。
- 編寫(xiě)代碼繼承Visitor類或?qū)崿F(xiàn)Listener接口,開(kāi)發(fā)自己的業(yè)務(wù)邏輯代碼。
基于上面的流程,我們借助現(xiàn)有案例剖析一下細(xì)節(jié)。
第一步:基于ANTLR4的規(guī)則定義語(yǔ)法文件,文件名以g4為后綴。例如實(shí)現(xiàn)計(jì)算器的語(yǔ)法規(guī)則文件命名為L(zhǎng)abeledExpr.g4。其內(nèi)容如下:
(注:此文件案例來(lái)源于《The Definitive ANTLR4 Reference》)
簡(jiǎn)單解讀一下LabeledExpr.g4文件。ANTLR4規(guī)則是基于正則表達(dá)式定義定義。規(guī)則的理解是自頂向下的,每個(gè)分號(hào)結(jié)束的語(yǔ)句表示一個(gè)規(guī)則 。例如第一行:grammar LabeledExpr; 表示我們的語(yǔ)法名稱是LabeledExpr, 這個(gè)名字需要跟文件名需要保持一致。Java編碼也有相似的規(guī)則:類名跟類文件一致。
- 規(guī)則prog 表示prog是一個(gè)或多個(gè)stat。
- 規(guī)則stat 適配三種子規(guī)則:空行、表達(dá)式expr、賦值表達(dá)式 ID’=’expr。
- 表達(dá)式expr適配五種子規(guī)則:乘除法、加減法、整型、ID、括號(hào)表達(dá)式。很顯然,這是一個(gè)遞歸的定義。
最后定義的是組成復(fù)合規(guī)則的基礎(chǔ)元素,比如:
- 規(guī)則ID: [a-zA-Z]+表示ID限于大小寫(xiě)英文字符串;
- INT: [0-9]+; 表示INT這個(gè)規(guī)則是0-9之間的一個(gè)或多個(gè)數(shù)字,當(dāng)然這個(gè)定義其實(shí)并不嚴(yán)格。再嚴(yán)格一點(diǎn),應(yīng)該限制其長(zhǎng)度。
在理解正則表達(dá)式的基礎(chǔ)上,ANTLR4的g4語(yǔ)法規(guī)則還是比較好理解的。
定義ANTLR4規(guī)則需要注意一種情況,即可能出現(xiàn)一個(gè)字符串同時(shí)支持多種規(guī)則,如以下的兩個(gè)規(guī)則:
- ID: [a-zA-Z]+;
- FROM: ‘from’;
很明顯,字符串” from”同時(shí)滿足上述兩個(gè)規(guī)則,ANTLR4處理的方式是按照定義的順序決定。這里ID定義在FROM前面,所以字符串from會(huì)優(yōu)先匹配到ID這個(gè)規(guī)則上。
其實(shí)在定義好與法規(guī)中,編寫(xiě)完成g4文件后,ANTLR4已經(jīng)為我們完成了50%的工作:幫我們實(shí)現(xiàn)了整個(gè)架構(gòu)及接口了,剩下的開(kāi)發(fā)工作就是基于接口或抽象類進(jìn)行具體的實(shí)現(xiàn)。實(shí)現(xiàn)上有兩種方式來(lái)處理生成的語(yǔ)法樹(shù),其一Visitor模式,另一種方式是Listener(監(jiān)聽(tīng)器模式)。
3.2.1 使用Visitor模式
第二步:使用ANTLR4工具解析g4文件,生成代碼。即ANTLR工具解析g4文件,為我們自動(dòng)生成基礎(chǔ)代碼。流程圖示如下:
命令行如下:
命令執(zhí)行完成后,生成的文件如下:
首先開(kāi)發(fā)入口類Calc.java。Calc類是整個(gè)程序的入口,調(diào)用ANTLR4的lexer和parser類核心代碼如下:
接下來(lái)定義類繼承LabeledExprBaseVisitor類,覆寫(xiě)的方法如下:
從圖中可以看出,生成的代碼和規(guī)則定義是對(duì)應(yīng)起來(lái)的。例如visitAddSub對(duì)應(yīng)AddSub規(guī)則,visitId對(duì)應(yīng)id規(guī)則。以此類推…實(shí)現(xiàn)加減法的代碼如下:
相當(dāng)直觀。代碼編寫(xiě)完成后,就是運(yùn)行Calc。運(yùn)行Calc的main函數(shù),在交互命令行輸入相應(yīng)的運(yùn)算表達(dá)式,換行Ctrl+D即可看到運(yùn)算結(jié)果。例如1+3*4=13。
3.2.2 使用Listener模式
類似的,我們也可以使用Listener模式實(shí)現(xiàn)四則運(yùn)算。命令行如下:
該命令的執(zhí)行同樣會(huì)為我們生產(chǎn)框架代碼。在框架代碼的基礎(chǔ)上,我們開(kāi)發(fā)入口類和接口實(shí)現(xiàn)類即可。首先開(kāi)發(fā)入口類Calc.java。Calc類是整個(gè)程序的入口,調(diào)用ANTLR4的lexer和parser類代碼如下:
可以看出生成ParseTree的調(diào)用邏輯一模一樣。實(shí)現(xiàn)Listener的代碼略微復(fù)雜一些,也需要用到棧這種數(shù)據(jù)結(jié)構(gòu),但是只需要一個(gè)操作數(shù)棧就可以了,也無(wú)需自行控制優(yōu)先級(jí)。以AddSub為例:
直接從棧中取出操作數(shù),進(jìn)行運(yùn)算即可。
3.2.3 小結(jié)
關(guān)于Listener模式和Visitor模式的區(qū)別,《The Definitive ANTLR 4 Reference》一書(shū)中有清晰的解釋:
Listener模式:
Visitor模式
- Listener模式通過(guò)walker對(duì)象自行遍歷,不用考慮其語(yǔ)法樹(shù)上下級(jí)關(guān)系。Vistor需要自行控制訪問(wèn)的子節(jié)點(diǎn),如果遺漏了某個(gè)子節(jié)點(diǎn),那么整個(gè)子節(jié)點(diǎn)都訪問(wèn)不到了。
- Listener模式的方法沒(méi)有返回值,Vistor模式可以設(shè)定任意返回值。
- Listener模式的訪問(wèn)棧清晰明確,Vistor模式是方法調(diào)用棧,如果實(shí)現(xiàn)出錯(cuò)有可能導(dǎo)致StackOverFlow。
通過(guò)這個(gè)簡(jiǎn)單的例子,我們驅(qū)動(dòng)Antlr4實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的計(jì)算器。學(xué)習(xí)了ANTLR4的應(yīng)用流程。了解了g4語(yǔ)法文件的定義方式、Visitor模式和Listener模式。通過(guò)ANTLR4,我們生成了ParseTree,并基于Visitor模式和Listener模式訪問(wèn)了這個(gè)ParseTree,實(shí)現(xiàn)了四則運(yùn)算。
綜合上述的例子可以發(fā)現(xiàn),如果沒(méi)有ANTLR4,我們自行編寫(xiě)算法也能實(shí)現(xiàn)同樣的功能。但是使用ANTLR不用關(guān)心表達(dá)式串的解析流程,只關(guān)注具體的業(yè)務(wù)實(shí)現(xiàn)即可,非常省心和省事。
更重要的是,ANTLR4相比自行實(shí)現(xiàn)提供了更具想象空間的抽象邏輯,上升到了方法論的高度,因?yàn)樗呀?jīng)不局限于解決某個(gè)問(wèn)題,而是解決一類問(wèn)題??梢哉f(shuō)ANTLR相比于自行硬編碼解決問(wèn)題的思路有如數(shù)學(xué)領(lǐng)域普通的面積公式和微積分的差距。
四、參考Presto源碼開(kāi)發(fā)SQL解析器
前面介紹了使用ANTLR4實(shí)現(xiàn)四則運(yùn)算,其目的在于理解ANTLR4的應(yīng)用方式。接下來(lái)圖窮匕首見(jiàn),展示出我們的真正目的:研究ANTLR4在Presto中如何實(shí)現(xiàn)SQL語(yǔ)句的解析。
支持完整的SQL語(yǔ)法是一個(gè)龐大的工程。在presto中有完整的SqlBase.g4文件,定義了presto支持的所有SQL語(yǔ)法,涵蓋了DDL語(yǔ)法和DML語(yǔ)法。該文件體系較為龐大,并不適合學(xué)習(xí)探究某個(gè)具體的細(xì)節(jié)點(diǎn)。
為了探究SQL解析的過(guò)程,理解SQL執(zhí)行背后的邏輯,在簡(jiǎn)單地閱讀相關(guān)資料文檔的基礎(chǔ)上,我選擇自己動(dòng)手編碼實(shí)驗(yàn)。為此,定義一個(gè)小目標(biāo):實(shí)現(xiàn)一個(gè)SQL解析器。用該解析器實(shí)現(xiàn)select field from table語(yǔ)法,從本地的csv數(shù)據(jù)源中查詢指定的字段。
4.1 裁剪SelectBase.g4文件
基于同實(shí)現(xiàn)四則運(yùn)算器同樣的流程,首先定義SelectBase.g4文件。由于有了Presto源碼作為參照系,我們的SelectBase.g4并不需要自己開(kāi)發(fā),只需要基于Presto的g4文件裁剪即可。裁剪后的內(nèi)容如下:
相比presto源碼中700多行的規(guī)則,我們裁剪到了其1/10的大小。該文件的核心規(guī)則為: SELECT selectItem (',' selectItem)* (FROM relation (',' relation)*)?
通過(guò)理解g4文件,也可以更清楚地理解我們查詢語(yǔ)句的構(gòu)成。例如通常我們最常見(jiàn)的查詢數(shù)據(jù)源是數(shù)據(jù)表。但是在SQL語(yǔ)法中,我們查詢數(shù)據(jù)表被抽象成了relation。
這個(gè)relation有可能來(lái)自于具體的數(shù)據(jù)表,或者是子查詢,或者是JOIN,或者是數(shù)據(jù)的抽樣,或者是表達(dá)式的unnest。在大數(shù)據(jù)領(lǐng)域,這樣的擴(kuò)展會(huì)極大方便數(shù)據(jù)的處理。
例如,使用unnest語(yǔ)法解析復(fù)雜類型的數(shù)據(jù),SQL如下:
盡管SQL較為復(fù)雜,但是通過(guò)理解g4文件,也能清晰理解其結(jié)構(gòu)劃分?;氐絊electBase.g4文件,同樣我們使用Antlr4命令處理g4文件,生成代碼:
這樣就生成了基礎(chǔ)的框架代碼。接下來(lái)就是自行處理業(yè)務(wù)邏輯的工作了。
4.2 遍歷語(yǔ)法樹(shù)封裝SQL結(jié)構(gòu)信息
接下來(lái)基于SQL語(yǔ)法定義語(yǔ)法樹(shù)的節(jié)點(diǎn)類型,如下圖所示。
通過(guò)這個(gè)類圖,可以清晰明了看清楚SQL語(yǔ)法中的各個(gè)基本元素。
然后基于visitor模式實(shí)現(xiàn)自己的解析類AstBuilder (這里為了簡(jiǎn)化問(wèn)題,依然從presto源碼中進(jìn)行裁剪)。以處理querySpecification規(guī)則代碼為例:
通過(guò)代碼,我們已經(jīng)解析出了查詢的數(shù)據(jù)源和具體的字段,封裝到了QuerySpecification對(duì)象中。
4.3 應(yīng)用Statement對(duì)象實(shí)現(xiàn)數(shù)據(jù)查詢
通過(guò)前面實(shí)現(xiàn)四則運(yùn)算器的例子,我們知道ANTLR把用戶輸入的語(yǔ)句解析成ParseTree。業(yè)務(wù)開(kāi)發(fā)人員自行實(shí)現(xiàn)相關(guān)接口解析ParseTree。Presto通過(guò)對(duì)輸入sql語(yǔ)句的解析,生成ParseTree, 對(duì)ParseTree進(jìn)行遍歷,最終生成了Statement對(duì)象。核心代碼如下:
有了Statement對(duì)象我們?nèi)绾问褂媚兀拷Y(jié)合前面的類圖,我們可以發(fā)現(xiàn):
- Query類型的Statement有QueryBody屬性。
- QuerySpecification類型的QueryBody有select屬性和from屬性。
通過(guò)這個(gè)結(jié)構(gòu),我們可以清晰地獲取到實(shí)現(xiàn)select查詢的必備元素:
- 從from屬性中獲取待查詢的目標(biāo)表Table。這里約定表名和csv文件名一致。
- 從select屬性中獲取待查詢的目標(biāo)字段SelectItem。這里約定csv首行為title行。
整個(gè)業(yè)務(wù)流程就清晰了,在解析sql語(yǔ)句生成statement對(duì)象后,按如下的步驟:
s1: 獲取查詢的數(shù)據(jù)表以及字段。
s2: 通過(guò)數(shù)據(jù)表名稱定為到數(shù)據(jù)文件,并讀取數(shù)據(jù)文件數(shù)據(jù)。
s3: 格式化輸出字段名稱到命令行。
s4: 格式化輸出字段內(nèi)容到命令行。
為了簡(jiǎn)化邏輯,代碼只處理主線,不做異常處理。
代碼僅供演示功能,暫不考慮異常邏輯,比如查詢字段不存在、csv文件定義字段名稱不符合要求等問(wèn)題。
4.4 實(shí)現(xiàn)效果展示
在我們項(xiàng)目data目錄,存儲(chǔ)如下的csv文件:
cities.csv文件樣例數(shù)據(jù)如下:
運(yùn)行代碼查詢數(shù)據(jù)。使用SQL語(yǔ)句指定字段從csv文件中查詢。最終實(shí)現(xiàn)類似SQL查詢的效果如下:
SQL樣例1:select City, City from cities
SQL樣例2:select name, age from employee
本節(jié)講述了如何基于Presto源碼,裁剪g4規(guī)則文件,然后基于Antlr4實(shí)現(xiàn)用sql語(yǔ)句從csv文件查詢數(shù)據(jù)。依托于對(duì)Presto源碼的裁剪進(jìn)行編碼實(shí)驗(yàn),對(duì)于研究SQL引擎實(shí)現(xiàn),理解Presto源碼能起到一定的作用。
五、總結(jié)
本文基于四則運(yùn)算器和使用SQL查詢csv數(shù)據(jù)兩個(gè)案例闡述了ANTLR4在項(xiàng)目開(kāi)發(fā)中的應(yīng)用思路和過(guò)程,相關(guān)的代碼可以在github上看到。理解ANTLR4的用法能夠幫助理解SQL的定義規(guī)則及執(zhí)行過(guò)程,輔助業(yè)務(wù)開(kāi)發(fā)中編寫(xiě)出高效的SQL語(yǔ)句。同時(shí)對(duì)于理解編譯原理,定義自己的DSL,抽象業(yè)務(wù)邏輯也大有裨益。紙上得來(lái)終覺(jué)淺,絕知此事要躬行。通過(guò)本文描述的方式研究源碼實(shí)現(xiàn),也不失為一種樂(lè)趣。