第17期:SQL的困難源于關(guān)系代數(shù)
在結(jié)構(gòu)化數(shù)據(jù)處理領(lǐng)域,SQL無疑是應(yīng)用最廣泛的工作語言,不僅被所有關(guān)系數(shù)據(jù)庫采用,許多新進(jìn)的大數(shù)據(jù)平臺(tái)也將實(shí)現(xiàn)SQL作為目標(biāo)。但現(xiàn)實(shí)是,面對(duì)當(dāng)前紛雜的計(jì)算查詢需求,SQL在很多方面并不夠好用。我們在前面說過SQL的過程性問題,這其實(shí)并不是最關(guān)鍵的問題,SQL的更大困難來源于其理論基礎(chǔ),即關(guān)系代數(shù)。
一
關(guān)系代數(shù)是一種代數(shù)體系。我們無法在本文的篇幅中嚴(yán)格定義代數(shù)體系這個(gè)概念,只能通俗地解釋。人們?yōu)榻鉀Q某種運(yùn)算問題,定義了一些數(shù)據(jù)對(duì)象及針對(duì)這些數(shù)據(jù)對(duì)象的一套運(yùn)算規(guī)則,確保這些運(yùn)算的封閉性和自洽性,就可以稱為一種代數(shù)體系了。比如說我們熟悉的有理數(shù)及其上的四則運(yùn)算就是一種用于解決日常生活中數(shù)值計(jì)算需求的代數(shù)體系。封閉性是指計(jì)算結(jié)果必須仍然是定義過的數(shù)據(jù)對(duì)象,比如有理數(shù)的四則運(yùn)算結(jié)果仍然是有理數(shù)。而自洽性指這些運(yùn)算不能出現(xiàn)矛盾的結(jié)果,比如我們要約定不能除以0,否則把某個(gè)數(shù)除以0規(guī)定成任何數(shù)都會(huì)推出邏輯矛盾來。
這里說的數(shù)據(jù)對(duì)象,和程序設(shè)計(jì)面向?qū)ο罄碚撝袛?shù)據(jù)對(duì)象不太一樣。前者主要強(qiáng)調(diào)數(shù)據(jù)上的運(yùn)算,而后者更多強(qiáng)調(diào)對(duì)象的封裝性、繼承性和重載能力。前者是為了更好的描述和實(shí)施數(shù)據(jù)運(yùn)算,后者則主要是為了代碼復(fù)用。
二
代數(shù)體系設(shè)計(jì)得好與不好,嚴(yán)重影響我們實(shí)施計(jì)算的方便度和效率!
舉兩個(gè)例子:
1. 我們從小學(xué)過的算術(shù)體系都采用阿拉伯?dāng)?shù)字,用來表示數(shù)值和做四則運(yùn)算都很方便,但試想如果換成羅馬數(shù)字會(huì)是個(gè)什么感覺?
2. 所有整數(shù)乘法都可以用加法表示,但如果我們在算術(shù)體系中引入了乘法來表示若干個(gè)相同數(shù)相加這種運(yùn)算,就可以發(fā)明九九表來做而不必硬加,效率可顯著提高。
要讓計(jì)算機(jī)實(shí)施計(jì)算,還需要一套基于代數(shù)體系的形式化語言,用戶把計(jì)算目標(biāo)按約定的語法符號(hào)寫成代碼,就可以由計(jì)算機(jī)執(zhí)行了。而用計(jì)算機(jī)解決問題的過程,也可以理解為把題目解法翻譯成某種形式化語言的過程。如果代數(shù)體系及其形式化語言設(shè)計(jì)得不好,就可能發(fā)生翻譯問題解法的難度大于解決問題本身的現(xiàn)象!用羅馬數(shù)字來實(shí)施四則運(yùn)算就是這個(gè)結(jié)果。
三
關(guān)系代數(shù)就是用來實(shí)現(xiàn)批量結(jié)構(gòu)化數(shù)據(jù)計(jì)算的代數(shù)體系,其形式化語言就是SQL。講述關(guān)系代數(shù)和SQL原理的資料很多,這里就不再贅述了。
那么用SQL解決結(jié)構(gòu)化數(shù)據(jù)運(yùn)算的效果如何呢?
人們通常關(guān)心兩個(gè)效率問題。一是運(yùn)算的描述效率,二是運(yùn)算的執(zhí)行效率。這兩個(gè)效率很容易理解,如果描述效率太低,就意味著開發(fā)成本太高,很難寫出程序進(jìn)行計(jì)算;而如果執(zhí)行效率低,則需要運(yùn)行很久才能得到結(jié)果,那實(shí)用價(jià)值也就大打折扣了。實(shí)際上,執(zhí)行高效在本質(zhì)上也是個(gè)描述問題,在軟件層面不可能提高硬件性能,但可能設(shè)計(jì)出更高效的算法,那么這個(gè)語言不能限制我們寫出高效算法。
四
面對(duì)較復(fù)雜的大數(shù)據(jù)運(yùn)算,SQL在這兩方面表現(xiàn)都很差,我們分別舉兩個(gè)并不復(fù)雜的例子說明:
1. 找出一支股票最長連續(xù)上漲了多少天
這個(gè)問題對(duì)于Java或C++程序員來講非常簡單:做個(gè)初值為0的計(jì)數(shù)器,把數(shù)據(jù)按日期排序后遍歷,發(fā)現(xiàn)上漲就將計(jì)數(shù)器加1,下跌則清0,***看這個(gè)計(jì)數(shù)器出現(xiàn)過的***值,這是個(gè)很自然的思路。但是用SQL實(shí)現(xiàn)就太困難了。關(guān)系代數(shù)延用了數(shù)學(xué)上的無序集合概念,數(shù)據(jù)排序只在輸出時(shí)有效,無法規(guī)定數(shù)據(jù)的遍歷次序,也就無法實(shí)施上述的自然思路。需要人為地制造出日期的序號(hào)后,再產(chǎn)生一個(gè)分組標(biāo)志,把上漲的日期和前一天分成一組,下跌的日期分到另一組,然后計(jì)算***的分組COUNT()值,實(shí)現(xiàn)思路很難理解。這就發(fā)生前面所說的翻譯問題解法的難度大于解決問題本身的現(xiàn)象了。
2. 從10億條數(shù)據(jù)中找出***的前10名
我們知道,這樣的問題是不需要把10億行數(shù)據(jù)全部排序的。先產(chǎn)生一個(gè)有10個(gè)成員的空集合,然后遍歷數(shù)據(jù),過程中始終保持這個(gè)小集合是當(dāng)前已遍歷過數(shù)據(jù)中***的10個(gè),這樣整個(gè)10億行數(shù)據(jù)只要遍歷一次,內(nèi)存占用很小,運(yùn)算性能很好。但關(guān)系代數(shù)中沒有顯式的集合數(shù)據(jù)類型,無法描述這個(gè)算法,只能把10億行數(shù)據(jù)大排序再取出前10后,剩下的已排序的數(shù)據(jù)沒有用了。10億行大排序的成本很高,如果內(nèi)存裝不下則還會(huì)設(shè)計(jì)多次外存數(shù)據(jù)倒換的問題,性能會(huì)嚴(yán)重下降。這就會(huì)發(fā)生我們明知有好的算法卻無計(jì)可施的尷尬局面。這種情況常常就只能寄希望于數(shù)據(jù)庫在工程上的優(yōu)化,但情況復(fù)雜的SQL會(huì)超出數(shù)據(jù)庫的優(yōu)化能力(比如在分組中取每組的前10名)。
SQL中類似的問題還很多,遠(yuǎn)遠(yuǎn)不像傳說中的那么強(qiáng)悍。限于篇幅我們不能在本文中一一羅列,以后會(huì)逐步撰文深入剖析。
五
在運(yùn)算簡單的情況,并且性能要求不高時(shí),用SQL還是比較方便的,畢竟掌握者眾多,相關(guān)軟件也很豐富。但現(xiàn)代應(yīng)用中的數(shù)據(jù)需求越來越復(fù)雜,數(shù)據(jù)量也越來越大,繼續(xù)采用SQL就會(huì)嚴(yán)重影響工作效率了。而且,SQL的不適應(yīng)并非實(shí)現(xiàn)層面的問題,而是其基礎(chǔ)理論的問題,這不是在工程上進(jìn)行優(yōu)化就能解決的。面臨當(dāng)前的數(shù)據(jù)運(yùn)算需求,關(guān)系代數(shù)顯得過于簡單了,需要從數(shù)學(xué)上進(jìn)行徹底的革新。