第19期:從SQL語法看集合化
SQL作為最常用的結(jié)構(gòu)化數(shù)據(jù)計算語言,雖然在做一些細(xì)致處理時不太方便,但用于描述基本運算還是比Java等高級語言要簡單許多。這是因為SQL是一種集合化的語言,而Java等語言不是。我們下面從SQL的語法上看集合化語言的一些特征,為了方便討論,我們就用Java作為參照語言,其它高級語言是類似的。
集合運算能力
結(jié)構(gòu)化數(shù)據(jù)經(jīng)常是批量(以集合形式)出現(xiàn)的,為了方便地計算這類數(shù)據(jù),程序設(shè)計語言有必要提供足夠的集合運算能力。
Java等高級語言則沒有直接提供集合運算類庫,雖然也有數(shù)組(相當(dāng)于集合)數(shù)據(jù)類型,但并沒有定義多少基本運算,以至于我們要對數(shù)據(jù)成員做個簡單地求和也需要寫四五行循環(huán)語句才能完成,而要做過濾、分組聚合等運算則常常要寫出數(shù)百行代碼。代碼過長不僅僅是寫起來很繁瑣,而且也不利于理解算法的整體結(jié)構(gòu),算法過程都湮沒在細(xì)節(jié)處理中。
而SQL則提供有較豐富的集合運算,如SUM/COUNT等聚合運算,WHERE用于過濾、GROUP用于分組,也支持針對集合的交、并、差等基本運算。這樣寫出來的代碼就會短小很多。
表達(dá)式參數(shù)
那么,有了集合運算能力是否就夠了呢?假如,我們?yōu)镴ava這類語言開發(fā)一批的集合運算類庫,是否就可以達(dá)到SQL的效果呢?
沒有這么簡單!
我們來看一下過濾運算。過濾通常需要一個條件,把滿足條件的集合成員保留,更技術(shù)的說法,是保留條件計算結(jié)果為真的成員。在SQL中這個條件是以一個表達(dá)式形式出現(xiàn)的,比如寫WHERE x>0,就表示保留那些使得x>0計算結(jié)果為真的成員。這個表達(dá)式x>0并不是在執(zhí)行這個語句之前先計算好的,而是在針對集合成員遍歷時才計算的。本質(zhì)上,這個表達(dá)式就是一個函數(shù),是一個以當(dāng)前集合成員為參數(shù)的函數(shù)。對于WHERE運算而言,相當(dāng)于把一個用表達(dá)式定義的函數(shù)用作了WHERE的參數(shù)。
Java的語法不能直接支持這種寫法。Java當(dāng)然也允許把一個函數(shù)作為參數(shù)傳遞給另一個函數(shù),但寫法要麻煩很多,需要事先定義一個函數(shù),代碼看起來非常臃腫。而直接把表達(dá)式寫到函數(shù)的參數(shù)中,會被先計算出來,而不是針對每個集合成員分別計算。
相比之下,SQL這種用表達(dá)式直接定義函數(shù)而作為參數(shù)傳遞的方法,顯然要簡捷和直觀得多了。
這種寫法有一個術(shù)語叫做lambda語法,或者叫函數(shù)式語言。
SQL中大量使用了lambda語法。除了過濾這種運算可以說必須要用外,有些并非必須的情況,使用了這種語法形式也會更為簡單。比如聚合函數(shù)中可以填入表達(dá)式來計算運算后的聚合值,如sum(x*x)計算平方和,這里x*x也是在sum的執(zhí)行過程中再計算的。在不支持lamdba語法時,我們也可以先用集合運算計算出成員平方構(gòu)成的集合,再針對這個集合進(jìn)行地求和,但寫法上就不如使用lamdba語法更為直觀,畢竟針對單個成員的表達(dá)式要比針對整個集合的計算更容易書寫和理解。
直接引用字段
結(jié)構(gòu)化數(shù)據(jù)并非簡單的單值,而是帶有字段的記錄。
我們看到,在SQL的表達(dá)式參數(shù)中引用記錄字段時,大多數(shù)情況可以直接使用字段名稱而不必指明字段所屬的記錄,只有在多個同名字段時才需要冠以表名(或表的別名)以示區(qū)分。
再來看Java,即使我們可以容忍事先定義函數(shù)來變相實現(xiàn)lambda語法,也只能把當(dāng)前記錄作為參數(shù)傳入這個函數(shù),然后再寫計算式時就總要帶上這個記錄。比如用單價和數(shù)量計算金額時,如果用于表示當(dāng)前成員的參數(shù)名為x,則需要寫成 “x.單價*x.數(shù)量”。而在SQL中可以更為直觀地寫成 "單價*數(shù)量”。
SQL中這些看起來理所當(dāng)然的語法風(fēng)格,其實背后并沒有那么簡單,這需要精心設(shè)計后才能被解釋程序正確解析和運算。某些支持lambda語法的腳本語言就沒有這個特性,雖然可以用表達(dá)式定義函數(shù)作為參數(shù)傳遞,但必須寫成“x.單價*x.數(shù)量”這種啰嗦的形式。有了直接引用字段的語法機制后,才可以說是專門面向結(jié)構(gòu)化數(shù)據(jù)計算的語言。
動態(tài)數(shù)據(jù)結(jié)構(gòu)
SQL還能很好地支持動態(tài)數(shù)據(jù)結(jié)構(gòu)。
結(jié)構(gòu)化數(shù)據(jù)計算中,返回值經(jīng)常也是有結(jié)構(gòu)的數(shù)據(jù),而結(jié)果數(shù)據(jù)結(jié)構(gòu)和運算相關(guān),沒辦法在代碼編寫之前就先準(zhǔn)備好。所以需要支持動態(tài)的數(shù)據(jù)結(jié)構(gòu)能力。
SQL中任何一個SELECT語句都會產(chǎn)生一個新的數(shù)據(jù)結(jié)構(gòu),在代碼中可以隨意添加刪除字段,而不必事先定義結(jié)構(gòu)(類)。Java這類語言則不行,在代碼編譯階段就要把用到的結(jié)構(gòu)(類)都定義好,原則上不能在執(zhí)行過程中動態(tài)產(chǎn)生新的結(jié)構(gòu)。
解釋型語言
動態(tài)數(shù)據(jù)結(jié)構(gòu)不能在編譯型語言中實現(xiàn)。前面說到的lambda語法也不適合采用編譯型語言來實現(xiàn)。編譯器不能確定這個寫到參數(shù)位置的表達(dá)式是應(yīng)該當(dāng)場計算出表達(dá)式的值再傳遞,還是把整個表達(dá)式編譯成一個函數(shù)傳遞,需要再設(shè)計更多的語法符號加以區(qū)分。而解釋型語言則沒有這個問題,作為參數(shù)的表達(dá)式是先計算還是遍歷集合成員時再計算,可以由函數(shù)本身來決定。解釋執(zhí)行是集合化語言的另一個重要特征。