文法用來描述語言的語法規則,因此不只能夠用在編程語言上,也可用在漢語、英語上。mysql
咱們將一塊語法規則稱爲 產生式,使用 「Left → Right」 表示任意產生式,用 「Left => Right」 表示產生式的推導過程,好比對於產生式:git
E → i
E → E + E
複製代碼
咱們進行推導時,能夠這樣表示:E => E + E => i + E => i + i + E => i + i + igithub
也有使用 Left : Right 表示產生式的例子,好比 ANTLR。BNF 範式經過 Left ::= Right 表示產生式。sql
舉個例子,好比 SELECT * FROM table
能夠被表達爲:編程
S → SELECT * FROM table
複製代碼
固然這是最固定的語法,真實場景中,*
可能被替換爲其餘單詞,而 table
不但可能有其餘名字,還多是個子表達式。bash
通常用大寫的 S 表示文法的開頭,稱爲開始符號。編程語言
下面爲了方便書寫,使用 BNF 範式表示文法。函數
終結符就是語句的終結,讀到它表示產生式分析結束,相反,非終結符就是一個新產生式的開始,好比:性能
<selectStatement> ::= SELECT <selectList> FROM <tableName>
<selectList> ::= <selectField> [ , <selectList> ]
<tableName> ::= <tableName> [ , <tableList> ]
複製代碼
全部 ::=
號左邊的都是非終結符,因此 selectList
是非終結符,解析 selectStatement
時遇到了 selectList
將會進入 selectList
產生式,而解析到普通 SELECT
單詞就不會繼續解析。spa
對於有二義性的文法,能夠經過 上下文相關文法 方式描述,也就是在產生式左側補全條件,解決二義性:
aBc -> a1c | a2c
dBe -> d3e
複製代碼
通常產生式左側都是非終結符,大寫字母是非終結符,小寫字母是終結符。
上面表示,非終結符 B
在 ac
之間時,能夠解析爲 1
或 2
,而在 de
之間時,解析爲 3
。但咱們能夠增長一個非終結符讓產生式可讀性更好:
B -> 1 | 2
C -> 3
複製代碼
這樣就將上下文相關文法轉換爲了上下文無關文法。
根據是否依賴上下文,文法分爲 上下文相關文法 與 上下文無關文法,通常來講 上下文相關文法 均可以轉換爲一堆 上下文無關文法 來處理,而用程序處理 上下文無關文法 相對輕鬆。
SQL 的文法就是上下文相關文法,在正式介紹 SQL 文法以前,舉一個簡單的例子,好比咱們描述等號(=)的文法:
SELECT
CASE
WHEN bee = 'red' THEN 'ANGRY'
ELSE 'NEUTRAL'
END AS BeeState
FROM bees;
SELECT * from bees WHERE bee = 'red';
複製代碼
上面兩個 SQL 中,等號先後的關鍵字取決於當前是在 CASE WHEN
語句裏,仍是在 WHERE
語句裏,因此咱們認爲等號所在位置的文法是上下文相關的。
可是當咱們將文法粒度變細,將 CASE WHEN
與 WHERE
區塊分別交由兩塊文法解決,將等號這個通用的表達式抽離出來,就能夠不關心上下文了,這種方式稱爲 上下文無關文法。
附上一個 mysql 上下文無關文法集合。
上面提到的推導符號 =>
在實際運行過程當中,顯然有兩種方向左和右:
E + E => ?
複製代碼
從最左邊的 E 開始分析,稱爲左推導,對語法解析來講是自頂向下的方式,經常使用方法是遞歸降低。
從最右邊的 E 開始分析,稱爲右推導,對語法解析來講是自底向上的方式,經常使用方法是移進、規約。
右推導過程比左推導過程複雜,因此若是考慮手寫,最好使用左推導的方式。
好比 select <selectList>
的 selectList
產生式,它能夠表示爲:
<SelectList> ::= <SelectList> , <SelectField>
| <SelectField>
複製代碼
因爲它能夠展開:SelectList => SelectList , a => SelectList , b, a => c, b, a。
但程序執行時,讀到這裏會進入死循環,由於 SelectList 能夠被無限展開,這就是左遞歸問題。
消除左遞歸通常經過轉化爲右遞歸的方式,由於左遞歸徹底不消耗 Token,而右遞歸能夠經過消耗 Token 的方式跳出死循環。
Token 見上一期精讀 精讀《手寫 SQL 編譯器 - 詞法分析》
<SelectList> ::= <SelectField> <G>
<G> ::= , <SelectList>
| null
複製代碼
這實際上是一個通用處理,能夠抽象出來:
E → E + F
E → F
複製代碼
E → FG
G → + FG
G → null
複製代碼
不過咱們也不難發現,經過通用方式消除左遞歸後的文法更難以閱讀,這是由於用死循環的方式解釋問題更容易讓人理解,但會致使機器崩潰。
筆者建議此處不要生硬的套公式,在套了公式後,再對產生式作一些修飾,讓其更具備語義:
<SelectList> ::= <SelectField>
| , <SelectList>
複製代碼
即使是上下文無關的文法,經過遞歸降低方式,許多時候也必須從左向右超前查看 K 個字符才能肯定使用哪一個產生式,這種文法稱爲 LL(k)。
但若是每次超前查看的內容都有許多字符相同,會致使第二次開始的超前查看重複解析字符串,影響性能。最理想的狀況是,每次超前查看都不會對已肯定的字符重複查看,解決方法是提取左公因式。
設想以下的 sql 文法:
<Field> ::= <Text> as <Text>
| <Text> as<String>
| <Text> <Text>
| <Text>
複製代碼
其實 Text 自己也是比較複雜的產生式,最壞的狀況須要對 Text 連續匹配六遍。咱們將 Text 公因式提取出來就能夠僅匹配一遍,由於不管是何種 Field 產生式,都一定先遇到 Text:
<Field> ::= <Text> <F>
<F> ::= <G>
| <Text>
<G> ::= as <H>
<H> ::= <space> <Text>
| <String>
複製代碼
和消除左遞歸同樣,提取左公因式也會下降文法的可讀性,須要進行人爲修復。不過提取左公因式的修復沒辦法在文法中處理,在後面的 「函數式」 處理環節是有辦法處理的,敬請期待。
對 SQL 的文法來講不存在優先級的概念,因此從某種程度來講,SQL 的語法複雜度還不如基本的加減乘除。
在實現語法解析前,須要使用文法描述 SQL 的語法,文法描述就是語法分析的主幹業務代碼。
下一篇將介紹語法分析相關知識,幫助你一步步打造本身的 SQL 編譯器。
若是你想參與討論,請點擊這裏,每週都有新的主題,週末或週一發佈。