? ? ? ? 如果您的事實表(fact tables)中有萬億行和數PB的數據,則有效存儲和查詢它們成為一個具有挑戰(zhàn)性的問題。 維度表(dimension table)通常要小得多(數百萬行),因此在本節(jié)中我們將主要關注事實數據的存儲。
?? ? ? ? 盡管事實數據表通常超過100列,但典型的數據倉庫查詢一次只能訪問4個或5個數據倉庫查詢(分析很少需要“SELECT *”查詢)。 以示例3-1中的查詢?yōu)槔核L問大量行(在2013日歷年期間每次都有人購買水果或糖果),但它只需訪問fact_sales表的三列:date_key,product_sk和 quantity。 查詢忽略所有其他列。
?? ? ? ? 例3-1。 分析人們是否更傾向于購買新鮮水果或糖果,具體取決于一周中的哪一天
SELECT
????dim_date.weekday, dim_product.category,
????SUM(fact_sales.quantity) AS quantity_sold
FROM fact_sales
????JOIN dim_date ON fact_sales.date_key = dim_date.date_key
????JOIN dim_product ON fact_sales.product_sk = dim_product.product_sk
WHERE
????dim_date.year = 2013 AND
????dim_product.category IN ('Fresh fruit', 'Candy')
GROUP BY
????dim_date.weekday, dim_product.category;
?? ? ? ? 我們如何有效地執(zhí)行此查詢?
?? ? ? ? 在大多數OLTP數據庫中,存儲都是以面向行的方式進行布局的:表格的一行中的所有值都相鄰存儲。 文檔數據庫是相似的:整個文檔通常存儲為一個連續(xù)的字節(jié)序列。 您可以在圖3-1的CSV示例中看到這一點。
?? ? ? ? 為了處理像例3-1這樣的查詢,您可能在fact_sales.date_key或fact_sales.product_sk上有索引,它們告訴存儲引擎在哪里查找特定日期或特定產品的所有銷售情況。 但是,面向行的存儲引擎仍然需要將所有這些行(每個包含超過100個屬性)從磁盤加載到內存中,解析它們,并過濾掉那些不符合所需條件的行。 這可能需要很長時間。
?? ? ? ? 面向列的存儲背后的想法很簡單:不要將所有來自一行的值存儲在一起,而是將每個列的所有值存儲在一起。 如果每列都存儲在一個單獨的文件中,則查詢只需要讀取和分析查詢中使用的那些列,這可以節(jié)省大量工作。 這個原理如圖3-10所示。
列存儲在關系數據模型中最容易理解,但它同樣適用于非關系數據。 例如,Parquet是一種列式存儲格式,支持基于Google Dremel的文檔數據模型。

?? ? ? ? 面向列的存儲布局依賴于包含相同順序的行的每個列文件。 因此,如果您需要重新組裝整行,您可以從每個單獨的列文件中獲取第23項,并將它們放在一起形成表格的第23行。
列壓縮
?? ? ? ? 除了僅從磁盤加載查詢所需的那些列以外,我們還可以通過壓縮數據來進一步降低對磁盤吞吐量的需求。 幸運的是,面向列的存儲通常非常適合壓縮。
?? ? ? ? 查看圖3-10中每列的值序列:它們通常看起來很重復,這是壓縮的好兆頭。 根據列中的數據,可以使用不同的壓縮技術。 一種在數據倉庫中特別有效的技術是位圖編碼,如圖3-11所示。

?? ? ? 通常情況下,一列中不同值的數量與行數相比較?。ɡ?,零售商可能擁有數十億的銷售交易,但只有100,000個不同的產品)。 現在我們可以獲取一個具有n個不同值的列,并將其轉換為n個獨立的位圖:每個不同值的一個位圖,每行一位。 如果該行具有該值,則該位為1,否則為0。
?? ? ? ? 如果n非常?。ɡ?,國家/地區(qū)列可能具有大約200個不同的值),那么這些位圖可以每行存儲一位。 但是,如果n更大,大部分位圖中都會有很多零(我們說它們很稀疏)。 在這種情況下,位圖可以進行運行長度編碼,如圖3-11底部所示。 這可以使列的編碼非常緊湊。
? ? ? ? 這些位圖索引非常適合數據倉庫中常見的各種查詢。 例如:
WHERE product_sk IN (30, 68, 69):?
????????加載product_sk = 30,product_sk = 68和product_sk = 69的三個位圖,并計算三個位圖的按位或,這可以非常有效地完成。WHERE product_sk = 31 AND store_sk = 3:? ?
????????加載product_sk = 31和store_sk = 3的位圖,并計算按位AND。 這是因為列按照相同的順序包含行,因此一列的位圖中的第k位對應于與另一列的位圖中的第k位相同的行。
? ? ? ? 對于不同類型的數據也有各種其他壓縮方案,在這里我們暫時不討論。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 面向列的存儲和列系列
????????Cassandra和HBase有一個列家族的概念,他們繼承了Bigtable。 然而,將它們稱為面向列是非常具有誤導性的:在每個列系列中,它們一起存儲行中的所有列以及行密鑰,并且它們不使用列壓縮。 因此,Bigtable模型仍然主要是面向行的。
?? ? ? ? 對于需要掃描數百萬行的數據倉庫查詢,一個很大的瓶頸是從磁盤獲取數據到內存的帶寬。 但這不是唯一的瓶頸。 分析型數據庫的開發(fā)人員也擔心如何有效地使用從主存儲器到CPU高速緩存的帶寬,避免CPU指令處理流水線中的分支預測誤差和氣泡,以及在現代CPU中使用單指令多數據指令。
? ? ? ? ? 除了減少需要從磁盤加載的數據量外,面向列的存儲布局對于有效利用CPU周期也很有幫助。 例如,查詢引擎可以將大量壓縮的列數據放在CPU的L1緩存中,并在緊密的循環(huán)中遍歷它(即沒有函數調用)。 一個CPU可以執(zhí)行這樣一個循環(huán)比代碼要快得多,這個代碼需要處理每個記錄的大量函數調用和條件。 列壓縮允許列中的更多行適合相同數量的L1緩存。 運算符(如前面所述的按位AND和OR)可以設計為直接在這種壓縮列數據塊上運行。 這種技術被稱為矢量化處理。
列存儲中的排序順序
?? ? ? ? 在列存儲中,存儲行的順序不一定很重要。 按插入順序存儲它們是最容易的,因為插入一個新行就意味著附加到每個列文件。 但是,我們可以選擇強制執(zhí)行命令,就像我們之前對SSTables所做的那樣,并將其用作索引機制。
? ? ? ? 請注意,對每個列單獨排序沒有意義,因為那樣我們就不會再知道列中的哪些項目屬于同一行。 我們只能重建一行,因為我們知道一列中的第k項與另一列中的第k項屬于同一行。
? ? ? ? 相反,即使數據按列存儲,數據也需要一次排序整行。 數據庫的管理員可以使用他們對常見查詢的知識來選擇表格應該被排序的列。 例如,如果查詢通常以日期范圍為目標,比如上個月,則可以將date_key作為第一個排序鍵。 然后,查詢優(yōu)化器只能掃描上個月的行,這比掃描所有行快得多。
? ? ? ? 第二列可以確定第一列中具有相同值的任何行的排序順序。 例如,如果date_key是圖3-10中的第一個排序關鍵字,那么product_sk可能是第二個排序關鍵字,因此同一天的同一產品的所有銷售都將在存儲中組合在一起。 這將有助于在某個日期范圍內按產品對銷售進行分組或過濾的查詢。
? ? ? ? 排序順序的另一個優(yōu)點是它可以幫助壓縮列。 如果主排序列沒有多個不同的值,那么在排序后,它將具有很長的序列,其中相同的值連續(xù)重復多次。 一個簡單的運行長度編碼,就像我們用于圖3-11中的位圖一樣,可以將該列壓縮到幾千字節(jié),即使該表有數十億行。
? ? ? ? 第一個排序鍵上的壓縮效果最強。 第二個和第三個排序鍵會更混亂,因此不會有這么長時間的重復值。 排序優(yōu)先級下面的列以基本上隨機的順序出現,所以它們可能不會壓縮。 但排序的前幾列仍然是一個整體。
幾個不同的排序順序
? ? ? ? 這個想法的巧妙擴展在C-Store中引入,并在商業(yè)數據倉庫Vertica中采用。 不同的查詢受益于不同的排序順序,為什么不以不同的方式存儲相同的數據? 無論如何,數據需要復制到多臺機器,以便在一臺機器發(fā)生故障時不會丟失數據。 您可能會存儲以不同方式排序的冗余數據,以便在處理查詢時,可以使用最適合查詢模式的版本。
? ? ? ? 在面向列的存儲中有多個排序順序有點類似于在面向行的存儲中具有多個二級索引。 但是最大的區(qū)別在于,面向行的存儲將每行保存在一個地方(在堆文件或聚簇索引中),并且二級索引只包含指向匹配行的指針。 在列存儲中,通常沒有任何指向其他數據的指針,只有包含值的列。
寫入面向列的存儲
? ? ? ? 這些優(yōu)化在數據倉庫中很有意義,因為大多數負載由分析人員運行的大型只讀查詢組成。 面向列的存儲,壓縮和排序都有助于更快地讀取這些查詢。 然而,他們有寫作更加困難的缺點。
? ? ? ? 壓縮列無法實現更新就地的方法,如B樹方法。 如果你想在排序表中間插入一行,你很可能不得不重寫所有的列文件。 由于行由列中的位置標識,因此插入必須始終更新所有列。
? ? ? ? 幸運的是,本章早些時候我們已經看到了一個很好的解決方案:LSM樹。 所有寫入操作都先到內存中存儲,然后將它們添加到已排序的結構中并準備寫入磁盤。 無論內存存儲是面向行還是面向列都無關緊要。 當已經積累了足夠的寫入數據時,它們將與磁盤上的列文件合并并批量寫入新文件。 這基本上是Vertica所做的。
? ? ? ? 查詢需要檢查磁盤上的列數據和最近在內存中寫入的數據,并將兩者結合起來。 但是,查詢優(yōu)化器隱藏了用戶的這種區(qū)別。 從分析人員的角度來看,通過插入,更新或刪除進行修改的數據會立即反映在后續(xù)查詢中。
聚合:數據立方體和物化視圖
? ? ? ? 并非每個數據倉庫都必定是一個列存儲:傳統(tǒng)的面向行的數據庫和其他一些架構也被使用。 但是,對于臨時分析查詢,列式存儲可能會快得多,因此它正在迅速普及。
? ? ? ? 數據倉庫的另一個值得一提的是物化匯總。 如前所述,數據倉庫查詢通常涉及一個聚合函數,如SQL中的COUNT,SUM,AVG,MIN或MAX。 如果許多不同的查詢使用相同的聚合,那么每次都需要通過原始數據進行緊縮是非常浪費的。 為什么不緩存查詢最常用的一些計數或總和?
? ? ? ? 創(chuàng)建這種緩存的一種方式是物化視圖。 在關系數據模型中,它通常被定義為一個標準(虛擬)視圖:一個類似于表的對象,其內容是某些查詢的結果。 不同之處在于物化視圖是查詢結果的實際副本,寫入磁盤,而虛擬視圖只是寫入查詢的快捷方式。 從虛擬視圖讀取時,SQL引擎會將其展開為視圖的基礎查詢,然后處理擴展的查詢。
? ? ? ? 當基礎數據發(fā)生變化時,物化視圖需要更新,因為它是數據的非規(guī)范化副本。 數據庫可以自動執(zhí)行此操作,但這種更新會使寫入操作更加昂貴,這就是物化視圖在OLTP數據庫中不常使用的原因。 在讀取量大的數據倉庫中,它們可以更有意義(無論它們是否實際提高讀取性能取決于個別情況)。
? ? ? ? 物化視圖的常見特例稱為數據立方體或OLAP立方體。 它是按不同維度分組的聚合網格。 圖3-12顯示了一個例子。

? ? ? ? 想象一下,現在每個事實都只有兩個外部關鍵字 - 在圖3-12中,這些是日期和產品。 您現在可以繪制一個二維表格,其中一個軸線上的日期和另一個軸上的產品。 每個單元包含具有該日期 - 產品組合的所有事實的屬性(例如,net_price)的聚集(例如,SUM)。 然后,您可以沿每行或每列應用相同的匯總,并獲得一個已減少一個維度的匯總(按產品的銷售額(無論日期),還是按日期的銷售額(不論產品))。
? ? ? ? 一般來說,事實往往有兩個以上的維度。 在圖3-9中有五個維度:日期,產品,商店,促銷和客戶。 很難想象五維超立方體是什么樣子,但其原理仍然相同:每個單元格包含特定日期 - 產品 - 商店 - 促銷 - 客戶組合的銷售。 這些值可以在每個維度上重復匯總。
? ? ? ? 物化數據立方體的優(yōu)點是某些查詢變得非常快,因為它們已經被有效地預先計算。 例如,如果您想知道每個商店的總銷售額,則只需查看適當維度的總計 - 無需掃描數百萬行。
? ? ? ? 缺點是數據立方體不具有查詢原始數據的靈活性。 例如,沒有辦法計算哪些銷售比例來自成本超過100美元的商品,因為價格不是其中的一個維度。 因此,大多數數據倉庫盡量保留盡可能多的原始數據,并且僅將聚合數據(如數據立方體)用作某些查詢的性能提升。