【存儲】:《Column-Stores vs. Row-Stores》讀後感

在這裏插入圖片描述

文章的全稱應該是《Column-Stores vs. Row-Stores: How Different Are They Really?》讀後感, 但是知乎控制了標題的長度,所以縮短了一下。

1. 概述

從論文的標題可以看出這篇論文不是陳述一種新的技術、架構,而更偏議論文一點,它主要的目的在於搞清楚對於分析類的查詢爲什麼Column-Store比Row-Store好那麼多?好在哪裏?一般認爲原因是:

分析類查詢往往只查詢一個表裏面很少的幾個字段,Column-Store只需要從磁盤讀取用戶查詢的Column,而Row-Store讀取每一條記錄的時候你會把所有Column的數據讀出來,在IO上Column-Store比Row-Store效率高很多,因此性能更好。

而本文的目的是要告訴你Column-Store在存儲格式優勢只是一方面,如果沒有查詢引擎上其它幾個優化措施的配合,性能也不會太好的,這篇論文認爲Column-Store在查詢引擎層有以下幾種大的優化手段:

  • 塊遍歷(Block Iteration)
  • 壓縮(Compression)
  • 延遲物化(Late Materialization)
  • Invisible Join

其中前三點是前人就已經總結過的、在現有Column-Store上實現過了的,而最後一點是本論文的創新。下面我們一一看一下這幾種優化手段的細節,最後再看看它們優化效果的對比。

2. 四大優化策略詳解

2.1. 塊遍歷

塊遍歷(Block Iteration)是相對於單記錄遍歷(per-tuple iteration)而言的,其實說白了就是一種批量化的操作。單記錄遍歷的問題在於對於每個條數據,我們都要從Row數據裏面抽取出我們需要的column(針對Row-Store來說),然後調用相應的函數去處理,函數調用的次數跟數據的條數成是 1:1的,在大數據量的情況下這個開銷非常可觀。而塊遍歷,因爲是一次性處理多條數據,函數調用次數被降下來,當然可以提高性能。

這種提高性能的方法在Row-Store裏面是case-by-case實現的(不是一種共識), 而對於Column-Store來說已經形成共識,大家都是這麼做的。而如果column的值是字節意義上等寬的,比如數字類型,Column-Store可以進一步提高性能,因爲查詢引擎要從一個Block裏取出其中一個值進行處理的時候直接用數組下標就可以獲取數據,進一步提升性能。而且以數組的方式對數據進行訪問使得我們可以利用現代CPU的一些優化措施比如SIMD(Single Instruction Multiple Data)來實現並行化執行,進一步提高性能。

2.2. 壓縮

壓縮這種優化的方法對於Column-Store比對Row-Store更有效,原因很簡單,我個人對壓縮的理解是:

對數據進行更高效的編碼, 使得我們可以以更少的空間表達相同的意思。

而能夠進行更高效編碼的前提是這個數據肯定要有某種規律,比如有很多數據一樣,或者數據的類型一樣。而Column-Store正好符合這個特點,因爲Column-Store是把同一個Column – 也就是相同類型的數據保存在一起,當然比Row-Store把一條記錄裏面不同類型的字段值保存在一起更有規律,更有規律意味着可以有更高的壓縮比。

但是爲什麼壓縮就能帶來查詢的高效呢?壓縮首先帶來的硬盤上存儲空間的降低,但是硬盤又不值錢。它的真正意義在於:數據佔用的硬盤空間越小,查詢引擎花在IO上的時間就越少(不管是從硬盤裏面把數據讀入內存,還是從內存裏面把數據讀入CPU)。同時要記住的是數據壓縮之後,要進行處理很多時候要需要解壓縮(不管是Column-Store還是Row-Store), 因此壓縮比不是我們追求的唯一,因爲後面解壓也需要花時間,因此一般會在壓縮比和解壓速度之間做一個權衡。

高壓縮比的典型如Lempel-Ziv, Huffman, 解壓快的典型如: Snappy, Lz2

前面提到解壓縮,有的場景下解壓縮這個步驟可以徹底避免掉,比如對於採用Run-Length編碼方式進行壓縮的數據,我們可以直接在數據壓縮的格式上進行一些計算:

Run-Length的大概意思是這樣的, 對於一個數字序列: 1 1 1 1 2 2 2, 它可以表達成 1x4, 2x3

這樣不管進行 count (4 + 3), sum (1 x 4 + 2 x 3) 等等都可以不對數據進行解壓直接計算,而且因爲掃描的數據比未壓縮的要少,從而可以進一步的提升性能。文中還提到對於Column-Store應用壓縮這種優化最好的場景是當數據是經過排序的,道理很簡單,因爲如果沒有經過排序,那麼數據就沒那麼“有規律”,也就達不到最好的壓縮比。

2.3. 延遲物化

要理解延遲物化(Late Materialization), 首先解釋一下什麼是物化:爲了能夠把底層存儲格式(面向Column的), 跟用戶查詢表達的意思(Row)對應上,在一個查詢的生命週期的某個時間點,一定要把數據轉換成Row的形式,這在Column-Store裏面被稱爲物化(Materization)。

在這裏插入圖片描述

理解了物化的概念之後,延遲物化就很好理解了,意思是把這個物化的時機儘量的拖延到整個查詢生命週期的後期。延遲物化意味着在查詢執行的前一段時間內,查詢執行的模型不是關係代數,而是基於Column的(我也不知道怎麼更好的表達這種“模型”,如果有知道的朋友歡迎告知)。下面看個例子, 比如下面的查詢:

SELECT name
FROM person
WHERE id > 10
  and age > 20

一般(Naive)的做法是從文件系統讀出三列的數據,馬上物化成一行行的person數據,然後應用兩個過濾條件: id > 10 和 age > 20 , 過濾完了之後從數據裏面抽出 name 字段,作爲最後的結果,大致轉換過程如下圖:
在這裏插入圖片描述

而延遲物化的做法則會先不拼出行式數據,直接在Column數據上分別應用兩個過濾條件,從而得到兩個滿足過濾條件的bitmap, 然後再把兩個bitmap做位與(bitwise AND)的操作得到同時滿足兩個條件的所有的bitmap,因爲最後用戶需要的只是 name 字段而已,因此下一步我們拿着這些 position 對 name 字段的數據進行過濾就得到了最終的結果。如下圖:

在這裏插入圖片描述

發現沒有?整個過程中我們壓根沒有進行物化操作,從而可以大大的提高效率。

總結起來延遲物化有四個方面的好處:

  1. 關係代數裏面的 selection 和 aggregation 都會產生一些不必要的物化操作,從一種形式的tuple, 變成另外一種形式的tuple。如果對物化進行延遲的話,可以減少物化的開銷(因爲要物化的字段少了),甚至直接不需要物化了。
  2. 如果Column數據是以面向Column的壓縮方式進行壓縮的話,如果要進行物化那麼就必須先解壓,而這就使得我們之前提到的可以直接在壓縮數據上進行查詢的優勢蕩然無存了。
  3. 操作系統Cache的利用率會更好一點,因爲不會被同一個Row裏面其它無關的屬性污染Cache Line。
  4. 塊遍歷的優化手段對Column類型的數據效果更好,因爲數據以Column形式保存在一起,數據是定長的可能性更大,而如果Row形式保存在一起數據是定長的可能性非常小(因爲你一行數據裏面只要有一個是非定長的,比如VARCHAR,那麼整行數據都是非定長的)。

2.4. Invisible Join

最後本文提出了一個具有創新性的性能優化措施: Invisible Join。Invisible Join 針對的場景是數倉裏面的星型模型(Star Schema), 如果用戶查詢符合下面的模式就可以應用Invisible Join:

利用事實表的裏面的外鍵跟維度表的主鍵進行JOIN的查詢的, 最後select出一些column返回給用戶。
所謂的星型模型指的是一個事實表(fact table), 周圍通過外鍵關聯一堆維度表(dimension table)的這麼一種模型,

在這裏插入圖片描述

爲了介紹Invisible Join的思路,我們要先介紹一下兩種傳統的方案,通過對比我們才能看Invisible Join方案的優點。

傳統方案一: 按Selectivity依次JOIN

傳統方案一最簡單,按照 Selectivity 從大到小對錶進行JOIN:
在這裏插入圖片描述

傳統方案一: 按Selectivity依次JOIN

傳統方案二: 延遲物化

方案二比較有意思, 它應用了延遲物化的策略,它先不進行JOIN,而是先在維度表上對數據進行過濾,拿到對應表的 POSITION, 然後把表的主鍵跟事實表的外鍵進行JOIN,這樣我們就可以拿到兩類POSITION: 事實表的POSITION和維度表的POSITION, 然後我們通過這些POSITION把數據提取出來就完成了一次JOIN, 重複以上的操作我們就可以完成整個查詢。

在這裏插入圖片描述

傳統方案二:延遲物化

Invisible Join詳解

上面兩種方案都各有各的缺點:1.

  • 傳統方案一因爲一開始就做了JOIN,享受不了延遲物化的各種優化。
  • 傳統方案二在提取最終值的時候對很多Column的提取是亂序的操作,而亂序的提取性能是很差的(隨機IO)。

下面正式介紹一下我們的主角: Invisible JOIN

Invisible JOIN其實是對傳統方案二的一種優化,傳統方案二的精髓在於延遲物化,但是受制於大量的值的提取還是亂序的,性能還是不是最好。Invisible JOIN把能這種亂序的值提取進一步的減少, 它的具體思路如下:

  1. 把所有過濾條件應用到每個維度表上,得到符合條件的維度表的主鍵(同時也是事實表的外鍵)。
  2. 遍歷事實表,並且查詢第一步得到的所有外鍵的值,得到符合條件的bitmap(s), 這裏會有多個bitmap,因爲維度表可能有多個。
  3. 對第二步的多個bitmap做AND操作,得到最終事實表裏面符合過濾條件的bitmap。
  4. 根據第三步的事實表的bitmap以及第一步的符合條件的維度表的主鍵值,組裝出最終的返回值。

如果只是這樣的話Invisible JOIN可能比上面的第二種方案好不了多少,論文認爲在很多時間維度表裏面符合過濾條件的數據往往是連續的,連續的好處在於,它能把lookup join變成一個值的範圍檢查,範圍檢查比lookup join要快,原因很簡單,範圍檢查只需要所算數運算就好了,不需要做lookup,因此可能大幅度的提高性能。

3. 性能對比

從論文提供的性能對比數字來看,這幾大優化策略裏面延遲物化的效果最好,能夠提升性能3倍以上;壓縮的優化效果次之: 兩倍以上;Invisible JOIN 再次之:50% 到 70%;塊遍歷則能性能 5% 到 50%。

在這裏插入圖片描述

而如果把這些優化手段都去掉,Column-Store的性能跟一個普通的Row-Store就沒什麼區別了。

4. 總結

讀這篇論文最大的收穫是首先知道了到底哪些因素促使Column-Store對分析類查詢性能可以大幅優於Row-Store: 延遲物化、壓縮、Invisible Join以及塊遍歷。

特別佩服這篇論文的是很多人稍微想一下就會覺得Column-Store在分析類場景下性能優於Row-Store是天經地義的: 更少的IO,但是這篇論文詳細的對各種場景進行了測試、論證,同時對Row-Store應用類似的性能優化的手段再進行測試、對比,掰開了揉碎了的分析。最終告訴我們:

Column-Store的優點不止在於它的存儲格式,查詢引擎層的各種優化也同樣關鍵,而由於Row-Store本身存儲格式的限制,即使在Row-Store上使用這些優化,效果也不好

參考

  1. https://zhuanlan.zhihu.com/p/54433448
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章