前言
在實時計算中,通常是從隊列中收集原始數據,這種原始數據在內存中通常是一個java bean,把數據收集過來以后,通常會把數據落地到數據庫,供后面的ETL使用。舉個一個簡單的例子,對一個游戲來說,為了統計某個游戲,某個服務器的登陸注冊
等事件,原始數據對應的java bean可能會是這樣:

public class Event { private String userName; private String game; private String server; private String event; }
當數據量過大的時候,通常沒有辦法實時的去做一個些統計操作,例如統計按照游戲和服務器分組統計出登陸的人次是多少,對應的SQL大致如下:
select count(user_name) from event group by name,sever where event = 'login'
當有一個sql執行引擎,可以在內存中對於一批收集過來的數據執行sql計算的時候,無疑能夠實時的計算出結果,另外由於sql是實時輸入的,程序也可以比較靈活。
例如,收集過來的一批數據,可以換成成一個List<Map<String,Object>>形式的數據結構,通過sql執行引擎,執行某個特定的sql,得到結果(也是一個List<Map<String,Object>>形式的數據結構),demo如下
----------------
[username:user1,game:lol,server:s1,event:login]
[username:user2,game:dota2,server:s2,event:register]
[username:user3,game:lol,server:s2,event:login]
[username:user4,game:dota2,server:s3,event:register]
[username:user5,game:lol,server:s10,event:login]
[username:user6,game:dota2,server:s1,event:login]
[username:user7,game:lol,server:s1,event:login]
[username:user8,game:lol,server:s1,event:login]
[username:user9,game:lol,server:s1,event:login]
----------------
select count(*) as loginNum, game,server from event group by game,server where event='login'
----------------
[loginNum:1,game:lol,server:s2]
[loginNum:4,game:lol,server:s1]
[loginNum:1,game:lol,server:s10]
[loginNum:1,game:dota2,server:s1]
----------------
解析
此sql執行引擎只支持的sql語法中的一個很小的子集,所以我更加偏向稱其為sql-like DSL(Domain Specific Language-特定領域語言),關於DSL的論述很多,我推薦兩本書,一本是Martin大叔的Domain Specific Language,另外一個本是DSL in
action。之所以選擇scala來實現,是因為scala語言中內置了對DSL的支持,可以很方便的實現一個自己的Parser,通過此Parser,可以解析你的DSL腳本(此處就是sql語句),得到你想要的中間結果,通常我們將中間結果稱為AST(Abstract syntax tree),類似於
select {...} from {...} group by {...} where {...}order by{...} limit {...}形式的sql語句,我將它轉化成如下類型的AST。
解析器的入口為
def select: Parser[SelectStmt] = "select" ~> projectionStatements ~ fromStatements ~ opt(groupStatements) ~ opt(whereExpr) ~ opt(orderByExpr) ~ opt(limit) ~ opt(";") ^^ { case p ~ f ~ g ~ w ~ o ~ l ~ end => SelectStmt(p, f, w, g, o, l) }
其中,fromStatements,groupStatements,whereExpr等有是一個單獨的解析器,通過scala中已經提供的parser combinators(解析器組合子),例如(~>,~,opt()...)等,將單獨的解析器組合起來,可以得到更復雜的解析器,類似於lego積木,你編寫一個解析
器,parserA, 只能解析某段特殊的文本,這個段文本的模式我們用patternA來表示。通過組合子 rep1sep(“,”,parserA),你就得到了一個新的解析器,這個解析器能解析的partern = patternA[,patternA][,patternA][,patternA]...
例如sql語句中的group by子句,不考慮having語法的話,大致格式是這樣的 group by [tableName.]coulumn1,[tableName.]coulumn1,[tableName.]coulumn1 可見[tableName.]coulumn1這種格式的文本,可以是基本的pattern,於是可以寫出一個解析器來解析這種格式的文本:
def selectIdent: Parser[SqlProj] = { ident ~ opt("." ~> ident) ^^ { case table ~ Some(b: String) => FieldIdent(Option(table), b) case column ~ None => FieldIdent(None, column) } }
這個函數中ident值得的標示符,opt()表示的是可以有也可以沒有,那么這個解析器解析的文本就可以有如下形式:標示符.標示符|標示符,那么通過rep1sep的組合子就能得到解析group by字句的解析器:
def groupStatements: Parser[SqlGroupBy] = "group" ~> "by" ~> rep1sep(selectIdent, ",") ^^ { case keys => SqlGroupBy(keys) }
其他部分的sql字句的解析大抵如此,整個項目的代碼,在github上。下一篇講拿到AST之后,怎么執行,得到想要的結果。