數據庫語言的目標
要說清這個目標,先要理解數據庫是做什么的。
數據庫這個軟件,名字中有個“庫”字,會讓人覺得它主要是為了存儲的。其實不然,數據庫實現的重要功能有兩條:計算、事務!也就是我們常說的 OLAP 和 OLTP,數據庫的存儲都是為這兩件事服務的,單純的存儲并不是數據庫的目標。
我們知道,SQL 是目前數據庫的主流語言。那么,用 SQL 做這兩件事是不是很方便呢?
事務類功能主要解決數據在寫入和讀出時要保持的一致性,實現這件事的難度并不小,但對于應用程序的接口卻非常簡單,用于操縱數據庫讀寫的代碼也很簡單。如果假定目前關系數據庫的邏輯存儲模式是合理的(也就是用數據表和記錄來存儲數據,其合理性與否是另一個復雜問題,不在這里展開了),那么 SQL 在描述事務類功能時沒什么大問題,因為并不需要描述多復雜的動作,復雜性都在數據庫內部解決了。
但計算類功能卻不一樣了。
這里說的計算是個更廣泛的概念,并不只是簡單的加加減減,查找、關聯都可以看成是某種計算。
什么樣的計算體系才算好呢?
還是兩條:寫著簡單、跑得快。
寫著簡單,很好理解,就是讓程序員很快能寫出來代碼來,這樣單位時間內可以完成更多的工作;跑得快就更容易理解,我們當然希望更短時間內獲得計算結果。
其實 SQL 中的 Q 就是查詢的意思,發明它的初衷主要是為了做查詢(也就是計算),這才是 SQL 的主要目標。然而,SQL 在描述計算任務時,卻很難說是很勝任的。
SQL為什么不行
先看寫著簡單的問題。
SQL 寫出來很象英語,有些查詢可以當英語來讀和寫(網上多得很,就不舉例了),這應當算是滿足寫著簡單這一條了吧。
且慢!我們在教科書上看到的 SQL 經常只有兩三行,這些 SQL 確實算是寫著簡單的,但如果我們嘗試一些稍復雜化的問題呢?
這是一個其實還不算很復雜的例子:計算一支股票最長連續上漲了多少天?用 SQL 寫出來是這樣的:
select max (consecutive_day)from (select count(*)
(consecutive_day from (select sum(rise_mark) over(order by trade_date) days_no_gain
from (select trade_date,
case when closing_price》lag(closing_price) over(order by trade_date)
then 0 else 1 END rise_mark
from stock_price
)
)
group by days_no_gain)
這個語句的工作原理就不解釋了,反正有點繞,同學們可以自己嘗試一下。
這是潤乾公司的招聘考題,通過率不足 20%;因為太難,后來被改成另一種方式:把 SQL 語句寫出來讓應聘者解釋它在算什么,通過率依然不高。
這說明什么?說明情況稍有復雜,SQL 就變得即難懂又難寫!
再看跑得快的問題,還是一個經常拿出來的簡單例子:1 億條數據中取前 10 名。這個任務用 SQL 寫出來并不復雜:
SELECT TOP 10 x FROM T ORDER BY x DESC
但是,這個語句對應的執行邏輯是先對所有數據進行大排序,然后再取出前 10 個,后面的不要了。大家知道,排序是一個很慢的動作,會多次遍歷數據,如果數據量大到內存裝不下,那還需要外存做緩存,性能還會進一步急劇下降。如果嚴格按這句 SQL 體現的邏輯去執行,這個運算無論如何是跑不快的。然而,很多程序員都知道這個運算并不需要大排序,也用不著外存緩存,一次遍歷用一點點內存就可以完成,也就是存在更高性能的算法。可惜的是,用 SQL 卻寫不出這樣的算法,只能寄希望于數據庫的優化器足夠聰明,能把這句 SQL 轉換成高性能算法執行,但情況復雜時數據庫的優化器也未必靠譜。
看樣子,SQL 在這兩方面做得都不夠好。這兩個并不復雜的問題都是這樣,現實中數千行的 SQL 代碼中,這種難寫且跑不快的情況比比皆是。
為什么 SQL 不行呢?
要回答這個問題,我們要分析一下用程序代碼實現計算到底是在干什么。
本質上講,編寫程序的過程,就是把解決問題的思路翻譯成計算機可執行的精確化形式語言的過程。舉例來說,就象小學生解應用題,分析問題想出解法之后,還要列出四則運算表達式。用程序計算也是一樣,不僅要想出解決問題的方法,還要把解法翻譯成計算機能理解執行的動作才算完成。
用于描述計算方法的形式語言,其核心在于所采用的代數體系。所謂代數體系,簡單說就是一些數據類型和其上的運算規則,比如小學學到的算術,就是整數和加減乘除運算。有了這套東西,我們就能把想做的運算用這個代數體系約定的符號寫出來,也就是代碼,然后計算機就可以執行了。
如果這個代數體系設計時考慮不周到,提供的數據類型和運算不方便,那就會導致描述算法非常困難。這時候會發生一個怪現象:翻譯解法到代碼的難度遠遠超過解決問題本身。
舉個例子,我們從小學習用阿拉伯數字做日常計算,做加減乘除都很方便,所有人都天經地義認為數值運算就該是這樣的。其實未必!估計很多人都知道還有一種叫做羅馬數字的東西,你知道用羅馬數字該怎么做加減乘除嗎?古羅馬人又是如何上街買菜的?
代碼難寫很大程度是代數的問題。
再看跑不快的原因。
軟件沒辦法改變硬件的性能,CPU 和硬盤該多快就是多快。不過,我們可以設計出低復雜度的算法,也就是計算量更小的算法,這樣計算機執行的動作變少,自然也就會快了。但是,光想出算法還不夠,還要把這個算法用某種形式語言寫得出來才行,否則計算機不會執行。而且,寫起來還要比較簡單,都要寫很長很麻煩,也沒有人會去用。所以呢,對于程序來講,跑得快和寫著簡單其實是同一個問題,背后還是這個形式語言采用的代數的問題。如果這個代數不好,就會導致高性能算法很難實現甚至實現不了,也就沒辦法跑得快了。就象上面說的,用 SQL 寫不出我們期望的小內存單次遍歷算法,能不能跑得快就只能寄希望于優化器。
我們再做個類比:
上過小學的同學大概都知道高斯計算 1+2+3+…+100 的小故事。普通人就是一步步地硬加 100 次,高斯小朋友很聰明,發現 1+100=101、2+99=101、…、50+51=101,結果是 50 乘 101,很快算完回家午飯了。
聽過這個故事,我們都會感慨高斯很聰明,能想到這么巧妙的辦法,即簡單又迅速。這沒有錯,但是,大家容易忽略一點:在高斯的時代,人類的算術體系(也是一個代數)中已經有了乘法!象前面所說,我們從小學習四則運算,會覺得乘法是理所當然的,然而并不是!乘法是后于加法被發明出來的。如果高斯的年代還沒有乘法,即使有聰明的高斯,也沒辦法快速解決這個問題。
目前主流數據庫是關系數據庫,之所以這么叫,是因為它的數學基礎被稱為關系代數,SQL 也就是關系代數理論上發展出來的形式語言。
現在我們能回答,為什么 SQL 在期望的兩個方面做得不夠好?問題出在關系代數上,關系代數就像一個只有加法還沒發明乘法的算術體系,很多事做不好是必然的。
關系代數已經發明五十年了,五十年前的應用需求以及硬件環境,和今天比的差異是很巨大了,繼續延用五十年前的理論來解決今天的問題,聽著就感覺太陳舊了?然而現實就是這樣,由于存量用戶太多,而且也還沒有成熟的新技術出現,基于關系代數的 SQL,今天仍然是最重要的數據庫語言。雖然這幾十年來也有一些改進完善,但根子并沒有變,面對當代的復雜需求和硬件環境,SQL 不勝任也是情理之中的事。
而且,不幸的是,這個問題是理論上的,在工程上無論如何優化也無濟于事,只能有限改善,不能根除。不過,絕大部分的數據庫開發者并不會想到這一層,或者說為了照顧存量用戶的兼容性,也沒打算想到這一層。于是,主流數據庫界一直在這個圈圈里打轉轉。
SPL為什么能行
那么該怎樣讓計算寫著更簡單、跑得更快呢?
發明新的代數!有“乘法”的代數。在其基礎上再設計新的語言。
這就是 SPL 的由來。它的理論基礎不再是關系代數,稱為離散數據集。基于這個新代數設計的形式語言,起名為SPL(Structured Process Language)。
SPL 針對 SQL 的不足(更確切地說法是,離散數據集針對關系代數的各種缺陷)進行了革新。SPL 重新定義了并擴展許多結構化數據中的運算,增加了離散性、強化了有序計算、實現了徹底的集合化、支持對象引用、提倡分步運算。
限于篇幅,這里不能介紹 SPL(離散數據集)的全貌。我們在這里列舉 SPL(離散數據集)針對 SQL(關系代數)的部分差異化改進:
游離記錄
離散數據集中的記錄是一種基本數據類型,它可以不依賴于數據表而獨立存在。數據表是記錄構成的集合,而構成某個數據表的記錄還可以用于構成其它數據表。比如過濾運算就是用原數據表中滿足條件的記錄構成新數據表,這樣,無論空間占用還是運算性能都更有優勢。
關系代數沒有可運算的數據類型來表示記錄,單記錄實際上是只有一行的數據表,不同數據表中的記錄也不能共享。比如,過濾運算時會復制出新記錄來構成新數據表,空間和時間成本都變大。
特別地,因為有游離記錄,離散數據集允許記錄的字段取值是某個記錄,這樣可以更方便地實現外鍵連接。
有序性
關系代數是基于無序集合設計的,集合成員沒有序號的概念,也沒有提供定位計算以及相鄰引用的機制。SQL 實踐時在工程上做了一些局部完善,使得現代 SQL 能方便地進行一部分有序運算。
離散數據集中的集合是有序的,集合成員都有序號的概念,可以用序號訪問成員,并定義了定位運算以返回成員在集合中的序號。離散數據集提供了符號以在集合運算中實現相鄰引用,并支持針對集合中某個序號位置進行計算。
有序運算很常見,卻一直是 SQL 的困難問題,即使在有了窗口函數后仍然很繁瑣。SPL 則大大改善了這個局面,前面那個股票上漲的例子就能說明問題。
離散性與集合化
關系代數中定義了豐富的集合運算,即能將集合作為整體參加運算,比如聚合、分組等。這是 SQL 比 Java 等高級語言更為方便的地方。
但關系代數的離散性非常差,沒有游離記錄。而 Java 等高級語言在這方面則沒有問題。
離散數據集則相當于將離散性和集合化結合起來了,既有集合數據類型及相關的運算,也有集合成員游離在集合之外單獨運算或再組成其它集合。可以說 SPL 集中了 SQL 和 Java 兩者的優勢。
有序運算是典型的離散性與集合化的結合場景。次序的概念只有在集合中才有意義,單個成員無所謂次序,這里體現了集合化;而有序計算又需要針對某個成員及其相鄰成員進行計算,需要離散性。
在離散性的支持下才能獲得更徹底的集合化,才能解決諸如有序計算類型的問題。
離散數據集是即有離散性又有集合化的代數體系,關系代數只有集合化。
分組理解
分組運算的本意是將一個大集合按某種規則拆成若干個子集合,關系代數中沒有數據類型能夠表示集合的集合,于是強迫在分組后做聚合運算。
離散數據集中允許集合的集合,可以表示合理的分組運算結果,分組和分組后的聚合被拆分成相互獨立的兩步運算,這樣可以針對分組子集再進行更復雜的運算。
關系代數中只有一種等值分組,即按分組鍵值劃分集合,等值分組是個完全劃分。
離散數據集認為任何拆分大集合的方法都是分組運算,除了常規的等值分組外,還提供了與有序性結合的有序分組,以及可能得到不完全劃分結果的對位分組。
聚合理解
關系代數中沒有顯式的集合數據類型,聚合計算的結果都是單值,分組后的聚合運算也是這樣,只有 SUM、COUNT、MAX、MIN 等幾種。特別地,關系代數無法把 TOPN 運算看成是聚合,針對全集的 TOPN 只能在輸出結果集時排序后取前 N 條,而針對分組子集則很難做到 TOPN,需要轉變思路拼出序號才能完成。
離散數據集提倡普遍集合,聚合運算的結果不一定是單值,仍然可能是個集合。在離散數據集中,TOPN 運算和 SUM、COUNT 這些是地位等同的,即可以針對全集也可以針對分組子集。
SPL 把 TOPN 理解成聚合運算后,在工程實現時還可以避免全量數據的排序,從而獲得高性能。而 SQL 的 TOPN 總是伴隨 ORDER BY 動作,理論上需要大排序才能實現,需要寄希望于數據庫在工程實現時做優化。
有序支持的高性能
離散數據集特別強調有序集合,利用有序的特征可以實施很多高性能算法。這是基于無序集合的關系代數無能為力的,只能寄希望于工程上的優化。
下面是部分利用有序特征后可以實施的低復雜度運算:
1) 數據表對主鍵有序,相當于天然有一個索引。對鍵字段的過濾經常可以快速定位,以減少外存遍歷量。隨機按鍵值取數時也可以用二分法定位,在同時針對多個鍵值取數時還能重復利用索引信息。
2) 通常的分組運算是用 HASH 算法實現的,如果我們確定地知道數據對分組鍵值有序,則可以只做相鄰對比,避免計算 HASH 值,也不會有 HASH 沖突的問題,而且非常容易并行。
3) 數據表對鍵有序,兩個大表之間對位連接可以執行更高性能的歸并算法,只要對數據遍歷一次,不必緩存,對內存占用很小;而傳統的 HASH 值分堆方法不僅比較復雜度高,需要較大內存并做外部緩存,還可能因 HASH 函數不當而造成二次 HASH 再緩存。
4) 大表作為外鍵表的連接。事實表小時,可以利用外鍵表有序,快速從中取出關聯鍵值對應的數據實現連接,不需要做 HASH 分堆動作。事實表也很大時,可以將外鍵表用分位點分成多個邏輯段,再將事實表按邏輯段進行分堆,這樣只需要對一個表做分堆,而且分堆過程中不會出現 HASH 分堆時的可能出現的二次分堆,計算復雜度能大幅下降。
其中 3 和 4 利用了離散數據集對連接運算的改造,如果仍然延用關系代數的定義(可能產生多對多),則很難實現這種低復雜的算法。
除了理論上的差異, SPL 還有許多工程層面的優勢,比如更易于編寫并行代碼、大內存預關聯提高外鍵連接性能等、特有的列存機制以支持隨意分段并行等。
再把前面的問題用 SPL 重寫一遍有個直接感受。
一支股票最長連續上漲多少天:
stock_price.sort(trade_date).group@i(closing_price《closing_price[-1]).max(~.len())
計算思路和前面的 SQL 相同,但因為引入了有序性后,表達起來容易多了,不再繞了。
1 億條數據中取前 10 名:
T.groups(;top(-10,x))
SPL 有更豐富的集合數據類型,容易描述單次遍歷上實施簡單聚合的高效算法,不涉及大排序動作。
審核編輯 :李倩
-
SQL
+關注
關注
1文章
764瀏覽量
44152 -
數據庫
+關注
關注
7文章
3807瀏覽量
64420 -
代碼
+關注
關注
30文章
4790瀏覽量
68649
原文標題:比SQL還好用,又一門國產數據庫語言誕生了
文章出處:【微信號:cxuangoodjob,微信公眾號:程序員cxuan】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論