跳至主要內容
論文精煉 · Data warehouse / OLAP

Data Cube:一個一般化 Group-By、Cross-Tab 與小計的關聯式聚合運算子

CUBE 運算子:一個 SQL 子句就一次算完 N 個維度上所有的 group-by,而且結果仍是一個關聯。

作者Jim Gray、Surajit Chaudhuri、Adam Bosworth、Andrew Layman 等人(Microsoft Research,Redmond, WA),以及 Hamid Pirahesh 與 Frank Pellow(IBM Research,San Jose, CA) 發表於Data Mining and Knowledge Discovery 1(1): 29-53,1997;Microsoft 技術報告 MSR-TR-97-32(延伸摘要發表於 ICDE 1996) 年份1995–1997
閱讀原始論文 PDF 所有論文

一口氣講完 — 整篇論文的濃縮

分析工作習慣同時沿多個維度做彙總,但 SQL 只有五個聚合函式與 GROUP BY,產出的是零維或一維聚合,因此一張六維交叉分析表得手寫成 64 個 GROUP BY 的 64 路 UNION。本論文定義 CUBE 運算子,一句敘述就算出 N 個分組欄位所有子集合的聚合;同時定義其不對稱的退化形式 ROLLUP,只產生逐層變粗的小計,僅比核心 group-by 多出 N 列。為了讓這些超級聚合(super-aggregate)列仍然是關聯式的,作者在每個分組欄位的定義域上多加一個 ALL 值,用來代表該次聚合所涵蓋的集合,於是 cube 就是一個普通的關聯,可以和其餘 SQL 自由組合。在計算面,論文把聚合函式分成 distributive、algebraic、holistic 三類,並指出前兩類可以把核心 group-by 的暫存區(scratchpad)逐層往上折疊成超級聚合,而不必重新掃描原始資料表。這套設計幾乎立刻被採用:SQL Server 6.5 當時已出貨 CUBE 與 ROLLUP,SQL 標準也吸收了這套語法。

在這篇論文之前 — 它所降落的世界

1990 年代中期,決策支援的工作流程是一個 extract-visualize-analyze 迴圈:先把彙總後的資料從 SQL 資料庫抽出成檔案或資料表,再放進試算表或視覺化工具呈現,然後據此擬定下一個查詢。這個迴圈的視覺化端早就以 N 維方式思考——Excel 樞紐分析表、內建交叉分析的報表工具、Microsoft Access 的 TRANSFORM-PIVOT 運算子、Essbase 的多維陣列——但資料庫端只有 COUNT、SUM、MIN、MAX、AVG,以及每個群組只回傳一列的 GROUP BY。各家廠商各自以互不相容的方式補洞:Red Brick 加了 Rank、N_tile、Ratio_To_Total 與累積型聚合,Informix Illustra 與 IBM DB2 Common Server 則讓使用者以 Init、Iter、Final 三個 callback 註冊新的聚合函式。聚合也絕非冷門需求——論文自己對標準基準測試的統計顯示,TPC-D 的 16 個查詢裡就有 27 個聚合與 15 個 GROUP BY,連 OLTP 基準測試裡都出現聚合。真正缺的,是一個在形狀上與下游工具同樣是 N 維的關聯式運算子。

問題 — 當時真正壞掉的地方

  • 標準 GROUP BY 把關聯切成互斥的 tuple 集合,每個集合只回傳一個聚合值,因此 SQL 只能表達零維或一維聚合,而資料分析要的是它的 N 維推廣。
  • SQL92 無法直接以計算出來的類別分組,所以要做直方圖——把時間戳記歸成日、把經緯度歸成國家——只能間接寫成一個 table-valued 子查詢,再對它做 GROUP BY。
  • 報表上那種每層都有小計的 roll-up 版面並不是關聯式的:Model 與 Year 底下的空白格實際上是 NULL,而 NULL 無法構成鍵值。
  • 最直覺的補救是像 Chris Date 建議的那樣,每個聚合層級各加一個答案欄位,但欄位數會隨被聚合屬性的冪集合成長——一個 6 維 TPC-D 查詢就要 64 個欄位——造成命名困難且名稱極長。
  • 試算表的樞紐分析表是用欄位的「值」而不是欄位名稱來產生欄位,因此對兩個基數為 N 與 M 的欄位做 pivot,會得到 N x M 個欄位,爆炸得更嚴重。
  • 用傳統 SQL 寫對稱的交叉分析,等於對每個維度子集合各寫一個 GROUP BY 再 UNION;六個維度就是 64 路 UNION,而這種寫法複雜到最佳化器無從分析,多數系統只能掃描資料 64 次、排序或雜湊 64 次。

核心概念 — 主要貢獻,以及它們為何成立

CUBE:N 維的 GROUP BY

cube 運算子把 N 個聚合屬性各視為 N 維空間的一個軸:先用一般的 GROUP BY 算出核心(core),再把所有以 ALL 取代某個分組欄位子集合而得的超級聚合 UNION 進來。這些子集合正是分組欄位的冪集合,共 2^N 種分組,其中 2^N - 1 種屬於超級聚合。關鍵在於結果是一個與核心同 schema 的關聯,因此 cube 的輸出可以再被 SELECT、被 JOIN、被嵌進更複雜的非程序式分析程式,而不只是報表工具的一個功能。由於結果列數是各維度 (Ci + 1) 的乘積,只要維度基數夠大,cube 其實只比核心 group-by 稍大一點。

把 ALL 值看成集合

論文沒有把 schema 撐成 2^N 個欄位,而是在每個維度的定義域上多加一個 ALL 值,用來標示這一列是超級聚合。依照 Joe Hellerstein 的建議,ALL 不該讀成「未知」,而該讀成該次聚合所涵蓋的那個集合,所以 Model.ALL 就等於集合 {Chevy, Ford}。這個讀法正好定義了關聯運算子在 cube 輸出上的語意——等於、大小比較與 IN——而 ALL() 函式會回傳其背後的集合,套用在任何一般值上則回傳 NULL。作者也坦承這等於把 SQL 拖進巢狀關聯的世界,讓關聯本身成為值,並直言這對關聯式系統是一大步,而非免費午餐。

ROLLUP:不對稱的 drill-down

對 drill-down 報表而言完整的 cube 是殺雞用牛刀,而且部分結果根本沒有意義:當分組屬性彼此函數相依,例如年、週、日,多數 cube 格子毫無意義。因此 ROLLUP 只產生那條線性的超級聚合鏈,從最右邊的欄位開始逐一換成 ALL,一路到總計,使得 N 維 roll-up 只在答案集合中多加 N 列,而非指數級的列數。它的答案集合天然是循序的,這正是 running sum、running average 這類累積型聚合所需要的,而完整 cube 本質上是非線性、多維的。這也正是論文開頭那張 Sales by Model by Year by Color 報表的形狀。

分組運算子的代數

GROUP BY、ROLLUP 與 CUBE 之間有一組簡潔的代數:ROLLUP 的 CUBE 還是 CUBE,GROUP BY 的 ROLLUP 還是 ROLLUP。因此論文把三者排進同一個複合子句、能力最強的放在最內層——對某些欄位 group by、對某些欄位 roll up、對其餘欄位 cube——於是一句敘述就能對 manufacturer 分組、對年月日 roll up、再對 color 與 model 做 cube。因為這是 GROUP BY 的語法擴充而非新敘述,最佳化器看到的是一個聚合運算子,而不是 64 個彼此無關查詢的 UNION。同一套擴充也把分組清單一般化成可帶 AS 相關名稱的運算式,直方圖至此才真正能直接寫出來。

distributive、algebraic、holistic 三分法

本論文最歷久不衰的貢獻,是依「部分聚合需要攜帶多少狀態」來分類聚合函式。若存在某個 G,使得把子聚合再聚合就能得到正確答案,該函式即為 distributive,COUNT、SUM、MIN、MAX 都屬此類,其中 COUNT 對應的 G 是 SUM。若子聚合可以用固定大小的 M-tuple 概括、再由函式 H 收尾,則為 algebraic:平均值攜帶 sum 與 count,標準差、MaxN、MinN 與質心也是同樣道理。若狀態大小沒有常數上界,則為 holistic,例如中位數、眾數與 rank。只有前兩類能從較低維度的結果推出超級聚合,而不必回頭讀原始資料;同一個性質也正是分割式與平行聚合能夠成立的原因。

GROUPING() 與極簡的 NULL 設計

作者承認,資深 SQL 實作者一定會對 NULL 之外再多一個非值感到恐懼:ALL 需要新關鍵字、需要在欄位定義與系統目錄中加上 ALL NOT ALLOWED 屬性,還要在各比較運算子中處理特例。於是他們提出極簡替代方案:本該放 ALL 的地方改放 NULL、不實作 ALL(),並加入一個布林函式 GROUPING(),當 select 清單中的元素是超級聚合佔位符時回傳 TRUE。範例中的總計因此變成 (NULL, NULL, NULL, 941, TRUE, TRUE, TRUE),而不是 (ALL, ALL, ALL, 941)。這正是 Microsoft SQL Server 6.5 實際出貨的設計,也是 SQL 標準最後採納的設計。

decoration 與 star/snowflake schema

cube 的答案常常需要既非分組欄位、也非聚合值的欄位——例如部門編號旁邊的部門名稱——而現行 SQL 並不允許。論文主張,只要這種 decoration 欄位對聚合欄位函數相依,就可以出現在 SELECT 清單中;而當超級聚合的 tuple 已不足以決定它時,就回傳 NULL:指定了 nation 時 continent 會填入值,但 nation 為 ALL 的列上 continent 就是 NULL。接著論文為周邊的設計模式命名:中央一張記錄事件細節的 fact table,加上為各維度提供屬性與聚合粒度的側表,每個維度一張表稱為 star schema,粒度本身再拆開則稱為 snowflake schema。論文也提醒,這些粒度實際上構成的是 lattice 而非純階層,因為日可以巢狀於週,週卻無法乾淨地巢狀於月、季或年。

運作方式 — 具體的機制

GROUP BY CUBE 的語意

CUBE 先對 select 清單中所有屬性做一次標準 GROUP BY,得到 cube 的核心。接著把整個 cube 的每個超級聚合 UNION 進來,被摺疊掉的欄位以 ALL 取代;對 N 個屬性而言,這會產生 2^N - 1 種超級聚合分組。若各維度基數為 C1 到 CN,結果關聯的列數就是各 (Ci + 1) 的乘積,每個定義域多出來的那個值即 ALL:範例中 2 個車款、3 個年份、3 種顏色的 18 列 SALES 表,會變成 3 x 4 x 4 共 48 列的 cube。ROLLUP 則是同一套構造,只保留那條「從最右邊欄位起逐一換成 ALL」的 tuple 鏈。

聚合函式介面

聚合函式透過三個 callback 與一塊私有 scratchpad handle 掛進系統,設計取自 Informix Illustra 與 IBM DB2 Common Server:Init(start)配置並初始化 handle,Iter(next)把一個值折進去,Final(end)由儲存的狀態算出結果並釋放 handle。以 Average 為例,handle 中的 count 與 sum 初始化為零,每遇到一個非 null 值就把 count 加一、把值加進 sum,最後以 sum 除以 count。較進階的系統還允許聚合函式宣告計算成本,好讓查詢最佳化器盡量減少呼叫昂貴函式的次數。這套 callback 設計(成本宣告除外)後來成為 SQL 標準草案的一部分,也是使用者自訂聚合能套用在 cube 上的前提。

樸素的 2^N 演算法

最簡單的 cube 演算法是一開始就為每個 cube 格子各配置一個 handle。當 tuple (x1, x2, ..., xN, v) 進來時,Iter(handle, v) 會被呼叫 2^N 次——每個座標在每個位置上不是 xi 就是 ALL 的格子各一次。所有輸入 tuple 處理完後,系統再對 cube 中 (Ci + 1) 乘積個格子逐一呼叫 Final。若基底資料表基數為 T,這總共是 T x 2^N 次 Iter 呼叫;對 holistic 聚合而言,這也是論文所知的唯一方法,而 roll-up 則有對應的 order-N 演算法。

由核心推算超級聚合

對 distributive 函式來說,從核心 group-by 而非從基底資料表推算超級聚合便宜得多,可把 Iter 呼叫次數約略降低 T 倍。把核心存成一個各軸大小為 Ci + 1 的 N 維陣列,再逐一投影掉一個維度,就得到每一片低一維的切片:CUBE(ALL, x2, ..., xN) = F({CUBE(i, x2, ..., xN) | i = 1..C1})。做 N 次這樣的計算即可得到所有 N-1 維超級聚合,然後一次少一個維度地重複下去,直到全部為 ALL 的那一格。若某個低維格子可由兩片切片中的任一片推得——以交叉分析表來說,就是聚合最底下那一列或最右邊那一欄——兩者答案相同,因此演算法應沿基數較小的那個軸聚合。

algebraic 聚合的 scratchpad 折疊

algebraic 聚合無法用已完成的子聚合「值」往上疊,因為超級聚合需要的是中間狀態——平均值背後的 sum 與 count,而不是平均值本身。因此 cube 演算法為核心的每個格子各保留一個 handle(這本來就是 group-by 運算的一部分),核心算完後,再把這一整組 handle 交給各個 N-1 維的超級聚合。這需要一個新的 callback:Iter_super(&handle, &handle),把右邊的子聚合 scratchpad 折進左邊的超級聚合 scratchpad。每一層的 handle 再往上傳給更高層的超級聚合,如此反覆直到 (ALL, ALL, ..., ALL) 那一格算完並呼叫 Final;「先沿較小基數聚合」的排序原則在每一層都適用。

記憶體、稀疏性與平行化

若聚合結果放得進記憶體,就用以聚合欄位為鍵的陣列或雜湊表,每個項目存一個聚合值;若維度值是長字串,則維護一張雜湊符號表把每個字串映射成整數,讓值變得稠密,cube 便能存成 N 維陣列。若放不下,就退回標準 group-by 機制——以排序或 hybrid hashing 把相同值聚在一起,再用循序掃描聚合;ROLLUP 特別適合排序,因為使用者通常本來就要排序過的答案。超級聚合通常比核心小上好幾個數量級,所以即使核心放不進記憶體,超級聚合多半仍放得下。稀疏的核心只該物化非 null 的格子,並以雜湊或 B-tree 建索引;當來源資料橫跨多顆磁碟或多個節點時,各分割先平行聚合,再以與 algebraic/distributive 折疊完全相同的邏輯合併部分結果。

維護已物化的 cube

SQL Server 6.5 約半年的實地經驗顯示,客戶會把 cube 算出來存起來,再對底層資料表定義 trigger,讓資料變動時動態更新 cube——這帶出了計算章節沒有回答的問題。以 max 為例,insert 很簡單:走訪涵蓋這筆新記錄的 2^N 個超級聚合格子,取現值與新值的較大者;而且可以提早結束,因為一個值只要輸掉一次比較,在所有較低維度也會輸。delete 就不同了:刪掉目前的最大值,會迫使 2^N 個格子重新尋找全域最大值,看起來得重算整個 cube,所以 max 對 SELECT 與 INSERT 是 distributive,對 DELETE 卻是 holistic。論文因而提出 SELECT、INSERT、DELETE 各自有一組正交的 distributive/algebraic/holistic 階層,指出 COUNT 與 SUM 在三者下皆為 algebraic 故易於維護,其餘則留作待研究的課題。

論文證明了什麼 — 量測數據與證明

  • 對標準基準測試的統計顯示聚合是普遍需求而非特例:TPC-D 的 16 個查詢中有 27 個聚合與 15 個 GROUP BY,其中包含一個 6 維與三個 3 維的 GROUP BY;AS3AP 的 23 個查詢中有 20 個聚合;Wisconsin 的 18 個查詢中有 3 個聚合與 2 個 GROUP BY;連 OLTP 基準測試 TPC-C 的 18 個查詢中都有 4 個聚合。
  • cube 的大小是 N 個維度上 (Ci + 1) 的乘積:2 個車款、3 個年份、3 種顏色的 18 列 SALES 表,會產生 3 x 4 x 4 共 48 列的 cube。若每個維度基數都是 4,4 維 cube 比基底 GROUP BY 大 2.4 倍;但真實基數往往是數十到數百,因此 cube 通常只比核心大一點點,而 N 維 roll-up 則只多出 N 個超級聚合分組層級。
  • 用傳統 SQL 表達六維交叉分析,需要 64 個不同 GROUP BY 運算子的 64 路 UNION;而且這種結果複雜到無法進行最佳化分析,多數 SQL 系統只能掃描資料 64 次、排序或雜湊 64 次。
  • 樸素的 cube 演算法在基數為 T 的基底資料表上會呼叫 Iter T x 2^N 次,再對每個 cube 格子各呼叫一次 Final;改由核心 group-by 推算超級聚合,可把呼叫次數約略降低 T 倍。
  • 三分法連同其條件都被明確寫出:distributive 指存在 G 使 F({Xij}) = G({F({Xij | i = 1..I}) | j = 1..J}),COUNT、SUM、MIN、MAX 皆成立,其中 COUNT 對應的 G 是 SUM;algebraic 指存在固定大小的 M-tuple 值函式 G 與收尾函式 H,涵蓋平均值、標準差、MaxN、MinN 與質心;holistic 指不存在常數 M 能界定子聚合狀態,如中位數、眾數與 rank,論文對這一類也坦承不知有比 2^N 演算法更好的方法。
  • 這份提案在發表時已非紙上談兵:Microsoft SQL Server 6.5 以 NULL 加 GROUPING() 的編碼方式支援 CUBE 與 ROLLUP 已約半年,客戶已在物化 cube 並以 trigger 更新,論文也指出其中許多特性正被加入 SQL 標準。

限制與取捨 — 論文自承的,以及後來被發現的

  • 論文自承:ALL 是繼 NULL 之後的第二個非值,導入它會牽動語言的許多層面——新關鍵字、欄位定義與系統目錄中的 ALL NOT ALLOWED 子句、=、<、<=、>=、> 與 IN 的特例規則,以及「ALL 除 COUNT 外不參與任何聚合」的規定。作者自己最後退回 NULL 加 GROUPING() 的編碼,而那正是實際出貨、也被標準採納的版本;換句話說,論文最優雅的那個構想,正是世界選擇不實作的那一個。
  • 論文自承:中位數、眾數、rank 這類 holistic 聚合,沒有比 T x 2^N 樸素演算法更好的計算方式,論文明說不再多談 holistic 函式的 cube,理由是使用者會以統計方法近似中位數與四分位數而避開它們。這是訴諸實務而非證明;真正讓這些函式擁有有界狀態的,是後來的 sketch 資料結構,而不是這篇論文。
  • 論文自承:cube 的大小對維度數呈指數成長,而且當分組屬性彼此函數相依時(例如對年、週、日做 cube)根本沒有意義。作者曾研究讓程式設計者精確指定想要哪些超級聚合,但碰上 collation、correlation 與運算式相關的複雜度而放棄,賭 ROLLUP 與 CUBE 足以服務多數應用;後來的 SQL 加入 GROUPING SETS,提供的正是他們放棄的那種任意子集合。
  • 論文自承且留為開放問題:增量維護一個已物化的 cube,與第一次計算它是截然不同且更難的問題,因為函式的分類會隨操作而異——max 對 SELECT 與 INSERT 是 distributive,對 DELETE 卻是 holistic,刪掉當前最大值可能迫使 2^N 個格子、實際上等於整個 cube 重算。論文只說這些想法值得更多研究。
  • 後續研究揭露的不足:論文沒有給出「該預先物化哪些子 cube」的成本模型,沒有給出在 2^N 個 group-by 之間共用排序與雜湊表的演算法,也沒有處理它自己承認比階層更複雜的維度粒度 lattice。這些缺口後來由它只能順帶引用的 view selection 與多維聚合研究補上,也由 PipeSort、Overlap、BUC 等後續 cube 演算法,以及考慮階層的聚合儲存量估計研究補上。

它後來變成什麼 — 繼承這個想法的系統

CUBE、ROLLUP、GROUPING SETS 與 GROUPING() 幾乎原封不動地進入 SQL:1999,如今出現在 DB2、Oracle(另加 GROUPING_ID 以區分多欄位情況)、SQL Server、PostgreSQL 9.5 之後、MySQL 的 WITH ROLLUP、Hive、Spark SQL、BigQuery、Snowflake 與 DuckDB——分析師今天敲的語法,就是這篇論文寫下的語法。distributive/algebraic/holistic 三分法更完全跳出 OLAP,成為思考部分聚合與聚合下推的通用語彙:MapReduce 的 combiner、Spark 的 reduceByKey 與 aggregateByKey、Flink 的 AggregateFunction(createAccumulator、add、merge、getResult),本質上就是本論文 Init、Iter、Iter_super、Final 介面的化名。近似 sketch——計算相異值的 HyperLogLog、估計分位數的 t-digest 與 KLL——最好理解為業界對論文避開的 holistic 類別所給的答案:把 holistic 函式轉成狀態有界的 algebraic 函式。維護章節提出的物化問題則開啟了一整條研究線:Harinarayan、Rajaraman 與 Ullman 的貪婪式 view selection、Agrawal 等人的 PipeSort 與 Overlap 演算法、BUC 與 Dwarf,以及增量物化檢視表維護的一般理論。在商業上,cube 抽象成為一個產品類別,從 Arbor Essbase、Microsoft OLAP Services,一路到 Apache Kylin 與 Apache Druid,都以與此處描述相近的方式預先計算 cuboid。即使是放棄預算 cube 的欄式資料倉儲,也保留了這篇論文所確立的詞彙:維度、度量、roll-up、drill-down、star schema 與 snowflake schema。

論文原文 — 逐字引用

“The novelty is that cubes are relations. Consequently, the cube operator can be imbedded in more complex non-procedural data analysis programs.”

摘要

“A six dimension cross-tab requires a 64-way union of 64 different GROUP BY operators to build the underlying representation.”

§2

“Aggregate function F() is holistic if there is no constant bound on the size of the storage needed to describe a sub-aggregate.”

§5

術語 — 依本篇論文的用法

Data cube(CUBE 運算子)
把一張資料表在 N 個分組欄位的所有子集合上做聚合所得到的關聯,每一種維度值組合(含被摺疊掉的維度)各成一列。論文的重點在於它是一個關聯,而不是一種報表版面。
ALL 值
放在超級聚合列的分組欄位中的特殊值,標示該欄位已被聚合掉。論文主張把它讀成該次聚合所涵蓋的集合,例如 ALL(Model) = {Chevy, Ford},而不是像 NULL 那樣的未知值。
超級聚合(super-aggregate)
任何至少含一個 ALL 的 cube 列,也就是在 cube 的較低維度子空間上算出的聚合。N 個分組欄位會在核心 group-by 之外再產生 2^N - 1 種超級聚合分組。
ROLLUP
CUBE 的不對稱退化形式,從最右邊的分組欄位開始逐一換成 ALL,產生 drill-down 報表中逐層變粗的小計。N 維 roll-up 只在核心答案集合上多加 N 列。
交叉分析表(cross tab)
帶有列總計、欄總計與總計的對稱二維聚合。論文指出這種緊湊的陣列表示,與使用 ALL 值的關聯式表示完全等價,而且兩者都可推廣到 N 維。
distributive 聚合函式
存在某個 G 使 F({Xij}) = G({F({Xij | i}) | j}) 的聚合函式,也就是子聚合可以直接再被聚合。COUNT、SUM、MIN、MAX 都屬此類;除 COUNT 的 G 是 SUM 外,其餘皆為 F = G。
algebraic 聚合函式
子聚合可以用固定大小的 M-tuple 概括、再由函式 H 收尾的聚合函式。平均值攜帶 sum 與 count;標準差、MaxN、MinN 與質心是論文舉的其他例子。
holistic 聚合函式
描述一個子聚合所需的儲存空間不存在常數上界 M 的聚合函式,因此無法由部分結果推出超級聚合。論文所舉的例子是中位數、眾數(MostFrequent)與 rank。
GROUPING()
論文提出的布林函式,當 select 清單中的元素是超級聚合佔位符時回傳 TRUE,否則回傳 FALSE。它讓系統可以用 NULL 編碼 ALL,同時仍能與資料中真正的 NULL 區分開來。

在時間軸上的位置 — 這篇論文在整段故事中的座標

在時間軸上查看