本文來自北京潤乾軟件技術有限公司董事長蔣步星在清華大數據產業聯合會的講座。
下面我們來講關系代數中的具體的問題,先談關聯運算的描述。
使用SQL對于單表進行查詢并不是很難理解和實施,一般也就是選取字段、過濾、排序等,只有分組匯總稍復雜些,也不是多難懂。
但是,有意義的查詢經常是多表的,比如查一下從北京到上海打了多少電話,存款超過10萬元的人中本科學歷及以上的有多少。這些都需要用到多表關聯運算。
SQL中用多表關聯運算是用JOIN運算實現的,JOIN運算在關系代數中定義非常簡單通用,就是兩個表做笛卡爾積后再過濾。
簡單的好處,就是適用面廣,什么都能表達;但也有壞處,它沒有把更多的運算特征體現出來。這樣,對于關聯復雜的情況,無論是理解還是實施都很困難。類比一下,四則運算中如果只有加法沒有乘法顯然會很頭疼。雖然乘法都可以用加法表達,但實在很麻煩。
實施運算也麻煩,我們有九九表算乘法,不需要硬加起來就可以快速運算。要按關系代數的定義來實現JOIN運算也會有類似的困難,當然數據庫廠商實施的時候都會做工程上的優化,我沒有見過哪個廠商真地用笛卡爾積實現一般的等值JOIN運算,但要嚴格地實現這個定義,還是會給工程上優化造成很大困難。
事實上,仔細分析后,我們會發現,實際應用中大多數的JOIN都是上面這三種情況,而不需要引入笛卡爾積。
這里說大多數,不是全部,就象不可能把所有的加法都可用乘法來表示。仍然有一些關聯運算需要用原始的JOIN來描述,比如用SQL做矩陣乘法,就需要用笛卡爾積。
但日常數據分析中的大多數JOIN都屬于這三種情況,我們逐個來解釋。
首先是外鍵屬性化,先來看一個例子:
這里有兩張表,一個員工表,一個部門表,我們想查一下中國經理的美國員工。員工表里面有一個字段叫部門,這是個外鍵指向部門表的主鍵。部門表里又有一個經理字段指回員工表。這是一個很常見的數據結構設計,指來指去的。
我們要算中國經理的美國員工,SQL寫出來是這樣的:
員工表和部門表JOIN起來,再JOIN回員工表,也就是要同表自叉乘,需要起個別名。在WHERE中先寫上JOIN的條件,和最終我們希望的條件,一個國籍是中國,一個國籍是美國。整個句子要看一會才能明白。
這是我的招聘考題,通過率不到一半。許多還都是有兩三年工作經驗的。不到一半的通過率,可想而知它還是有一定難度的。
那么,為什么不能這樣寫呢?我還是說員工表,美國員工好理解,但是中國經理這件事我把它寫成這樣,就是上面紅色的部分,這個字段稍復雜一點,有子屬性,子屬性又有子屬性,但那一小段語句并不難理解,也就是部門的經理的國籍是中國。
也就是說,我們要:
(1) 外鍵指向表的字段可直接引用
(2) 允許多層和遞歸引用
SQL不是這樣理解外鍵的,而我們這樣去理解外鍵就容易多了,外鍵指向的屬性可以被直接引用,也可以多層引用,這樣就會簡單得多,把外鍵看成一種屬性,把JOIN這個運算不再簡單地理解成笛卡爾積加過濾。這是第一種情況。
第二種情況是同維表等同,這個比較簡單。
這兩個1:1的表,主鍵相同,在數據庫設計中經常會有這種情況,許多字段都放在一個表里太寬,還會造成空間冗余浪費,導致性能下降,因此常常會分到多個主鍵相同的同維表中。
現在我們要做聯合查詢,這又得做JOIN。
我們希望能把它看成同一個表,直接這么寫:
還是看紅色的部分,同維表的字段可以在任何一個表中隨意訪問。也就是(1)主鍵同維的表將視為一個寬表;(2)訪問其中任何一個均可引用其他字表的字段。
這樣也會讓關聯變得簡單一些,不必再理解這種JOIN。
第三種情況,按維匯總對齊
這里有合同表和回款表,我們希望按日期統計合同額與回款額,用SQL寫出來是這樣的:
先把每個表分組匯總后再JOIN起來,如果偷懶不用子查詢先JOIN后GROUP,那結果是錯誤的,統計值會變多。這個問題必須使用子查詢。
能不能再簡單一些呢?我們可以寫成這樣:
注意紅色的部分,我們只要把兩個表分別按日期對齊就行,這兩個表之間并沒有直接關聯。我們不必關心表間關聯,各自獨立計算各自的數據就可以了。
如果這個事情再扯到外鍵的事就會更復雜,比如:
我們希望按地區統計銷售員人數和合同額,用SQL寫出來是這樣:
這個子查詢中要套著帶JOIN的GROUP,更復雜了。
類似地,按我們剛才的寫法結合第一種情況的外鍵處理方法,這個查詢可以寫成這樣:
紅色部分中出現了子屬性,但整個句子仍然很簡單。
這一條總結起來就是:
三種常用的JOIN以及簡化方式說完了,這樣理解數據關聯有什么用處呢?
一個應用就是用來實現自助報表,或者OLAP。
我是做報表工具出身的,用戶經常會向我們提出業務人員自己做報表的需求,自助報表的關鍵在于讓用戶把需要的數據取出來,而取數的關鍵又在于解決多表關聯,就如前面所說,有意義的查詢大多是多表關聯的。把蜘蛛網式的表間關聯呈現給業務用戶,有時候還需要自關聯,要用別名,我想大多數業務人員會暈掉的。
傳統的OLAP工具是如何解決多表關聯問題呢?
一般都是采用事先建模的方式,要預測用戶可能以哪種關聯方式查看數據表,事先將關聯做成物理寬表或者邏輯視圖,這樣用戶看到就是一個單表了。這是典型的按需建模,也就是有了新的關聯需求時就需要再建一個視圖或寬表,而建模需要技術人員完成,這個過程的周期就會很長。
有些傳統OLAP工具能夠根據維的類型自動關聯,但解決得不徹底,當同一個表中出現多個同維字段時就會對不上了,比如一個學生有出生地和入學地,都是地區維度,就不知道該怎么自動對應了。對于自關聯的處理也很麻煩,需要事先約定指向路徑,其實還是按需建模。
如果關系代數對關聯的處理是我們上面簡化的那樣,就不需要這么麻煩了,那三種情況能夠描述的關聯占了大多數,對自助報表和OLAP業務幾乎是全部了,這類關聯只要一次性建模就可以了。
也就是說,能達到這樣的效果:
我們按照上述模型開發了一個語法翻譯引擎,把上面那種簡單語法翻譯成完整的SQL交由數據庫去執行。在這個基礎上,配上一個界面就可以方便地實現自助報表。
這是選擇數據項的界面,不需要涉及到表的概念,只是數據項稍復雜些,有子數據項,數據項的組織呈樹狀,不象平常的數據字典是線狀的,這個樹的層次關系就體現了表的外鍵關系。
做報表時,我只要把需要的數據項往表里拽就行了。
這和普通單表取數沒太大區別,無非就是字段有多層子屬性。
這個界面生成上面看到的那些簡單語法是很容易的,然后再翻譯成SQL執行就可以了。
在做匯總報表時也很簡單,把匯總用的維度選定后,仍然是把要統計的數據項往表里拖就可以了。
你不必關心這些數據項來自哪些數據表,這些數據表之間有什么關聯。
這樣看待關聯還有個好處,就是數據庫的結構更清晰:
這是我們平常的ER圖,網狀結構的,表和表之間都有關系,表多了就很亂,表間耦合度高,加刪表時可能牽一發動全身。
而用剛才那種方式理解表間關聯,看到的結構是這樣一種總線式的:
表和表之間沒有直接關聯,互相沒有耦合,表只和中間的維度關聯,增加刪除表時不影響其它表。
需要說明的是,結構圖中連線數量并不因為樣式變了而變少,這是數據本身的關聯特性決定的,但后一種畫法會讓結構更清晰了。
這種關聯機制在性能方面也有優勢。大家知道SQL做分布式計算是很難的,而難就難在JOIN,其它GROUP、WHERE都很容易分布并行計算。但如果把JOIN看成剛才說的那幾種情況,分布式計算時就會容易許多。
在這幾種JOIN中,同維對齊的表可以按同樣規則分段存儲,這樣就很容易分布了;外鍵指向的維表不能同步分段,但一般維表會較小一些,可以在每臺機器都存儲一份,甚至可以在內存中放下,這樣可以用冗余數據換取性能,大的事實表再分段存放,這時JOIN計算幾乎不需要發生跨機關聯了。
關系代數不區分這些JOIN類型,必須實現笛卡爾積式的JOIN運算,分布式計算就困難的多,一般的辦法是將表按鍵值HASH到不同節點機上再計算,造成大量的網絡傳輸。
不同的JOIN還可以用不同的方法去優化。同維對齊可以事先排序再用歸并算法,這個復雜度要比HASH JOIN小得多。外鍵連接,如果內存可放下則可用指針去做,外存也可以事先轉換成序號直接快速定位。
當然實際實現起來事還不少。我們目前只做了內存實驗。用java的計算與Oracle對比,采用數據量比內存小,這樣ORACLE會把數據都緩存到內存中。
我們不必看JAVA和ORACLE的對比,技術不一樣,一個純內存,一個有外存。我們只看兩者比值的變化,無連接運算的單表,耗時差不多。五個表JOIN時,JAVA用指針連接的方式比ORACLE的HASH JOIN快了一倍多。這種技術放在外存上也會有優勢。
新聞熱點
疑難解答