ddia/zh-tw/ch3.md

683 lines
86 KiB
Markdown
Raw Normal View History

2021-08-19 11:41:47 +08:00
# 第三章:儲存與檢索
2021-08-09 22:26:03 +08:00
2021-08-19 11:41:47 +08:00
![](../img/ch3.png)
2021-08-09 22:26:03 +08:00
2020-10-06 01:21:28 +08:00
> 建立秩序,省卻搜尋
>
> ——德國諺語
>
-------------------
[TOC]
一個數據庫在最基礎的層次上需要完成兩件事情:當你把資料交給資料庫時,它應當把資料儲存起來;而後當你向資料庫要資料時,它應當把資料返回給你。
在[第二章](ch2.md)中,我們討論了資料模型和查詢語言,即程式設計師將資料錄入資料庫的格式,以及再次要回資料的機制。在本章中我們會從資料庫的視角來討論同樣的問題:資料庫如何儲存我們提供的資料,以及如何在我們需要時重新找到資料。
2020-10-06 01:21:28 +08:00
作為程式設計師,為什麼要關心資料庫內部儲存與檢索的機理?你可能不會去從頭開始實現自己的儲存引擎,但是你**確實**需要從許多可用的儲存引擎中選擇一個合適的。而且為了讓儲存引擎能在你的工作負載型別上執行良好,你也需要大致瞭解儲存引擎在底層究竟做了什麼。
2020-10-06 01:21:28 +08:00
特別需要注意,針對**事務性**負載最佳化的和針對**分析性**負載最佳化的儲存引擎之間存在巨大差異。稍後我們將在 “[事務處理還是分析?](#事務處理還是分析?)” 一節中探討這一區別,並在 “[列式儲存](#列式儲存)”中討論一系列針對分析性負載而最佳化的儲存引擎。
2020-10-06 01:21:28 +08:00
但首先我們將從你可能已經很熟悉的兩大類資料庫傳統的關係型資料庫和很多所謂的“NoSQL”資料庫中使用的**儲存引擎**來開始本章的內容。我們將研究兩大類儲存引擎:**日誌結構log-structured** 的儲存引擎,以及**面向頁面page-oriented** 的儲存引擎例如B樹
2020-10-06 01:21:28 +08:00
## 驅動資料庫的資料結構
世界上最簡單的資料庫可以用兩個Bash函式實現
```bash
#!/bin/bash
db_set () {
echo "$1,$2" >> database
}
db_get () {
grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
}
2020-10-06 01:21:28 +08:00
```
這兩個函式實現了鍵值儲存的功能。執行 `db_set key value` 會將 **鍵key** 和**值value** 儲存在資料庫中。鍵和值幾乎可以是你喜歡的任何東西例如值可以是JSON文件。然後呼叫 `db_get key` 會查詢與該鍵關聯的最新值並將其返回。
2020-10-06 01:21:28 +08:00
麻雀雖小,五臟俱全:
```bash
$ db_set 123456 '{"name":"London","attractions":["Big Ben","London Eye"]}'
2021-08-09 22:26:03 +08:00
$ db_set 42 '{"name":"San Francisco","attractions":["Golden Gate Bridge"]}'
2020-10-06 01:21:28 +08:00
$ db_get 42
{"name":"San Francisco","attractions":["Golden Gate Bridge"]}
2020-10-06 01:21:28 +08:00
```
底層的儲存格式非常簡單一個文字檔案每行包含一條逗號分隔的鍵值對忽略轉義問題的話大致與CSV檔案類似。每次對 `db_set` 的呼叫都會向檔案末尾追加記錄,所以更新鍵的時候舊版本的值不會被覆蓋 —— 因而查詢最新值的時候,需要找到檔案中鍵最後一次出現的位置(因此 `db_get` 中使用了 `tail -n 1 ` 。)
```bash
$ db_set 42 '{"name":"San Francisco","attractions":["Exploratorium"]}'
2021-08-09 22:26:03 +08:00
$ db_get 42
{"name":"San Francisco","attractions":["Exploratorium"]}
2020-10-06 01:21:28 +08:00
$ cat database
123456,{"name":"London","attractions":["Big Ben","London Eye"]}
42,{"name":"San Francisco","attractions":["Golden Gate Bridge"]}
42,{"name":"San Francisco","attractions":["Exploratorium"]}
2020-10-06 01:21:28 +08:00
```
`db_set` 函式對於極其簡單的場景其實有非常好的效能,因為在檔案尾部追加寫入通常是非常高效的。與`db_set`做的事情類似,許多資料庫在內部使用了**日誌log**,也就是一個 **僅追加append-only** 的資料檔案。真正的資料庫有更多的問題需要處理(如併發控制,回收硬碟空間以避免日誌無限增長,處理錯誤與部分寫入的記錄),但基本原理是一樣的。日誌極其有用,我們還將在本書的其它部分重複見到它好幾次。
2020-10-06 01:21:28 +08:00
> **日誌log** 這個詞通常指應用日誌:即應用程式輸出的描述正在發生的事情的文字。本書在更普遍的意義下使用**日誌**這一詞:一個僅追加的記錄序列。它可能壓根就不是給人類看的,它可以使用二進位制格式,並僅能由其他程式讀取。
2020-10-06 01:21:28 +08:00
另一方面,如果這個資料庫中有著大量記錄,則這個`db_get` 函式的效能會非常糟糕。每次你想查詢一個鍵時,`db_get` 必須從頭到尾掃描整個資料庫檔案來查詢鍵的出現。用演算法的語言來說,查詢的開銷是 `O(n)` :如果資料庫記錄數量 n 翻了一倍,查詢時間也要翻一倍。這就不好了。
為了高效查詢資料庫中特定鍵的值,我們需要一個數據結構:**索引index**。本章將介紹一系列的索引結構,並在它們之間進行比較。索引背後的大致思想是透過儲存一些額外的元資料作為路標來幫助你找到想要的資料。如果你想以幾種不同的方式搜尋同一份資料,那麼你也許需要在資料的不同部分上建立多個索引。
2020-10-06 01:21:28 +08:00
索引是從主資料衍生的**額外的additional** 結構。許多資料庫允許新增與刪除索引,這不會影響資料的內容,而只會影響查詢的效能。維護額外的結構會產生開銷,特別是在寫入時。寫入效能很難超過簡單地追加寫入檔案,因為追加寫入是最簡單的寫入操作。任何型別的索引通常都會減慢寫入速度,因為每次寫入資料時都需要更新索引。
2020-10-06 01:21:28 +08:00
這是儲存系統中一個重要的權衡精心選擇的索引加快了讀查詢的速度但是每個索引都會拖慢寫入速度。因為這個原因資料庫預設並不會索引所有的內容而需要你也就是程式設計師或資料庫管理員DBA基於對應用的典型查詢模式的瞭解來手動選擇索引。你可以選擇那些能為應用帶來最大收益而且又不會引入超出必要開銷的索引。
2020-10-06 01:21:28 +08:00
### 雜湊索引
讓我們從**鍵值資料key-value Data** 的索引開始。這不是你可以索引的唯一資料型別,但鍵值資料是很常見的。對於更復雜的索引來說,這也是一個有用的構建模組。
2020-10-06 01:21:28 +08:00
2021-12-13 15:02:13 +08:00
鍵值儲存與在大多數程式語言中可以找到的**字典dictionary** 型別非常相似,通常字典都是用**雜湊對映hash map**或**散列表hash table**實現的。雜湊對映在許多演算法教科書中都有描述【1,2】所以這裡我們不會討論它的工作細節。既然我們已經可以用雜湊對映來表示**記憶體中**的資料結構,為什麼不使用它來索引**硬碟上**的資料呢?
2020-10-06 01:21:28 +08:00
假設我們的資料儲存只是一個追加寫入的檔案,就像前面的例子一樣,那麼最簡單的索引策略就是:保留一個記憶體中的雜湊對映,其中每個鍵都對映到資料檔案中的一個位元組偏移量,指明瞭可以找到對應值的位置,如[圖3-1](../img/fig3-1.png)所示。當你將新的鍵值對追加寫入檔案中時,還要更新雜湊對映,以反映剛剛寫入的資料的偏移量(這同時適用於插入新鍵與更新現有鍵)。當你想查詢一個值時,使用雜湊對映來查詢資料檔案中的偏移量,**尋找seek** 該位置並讀取該值即可。
2021-08-09 22:26:03 +08:00
2021-08-19 11:41:47 +08:00
![](../img/fig3-1.png)
2021-08-09 22:26:03 +08:00
2020-10-06 01:21:28 +08:00
**圖3-1 以類CSV格式儲存鍵值對的日誌並使用記憶體雜湊對映進行索引。**
聽上去簡單但這是一個可行的方法。現實中Bitcask實際上就是這麼做的Riak中預設的儲存引擎【3】。 Bitcask提供高效能的讀取和寫入操作但要求所有的鍵必須能放入可用記憶體中因為雜湊對映完全保留在記憶體中。而資料值可以使用比可用記憶體更多的空間因為可以在硬碟上透過一次硬碟查詢操作來載入所需部分如果資料檔案的那部分已經在檔案系統快取中則讀取根本不需要任何硬碟I/O。
2020-10-06 01:21:28 +08:00
像Bitcask這樣的儲存引擎非常適合每個鍵的值經常更新的情況。例如鍵可能是某個貓咪影片的網址URL而值可能是該影片被播放的次數每次有人點選播放按鈕時遞增。在這種型別的工作負載中有很多寫操作但是沒有太多不同的鍵 —— 每個鍵有很多的寫操作,但是將所有鍵儲存在記憶體中是可行的。
2020-10-06 01:21:28 +08:00
直到現在,我們只是追加寫入一個檔案 —— 所以如何避免最終用完硬碟空間一種好的解決方案是將日誌分為特定大小的段segment當日志增長到特定尺寸時關閉當前段檔案並開始寫入一個新的段檔案。然後我們就可以對這些段進行**壓縮compaction**,如[圖3-2](../img/fig3-2.png)所示。這裡的壓縮意味著在日誌中丟棄重複的鍵,只保留每個鍵的最近更新。
2021-08-09 22:26:03 +08:00
2021-08-19 11:41:47 +08:00
![](../img/fig3-2.png)
2021-08-09 22:26:03 +08:00
**圖3-2 鍵值更新日誌(統計貓咪影片的播放次數)的壓縮,只保留每個鍵的最近值**
2020-10-06 01:21:28 +08:00
而且,由於壓縮經常會使得段變得很小(假設在一個段內鍵被平均重寫了好幾次),我們也可以在執行壓縮的同時將多個段合併在一起,如[圖3-3](../img/fig3-3.png)所示。段被寫入後永遠不會被修改,所以合併的段被寫入一個新的檔案。凍結段的合併和壓縮可以在後臺執行緒中完成,這個過程進行的同時,我們仍然可以繼續使用舊的段檔案來正常提供讀寫請求。合併過程完成後,我們將讀取請求轉換為使用新合併的段而不是舊的段 —— 然後舊的段檔案就可以簡單地刪除掉了。
2021-08-09 22:26:03 +08:00
2021-08-19 11:41:47 +08:00
![](../img/fig3-3.png)
2021-08-09 22:26:03 +08:00
2020-10-06 01:21:28 +08:00
**圖3-3 同時執行壓縮和分段合併**
每個段現在都有自己的記憶體散列表,將鍵對映到檔案偏移量。為了找到一個鍵的值,我們首先檢查最近的段的雜湊對映;如果鍵不存在,我們就檢查第二個最近的段,依此類推。合併過程將保持段的數量足夠小,所以查詢過程不需要檢查太多的雜湊對映。
要讓這個簡單的想法在實際中能工作會涉及到大量的細節。簡單來說,下面幾點都是實現過程中需要認真考慮的問題:
2020-10-06 01:21:28 +08:00
2021-12-08 14:11:22 +08:00
* 檔案格式
2020-10-06 01:21:28 +08:00
2021-12-08 14:11:22 +08:00
CSV不是日誌的最佳格式。使用二進位制格式更快更簡單首先以位元組為單位對字串的長度進行編碼然後是原始的字串不需要轉義
2020-10-06 01:21:28 +08:00
2021-12-08 14:11:22 +08:00
* 刪除記錄
2020-10-06 01:21:28 +08:00
如果要刪除一個鍵及其關聯的值則必須在資料檔案中追加一個特殊的刪除記錄邏輯刪除有時被稱為墓碑即tombstone。當日志段被合併時合併過程會透過這個墓碑知道要將被刪除鍵的所有歷史值都丟棄掉。
2020-10-06 01:21:28 +08:00
2021-12-08 14:11:22 +08:00
* 崩潰恢復
2020-10-06 01:21:28 +08:00
2021-12-08 14:11:22 +08:00
如果資料庫重新啟動,則記憶體雜湊對映將丟失。原則上,你可以透過從頭到尾讀取整個段檔案並記錄下來每個鍵的最近值來恢復每個段的雜湊對映。但是,如果段檔案很大,可能需要很長時間,這會使服務的重啟比較痛苦。 Bitcask 透過將每個段的雜湊對映的快照儲存在硬碟上來加速恢復,可以使雜湊對映更快地載入到記憶體中。
2020-10-06 01:21:28 +08:00
2021-12-08 14:11:22 +08:00
* 部分寫入記錄
2020-10-06 01:21:28 +08:00
2021-12-08 14:11:22 +08:00
資料庫隨時可能崩潰,包括在將記錄追加到日誌的過程中。 Bitcask檔案包含校驗和允許檢測和忽略日誌中的這些損壞部分。
2020-10-06 01:21:28 +08:00
2021-12-08 14:11:22 +08:00
* 併發控制
2020-10-06 01:21:28 +08:00
2021-12-08 14:11:22 +08:00
由於寫操作是以嚴格的順序追加到日誌中的,所以常見的實現是隻有一個寫入執行緒。也因為資料檔案段是僅追加的或者說是不可變的,所以它們可以被多個執行緒同時讀取。
2020-10-06 01:21:28 +08:00
乍一看,僅追加日誌似乎很浪費:為什麼不直接在檔案裡更新,用新值覆蓋舊值?僅追加的設計之所以是個好的設計,有如下幾個原因:
2020-10-06 01:21:28 +08:00
* 追加和分段合併都是順序寫入操作,通常比隨機寫入快得多,尤其是在磁性機械硬碟上。在某種程度上,順序寫入在基於快閃記憶體的**固態硬碟SSD** 上也是好的選擇【4】。我們將在“[比較B樹和LSM樹](#比較B樹和LSM樹)”中進一步討論這個問題。
* 如果段檔案是僅追加的或不可變的,併發和崩潰恢復就簡單多了。例如,當一個數據值被更新的時候發生崩潰,你不用擔心檔案裡將會同時包含舊值和新值各自的一部分。
* 合併舊段的處理也可以避免資料檔案隨著時間的推移而碎片化的問題。
2020-10-06 01:21:28 +08:00
但是,散列表索引也有其侷限性:
2020-10-06 01:21:28 +08:00
* 散列表必須能放進記憶體。如果你有非常多的鍵那真是倒黴。原則上可以在硬碟上維護一個雜湊對映不幸的是硬碟雜湊對映很難表現優秀。它需要大量的隨機訪問I/O當它用滿時想要再增長是很昂貴的並且雜湊衝突的處理也需要很煩瑣的邏輯【5】。
* 範圍查詢效率不高。例如你無法輕鬆掃描kitty00000和kitty99999之間的所有鍵——你必須在雜湊對映中單獨查詢每個鍵。
2020-10-06 01:21:28 +08:00
在下一節中,我們將看到一個沒有這些限制的索引結構。
2020-10-06 01:21:28 +08:00
### SSTables和LSM樹
在[圖3-3](../img/fig3-3.png)中,每個日誌結構儲存段都是一系列鍵值對。這些鍵值對按照它們寫入的順序排列,日誌中稍後的值優先於日誌中較早的相同鍵的值。除此之外,檔案中鍵值對的順序並不重要。
2021-08-09 22:26:03 +08:00
現在我們可以對段檔案的格式做一個簡單的改變:要求鍵值對的序列按鍵排序。乍一看,這個要求似乎打破了我們使用順序寫入的能力,我們將稍後再回到這個問題。
2020-10-06 01:21:28 +08:00
我們把這個格式稱為**排序字串表Sorted String Table**簡稱SSTable。我們還要求每個鍵只在每個合併的段檔案中出現一次壓縮過程已經保證。與使用雜湊索引的日誌段相比SSTable有幾個大的優勢
2020-10-06 01:21:28 +08:00
1. 即使檔案大於可用記憶體,合併段的操作仍然是簡單而高效的。這種方法就像歸併排序演算法中使用的方法一樣,如[圖3-4](../img/fig3-4.png)所示:你開始並排讀取多個輸入檔案,檢視每個檔案中的第一個鍵,複製最低的鍵(根據排序順序)到輸出檔案,不斷重複此步驟,將產生一個新的合併段檔案,而且它也是也按鍵排序的。
2021-08-09 22:26:03 +08:00
2021-08-19 11:41:47 +08:00
![](../img/fig3-4.png)
2021-08-09 22:26:03 +08:00
**圖3-4 合併幾個SSTable段只保留每個鍵的最新值**
2020-10-06 01:21:28 +08:00
如果在幾個輸入段中出現相同的鍵,該怎麼辦?請記住,每個段都包含在一段時間內寫入資料庫的所有值。這意味著一個輸入段中的所有值一定比另一個段中的所有值都更近(假設我們總是合併相鄰的段)。當多個段包含相同的鍵時,我們可以保留最近段的值,並丟棄舊段中的值。
2020-10-06 01:21:28 +08:00
2. 為了在檔案中找到一個特定的鍵,你不再需要在記憶體中儲存所有鍵的索引。以[圖3-5](../img/fig3-5.png)為例:假設你正在記憶體中尋找鍵 `handiwork`,但是你不知道這個鍵在段檔案中的確切偏移量。然而,你知道 `handbag``handsome` 的偏移,而且由於排序特性,你知道 `handiwork` 必須出現在這兩者之間。這意味著你可以跳到 `handbag` 的偏移位置並從那裡掃描,直到你找到 `handiwork`(或沒找到,如果該檔案中沒有該鍵)。
2021-08-09 22:26:03 +08:00
2021-08-19 11:41:47 +08:00
![](../img/fig3-5.png)
2021-08-09 22:26:03 +08:00
2020-10-06 01:21:28 +08:00
**圖3-5 具有記憶體索引的SSTable**
你仍然需要一個記憶體中的索引來告訴你一些鍵的偏移量,但它可以是稀疏的:每幾千位元組的段檔案有一個鍵就足夠了,因為幾千位元組可以很快地被掃描完[^i]。
2020-10-06 01:21:28 +08:00
[^i]: 如果所有的鍵與值都是定長的,你可以使用段檔案上的二分查詢並完全避免使用記憶體索引。然而實踐中的鍵和值通常都是變長的,因此如果沒有索引,就很難知道記錄的分界點(前一條記錄結束以及後一條記錄開始的地方)。
2020-10-06 01:21:28 +08:00
3. 由於讀取請求無論如何都需要掃描所請求範圍內的多個鍵值對因此可以將這些記錄分組為塊block並在將其寫入硬碟之前對其進行壓縮如[圖3-5](../img/fig3-5.png)中的陰影區域所示)[^譯註i] 。稀疏記憶體索引中的每個條目都指向壓縮塊的開始處。除了節省硬碟空間之外壓縮還可以減少對I/O頻寬的使用。
2021-08-09 22:26:03 +08:00
[^譯註i]: 這裡的壓縮是compression不是前文的compaction請注意區分。
2020-10-06 01:21:28 +08:00
#### 構建和維護SSTables
到目前為止還不錯,但是如何讓你的資料能夠預先排好序呢?畢竟我們接收到的寫入請求可能以任何順序發生。
2020-10-06 01:21:28 +08:00
雖然在硬碟上維護有序結構也是可能的(請參閱“[B樹](#B樹)”但在記憶體儲存則要容易得多。有許多可以使用的眾所周知的樹形資料結構例如紅黑樹或AVL樹【2】。使用這些資料結構你可以按任何順序插入鍵並按排序順序讀取它們。
2020-10-06 01:21:28 +08:00
現在我們可以讓我們的儲存引擎以如下方式工作:
2020-10-06 01:21:28 +08:00
* 有新寫入時,將其新增到記憶體中的平衡樹資料結構(例如紅黑樹)。這個記憶體樹有時被稱為**記憶體表memtable**。
* 當**記憶體表**大於某個閾值通常為幾兆位元組將其作為SSTable檔案寫入硬碟。這可以高效地完成因為樹已經維護了按鍵排序的鍵值對。新的SSTable檔案將成為資料庫中最新的段。當該SSTable被寫入硬碟時新的寫入可以在一個新的記憶體表例項上繼續進行。
* 收到讀取請求時,首先嚐試在記憶體表中找到對應的鍵,如果沒有就在最近的硬碟段中尋找,如果還沒有就在下一個較舊的段中繼續尋找,以此類推。
* 時不時地,在後臺執行一個合併和壓縮過程,以合併段檔案並將已覆蓋或已刪除的值丟棄掉。
2020-10-06 01:21:28 +08:00
這個方案效果很好。它只會遇到一個問題如果資料庫崩潰則最近的寫入在記憶體表中但尚未寫入硬碟將丟失。為了避免這個問題我們可以在硬碟上儲存一個單獨的日誌每個寫入都會立即被追加到這個日誌上就像在前面的章節中所描述的那樣。這個日誌沒有按排序順序但這並不重要因為它的唯一目的是在崩潰後恢復記憶體表。每當記憶體表寫出到SSTable時相應的日誌都可以被丟棄。
2020-10-06 01:21:28 +08:00
#### 用SSTables製作LSM樹
這裡描述的演算法本質上是LevelDB【6】和RocksDB【7】這些鍵值儲存引擎庫所使用的技術這些儲存引擎被設計嵌入到其他應用程式中。除此之外LevelDB可以在Riak中用作Bitcask的替代品。在Cassandra和HBase中也使用了類似的儲存引擎【8】而且他們都受到了Google的Bigtable論文【9】引入了術語 SSTable 和 memtable )的啟發。
2020-10-06 01:21:28 +08:00
2021-08-09 22:26:03 +08:00
最初這種索引結構是由Patrick O'Neil等人描述的且被命名為日誌結構合併樹或LSM樹【10】它是基於更早之前的日誌結構檔案系統【11】來構建的。基於這種合併和壓縮排序檔案原理的儲存引擎通常被稱為LSM儲存引擎。
2020-10-06 01:21:28 +08:00
2021-12-13 15:02:13 +08:00
Lucene是Elasticsearch和Solr使用的一種全文搜尋的索引引擎它使用類似的方法來儲存它的關鍵詞詞典【12,13】。全文索引比鍵值索引複雜得多但是基於類似的想法在搜尋查詢中給出一個單詞找到提及單詞的所有文件網頁產品描述等。這是透過鍵值結構實現的其中鍵是單詞或**詞語**即term值是所有包含該單詞的文件的ID列表記錄列表。在Lucene中從詞語到記錄列表的這種對映儲存在類似於SSTable的有序檔案中並根據需要在後臺合併【14】。
2020-10-06 01:21:28 +08:00
2021-08-09 22:26:03 +08:00
#### 效能最佳化
2020-10-06 01:21:28 +08:00
與往常一樣要讓儲存引擎在實踐中表現良好涉及到大量設計細節。例如當查詢資料庫中不存在的鍵時LSM樹演算法可能會很慢你必須先檢查記憶體表然後檢視從最近的到最舊的所有的段可能還必須從硬碟讀取每一個段檔案然後才能確定這個鍵不存在。為了最佳化這種訪問儲存引擎通常使用額外的布隆過濾器Bloom filters【15】。 (布隆過濾器是用於近似集合內容的高效記憶體資料結構,它可以告訴你資料庫中是不是不存在某個鍵,從而為不存在的鍵節省掉許多不必要的硬碟讀取操作。)
2020-10-06 01:21:28 +08:00
還有一些不同的策略來確定SSTables被壓縮和合並的順序和時間。最常見的選擇是size-tiered和leveled compaction。LevelDB和RocksDB使用leveled compactionLevelDB因此得名HBase使用size-tieredCassandra同時支援這兩種【16】。對於sized-tiered較新和較小的SSTables相繼被合併到較舊的和較大的SSTable中。對於leveled compactionkey範圍被拆分到較小的SSTables而較舊的資料被移動到單獨的層級level這使得壓縮compaction能夠更加增量地進行並且使用較少的硬碟空間。
2020-10-06 01:21:28 +08:00
即使有許多微妙的東西LSM樹的基本思想 —— 儲存一系列在後臺合併的SSTables —— 簡單而有效。即使資料集比可用記憶體大得多它仍能繼續正常工作。由於資料按排序順序儲存你可以高效地執行範圍查詢掃描所有從某個最小值到某個最大值之間的所有鍵並且因為硬碟寫入是連續的所以LSM樹可以支援非常高的寫入吞吐量。
2020-10-06 01:21:28 +08:00
2021-08-09 22:26:03 +08:00
### B樹
2020-10-06 01:21:28 +08:00
前面討論的日誌結構索引正處在逐漸被接受的階段但它們並不是最常見的索引型別。使用最廣泛的索引結構和日誌結構索引相當不同它就是我們接下來要討論的B樹。
2020-10-06 01:21:28 +08:00
從1970年被引入【17】僅不到10年後就變得“無處不在”【18】B樹很好地經受了時間的考驗。在幾乎所有的關係資料庫中它們仍然是標準的索引實現許多非關係資料庫也會使用到B樹。
2020-10-06 01:21:28 +08:00
像SSTables一樣B樹保持按鍵排序的鍵值對這允許高效的鍵值查詢和範圍查詢。但這也就是所有的相似之處了B樹有著非常不同的設計理念。
我們前面看到的日誌結構索引將資料庫分解為可變大小的段通常是幾兆位元組或更大的大小並且總是按順序寫入段。相比之下B樹將資料庫分解成固定大小的塊block或頁面page傳統上大小為4KB有時會更大並且一次只能讀取或寫入一個頁面。這種設計更接近於底層硬體因為硬碟空間也是按固定大小的塊來組織的。
2020-10-06 01:21:28 +08:00
每個頁面都可以使用地址或位置來標識,這允許一個頁面引用另一個頁面 —— 類似於指標,但在硬碟而不是在記憶體中。我們可以使用這些頁面引用來構建一個頁面樹,如[圖3-6](../img/fig3-6.png)所示。
2020-10-06 01:21:28 +08:00
2021-08-19 11:41:47 +08:00
![](../img/fig3-6.png)
2020-10-06 01:21:28 +08:00
2021-08-09 22:26:03 +08:00
**圖3-6 使用B樹索引查詢一個鍵**
2020-10-06 01:21:28 +08:00
2021-08-09 22:26:03 +08:00
一個頁面會被指定為B樹的根在索引中查詢一個鍵時就從這裡開始。該頁面包含幾個鍵和對子頁面的引用。每個子頁面負責一段連續範圍的鍵引用之間的鍵指明瞭引用子頁面的鍵範圍。
2020-10-06 01:21:28 +08:00
在[圖3-6](../img/fig3-6.png)的例子中我們正在尋找鍵251 所以我們知道我們需要跟蹤邊界200和300之間的頁面引用。這將我們帶到一個類似的頁面進一步將200到300的範圍拆分到子範圍。
2020-10-06 01:21:28 +08:00
最終我們將到達某個包含單個鍵的頁面葉子頁面leaf page該頁面或者直接包含每個鍵的值或者包含了對可以找到值的頁面的引用。
2020-10-06 01:21:28 +08:00
在B樹的一個頁面中對子頁面的引用的數量稱為分支因子。例如在[圖3-6](../img/fig3-6.png)中分支因子是6。在實踐中分支因子取決於儲存頁面引用和範圍邊界所需的空間量但通常是幾百個。
2021-08-09 22:26:03 +08:00
如果要更新B樹中現有鍵的值需要搜尋包含該鍵的葉子頁面更改該頁面中的值並將該頁面寫回到硬碟對該頁面的任何引用都將保持有效。如果你想新增一個新的鍵你需要找到其範圍能包含新鍵的頁面並將其新增到該頁面。如果頁面中沒有足夠的可用空間容納新鍵則將其分成兩個半滿頁面並更新父頁面以反映新的鍵範圍分割槽如[圖3-7](../img/fig3-7.png)所示[^ii]。
2021-08-09 22:26:03 +08:00
2021-08-19 11:41:47 +08:00
![](../img/fig3-7.png)
2021-08-09 22:26:03 +08:00
**圖3-7 透過分割頁面來生長B樹**
[^ii]: 向B樹中插入一個新的鍵是相當符合直覺的但刪除一個鍵同時保持樹平衡就會牽扯很多其他東西了【2】。
這個演算法可以確保樹保持平衡具有n個鍵的B樹總是具有 $O(log n)$ 的深度。大多數資料庫可以放入一個三到四層的B樹所以你不需要追蹤多個頁面引用來找到你正在查詢的頁面。分支因子為500的4KB頁面的四層樹可以儲存多達256TB的資料。
2021-08-09 22:26:03 +08:00
#### 讓B樹更可靠
B樹的基本底層寫操作是用新資料覆寫硬碟上的頁面並假定覆寫不改變頁面的位置當頁面被覆寫時對該頁面的所有引用保持完整。這與日誌結構索引如LSM樹形成鮮明對比後者只追加到檔案並最終刪除過時的檔案但從不修改檔案中已有的內容。
2021-08-09 22:26:03 +08:00
你可以把覆寫硬碟上的頁面對應為實際的硬體操作。在磁性硬碟驅動器上這意味著將磁頭移動到正確的位置等待旋轉盤上的正確位置出現然後用新的資料覆寫適當的扇區。在固態硬碟上由於SSD必須一次擦除和重寫相當大的儲存晶片塊所以會發生更復雜的事情【19】。
2021-08-09 22:26:03 +08:00
而且,一些操作需要覆寫幾個不同的頁面。例如,如果因為插入導致頁面過滿而拆分頁面,則需要寫入新拆分的兩個頁面,並覆寫其父頁面以更新對兩個子頁面的引用。這是一個危險的操作,因為如果資料庫在僅有部分頁面被寫入時崩潰,那麼最終將導致一個損壞的索引(例如,可能有一個孤兒頁面不是任何父項的子項) 。
2021-08-09 22:26:03 +08:00
2021-12-13 15:02:13 +08:00
為了使資料庫能處理異常崩潰的場景B樹實現通常會帶有一個額外的硬碟資料結構**預寫式日誌**WAL即write-ahead log也稱為**重做日誌**即redo log。這是一個僅追加的檔案每個B樹的修改在其能被應用到樹本身的頁面之前都必須先寫入到該檔案。當資料庫在崩潰後恢復時這個日誌將被用來使B樹恢復到一致的狀態【5,20】。
2021-08-09 22:26:03 +08:00
另外還有一個更新頁面的複雜情況是如果多個執行緒要同時訪問B樹則需要仔細的併發控制 —— 否則執行緒可能會看到樹處於不一致的狀態。這通常是透過使用**鎖存器latches**(輕量級鎖)保護樹的資料結構來完成。日誌結構化的方法在這方面更簡單,因為它們在後臺進行所有的合併,而不會干擾新接收到的查詢,並且能夠時不時地將舊的段原子交換為新的段。
2021-08-09 22:26:03 +08:00
#### B樹的最佳化
2021-08-09 22:26:03 +08:00
由於B樹已經存在了很久所以並不奇怪這麼多年下來有很多最佳化的設計被開發出來僅舉幾例
2021-08-09 22:26:03 +08:00
* 一些資料庫如LMDB使用寫時複製方案【21】而不是覆蓋頁面並維護WAL以支援崩潰恢復。修改的頁面被寫入到不同的位置並且還在樹中建立了父頁面的新版本以指向新的位置。這種方法對於併發控制也很有用我們將在“[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)”中看到。
* 我們可以透過不儲存整個鍵,而是縮短其大小,來節省頁面空間。特別是在樹內部的頁面上,鍵只需要提供足夠的資訊來充當鍵範圍之間的邊界。在頁面中包含更多的鍵允許樹具有更高的分支因子,因此也就允許更少的層級[^iii]。
* 通常頁面可以放置在硬碟上的任何位置沒有什麼要求相鄰鍵範圍的頁面也放在硬碟上相鄰的區域。如果某個查詢需要按照排序順序掃描大部分的鍵範圍那麼這種按頁面儲存的佈局可能會效率低下因為每次頁面讀取可能都需要進行硬碟查詢。因此許多B樹的實現在佈局樹時會盡量使葉子頁面按順序出現在硬碟上。但是隨著樹的增長要維持這個順序是很困難的。相比之下由於LSM樹在合併過程中一次又一次地重寫儲存的大部分所以它們更容易使順序鍵在硬碟上彼此靠近。
* 額外的指標已被新增到樹中。例如,每個葉子頁面可以引用其左邊和右邊的兄弟頁面,使得不用跳回父頁面就能按順序對鍵進行掃描。
* B樹的變體如分形樹fractal tree【22】借用一些日誌結構的思想來減少硬碟查詢而且它們與分形無關
[^iii]: 這個變種有時被稱為B+樹但因為這個最佳化已被廣泛使用所以經常無法區分於其它的B樹變種。
2021-08-09 22:26:03 +08:00
### 比較B樹和LSM樹
儘管B樹實現通常比LSM樹實現更成熟但LSM樹由於其效能特點也非常有趣。根據經驗通常LSM樹的寫入速度更快而B樹的讀取速度更快【23】。 LSM樹上的讀取通常比較慢因為它們必須檢查幾種不同的資料結構和不同壓縮Compaction層級的SSTables。
然而,基準測試的結果通常和工作負載的細節相關。你需要用你特有的工作負載來測試系統,以便進行有效的比較。在本節中,我們將簡要討論一些在衡量儲存引擎效能時值得考慮的事情。
2021-08-09 22:26:03 +08:00
#### LSM樹的優點
B樹索引中的每塊資料都必須至少寫入兩次一次寫入預先寫入日誌WAL一次寫入樹頁面本身如果有分頁還需要再寫入一次。即使在該頁面中只有幾個位元組發生了變化也需要接受寫入整個頁面的開銷。有些儲存引擎甚至會覆寫同一個頁面兩次以免在電源故障的情況下導致頁面部分更新【24,25】。
2021-08-09 22:26:03 +08:00
由於反覆壓縮和合並SSTables日誌結構索引也會多次重寫資料。這種影響 —— 在資料庫的生命週期中每次寫入資料庫導致對硬碟的多次寫入 —— 被稱為**寫放大write amplification**。需要特別注意的是固態硬碟,固態硬碟的快閃記憶體壽命在覆寫有限次數後就會耗盡。
2021-08-09 22:26:03 +08:00
在寫入繁重的應用程式中,效能瓶頸可能是資料庫可以寫入硬碟的速度。在這種情況下,寫放大會導致直接的效能代價:儲存引擎寫入硬碟的次數越多,可用硬碟頻寬內它能處理的每秒寫入次數就越少。
2021-08-09 22:26:03 +08:00
而且LSM樹通常能夠比B樹支援更高的寫入吞吐量部分原因是它們有時具有較低的寫放大儘管這取決於儲存引擎的配置和工作負載部分是因為它們順序地寫入緊湊的SSTable檔案而不是必須覆寫樹中的幾個頁面【26】。這種差異在磁性硬碟驅動器上尤其重要其順序寫入比隨機寫入要快得多。
2021-08-09 22:26:03 +08:00
LSM樹可以被壓縮得更好因此通常能比B樹在硬碟上產生更小的檔案。B樹儲存引擎會由於碎片化fragmentation而留下一些未使用的硬碟空間當頁面被拆分或某行不能放入現有頁面時頁面中的某些空間仍未被使用。由於LSM樹不是面向頁面的並且會透過定期重寫SSTables以去除碎片所以它們具有較低的儲存開銷特別是當使用分層壓縮leveled compaction時【27】。
2021-08-09 22:26:03 +08:00
在許多固態硬碟上韌體內部使用了日誌結構化演算法以將隨機寫入轉變為順序寫入底層儲存晶片因此儲存引擎寫入模式的影響不太明顯【19】。但是較低的寫入放大率和減少的碎片仍然對固態硬碟更有利更緊湊地表示資料允許在可用的I/O頻寬內處理更多的讀取和寫入請求。
2021-08-09 22:26:03 +08:00
#### LSM樹的缺點
日誌結構儲存的缺點是壓縮過程有時會干擾正在進行的讀寫操作。儘管儲存引擎嘗試增量地執行壓縮以儘量不影響併發訪問,但是硬碟資源有限,所以很容易發生某個請求需要等待硬碟先完成昂貴的壓縮操作。對吞吐量和平均響應時間的影響通常很小,但是日誌結構化儲存引擎在更高百分位的響應時間(請參閱“[描述效能](ch1.md#描述效能)”有時會相當長而B樹的行為則相對更具可預測性【28】。
2021-08-09 22:26:03 +08:00
壓縮的另一個問題出現在高寫入吞吐量時:硬碟的有限寫入頻寬需要在初始寫入(記錄日誌和重新整理記憶體表到硬碟)和在後臺執行的壓縮執行緒之間共享。寫入空資料庫時,可以使用全硬碟頻寬進行初始寫入,但資料庫越大,壓縮所需的硬碟頻寬就越多。
2021-08-09 22:26:03 +08:00
如果寫入吞吐量很高並且壓縮沒有仔細配置好有可能導致壓縮跟不上寫入速率。在這種情況下硬碟上未合併段的數量不斷增加直到硬碟空間用完讀取速度也會減慢因為它們需要檢查更多的段檔案。通常情況下即使壓縮無法跟上基於SSTable的儲存引擎也不會限制傳入寫入的速率所以你需要進行明確的監控來檢測這種情況【29,30】。
2021-08-09 22:26:03 +08:00
B樹的一個優點是每個鍵只存在於索引中的一個位置而日誌結構化的儲存引擎可能在不同的段中有相同鍵的多個副本。這個方面使得B樹在想要提供強大的事務語義的資料庫中很有吸引力在許多關係資料庫中事務隔離是透過在鍵範圍上使用鎖來實現的在B樹索引中這些鎖可以直接附加到樹上【5】。在[第七章](ch7.md)中,我們將更詳細地討論這一點。
2021-08-09 22:26:03 +08:00
B樹在資料庫架構中是非常根深蒂固的為許多工作負載都提供了始終如一的良好效能所以它們不可能很快就會消失。在新的資料儲存中日誌結構化索引變得越來越流行。沒有快速和容易的規則來確定哪種型別的儲存引擎對你的場景更好所以值得去透過一些測試來得到相關的經驗。
2021-08-09 22:26:03 +08:00
### 其他索引結構
到目前為止,我們只討論了鍵值索引,它們就像關係模型中的**主鍵primary key** 索引。主鍵唯一標識關係表中的一行或文件資料庫中的一個文件或圖形資料庫中的一個頂點。資料庫中的其他記錄可以透過其主鍵或ID引用該行/文件/頂點,索引就被用於解析這樣的引用。
2021-08-09 22:26:03 +08:00
次級索引secondary indexes也很常見。在關係資料庫中你可以使用 `CREATE INDEX` 命令在同一個表上建立多個次級索引而且這些索引通常對於有效地執行聯接join而言至關重要。例如在[第二章](ch2.md)中的[圖2-1](../img/fig2-1.png)中,很可能在 `user_id` 列上有一個次級索引,以便你可以在每個表中找到屬於同一使用者的所有行。
2021-08-09 22:26:03 +08:00
次級索引可以很容易地從鍵值索引構建。次級索引主要的不同是鍵不是唯一的即可能有許多行文件頂點具有相同的鍵。這可以透過兩種方式來解決或者將匹配行識別符號的列表作為索引裡的值就像全文索引中的記錄列表或者透過向每個鍵新增行識別符號來使鍵唯一。無論哪種方式B樹和日誌結構索引都可以用作次級索引。
2021-08-09 22:26:03 +08:00
#### 將值儲存在索引中
索引中的鍵是查詢要搜尋的內容,而其值可以是以下兩種情況之一:它可以是實際的行(文件,頂點),也可以是對儲存在別處的行的引用。在後一種情況下,行被儲存的地方被稱為**堆檔案heap file**,並且儲存的資料沒有特定的順序(它可以是僅追加的,或者它可以跟蹤被刪除的行以便後續可以用新的資料進行覆蓋)。堆檔案方法很常見,因為它避免了在存在多個次級索引時對資料的複製:每個索引只引用堆檔案中的一個位置,實際的資料都儲存在一個地方。
2021-09-26 11:24:59 +08:00
在不更改鍵的情況下更新值時堆檔案方法可以非常高效只要新值的位元組數不大於舊值就可以覆蓋該記錄。如果新值更大情況會更復雜因為它可能需要移到堆中有足夠空間的新位置。在這種情況下要麼所有的索引都需要更新以指向記錄的新堆位置或者在舊堆位置留下一個轉發指標【5】。
2021-08-09 22:26:03 +08:00
在某些情況下從索引到堆檔案的額外跳躍對讀取來說效能損失太大因此可能希望將被索引的行直接儲存在索引中。這被稱為聚集索引clustered index。例如在MySQL的InnoDB儲存引擎中表的主鍵總是一個聚集索引次級索引則引用主鍵而不是堆檔案中的位置【31】。在SQL Server中可以為每個表指定一個聚集索引【32】。
2021-08-09 22:26:03 +08:00
**聚集索引**(在索引中儲存所有的行資料)和 **非聚集索引**(僅在索引中儲存對資料的引用)之間的折衷被稱為 **覆蓋索引covering index****包含列的索引index with included columns**其在索引記憶體儲表的一部分列【33】。這允許透過單獨使用索引來處理一些查詢這種情況叫做索引 **覆蓋cover** 了查詢【32】。
2021-08-09 22:26:03 +08:00
與任何型別的資料重複一樣,聚集索引和覆蓋索引可以加快讀取速度,但是它們需要額外的儲存空間,並且會增加寫入開銷。資料庫還需要額外的努力來執行事務保證,因為應用程式不應看到任何因為重複而導致的不一致。
2021-08-09 22:26:03 +08:00
#### 多列索引
至今討論的索引只是將一個鍵對映到一個值。如果我們需要同時查詢一個表中的多個列(或文件中的多個欄位),這顯然是不夠的。
最常見的多列索引被稱為 **連線索引concatenated index** ,它透過將一列的值追加到另一列後面,簡單地將多個欄位組合成一個鍵(索引定義中指定了欄位的連線順序)。這就像一個老式的紙質電話簿,它提供了一個從(姓氏,名字)到電話號碼的索引。由於排序順序,索引可以用來查詢所有具有特定姓氏的人,或所有具有特定姓氏-名字組合的人。但如果你想找到所有具有特定名字的人,這個索引是沒有用的。
2021-08-09 22:26:03 +08:00
2020-10-06 01:21:28 +08:00
**多維索引multi-dimensional index** 是一種查詢多個列的更一般的方法,這對於地理空間資料尤為重要。例如,餐廳搜尋網站可能有一個數據庫,其中包含每個餐廳的經度和緯度。當用戶在地圖上檢視餐館時,網站需要搜尋使用者正在檢視的矩形地圖區域內的所有餐館。這需要一個二維範圍查詢,如下所示:
```sql
SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
AND longitude > -0.1162 AND longitude < -0.1004;
2020-10-06 01:21:28 +08:00
```
一個標準的B樹或者LSM樹索引不能夠高效地處理這種查詢它可以返回一個緯度範圍內的所有餐館但經度可能是任意值或者返回在同一個經度範圍內的所有餐館但緯度可能是北極和南極之間的任意地方但不能同時滿足兩個條件。
2020-10-06 01:21:28 +08:00
一種選擇是使用空間填充曲線將二維位置轉換為單個數字然後使用常規B樹索引【34】。更普遍的是使用特殊化的空間索引例如R樹。例如PostGIS使用PostgreSQL的通用GiST工具【35】將地理空間索引實現為R樹。這裡我們沒有足夠的地方來描述R樹但是有大量的文獻可供參考。
2020-10-06 01:21:28 +08:00
有趣的是多維索引不僅可以用於地理位置。例如在電子商務網站上可以使用建立在維度上的三維索引來搜尋特定顏色範圍內的產品也可以在天氣觀測資料庫中建立日期溫度的二維索引以便有效地搜尋2013年內的溫度在25至30°C之間的所有觀測資料。如果使用一維索引你將不得不掃描2013年的所有記錄不管溫度如何然後透過溫度進行過濾或者反之亦然。 二維索引可以同時透過時間戳和溫度來收窄資料集。這個技術被HyperDex所使用【36】。
2020-10-06 01:21:28 +08:00
#### 全文搜尋和模糊索引
到目前為止所討論的所有索引都假定你有確切的資料,並允許你查詢鍵的確切值或具有排序順序的鍵的值範圍。他們不允許你做的是搜尋類似的鍵,如拼寫錯誤的單詞。這種模糊的查詢需要不同的技術。
2020-10-06 01:21:28 +08:00
例如全文搜尋引擎通常允許搜尋一個單詞以擴充套件為包括該單詞的同義詞忽略單詞的語法變體搜尋在相同文件中彼此靠近的單詞的出現並且支援各種其他取決於文字的語言分析功能。為了處理文件或查詢中的拼寫錯誤Lucene能夠在一定的編輯距離編輯距離1意味著新增刪除或替換了一個字母內搜尋文字【37】。
2020-10-06 01:21:28 +08:00
2021-08-09 22:26:03 +08:00
正如“[用SSTables製作LSM樹](#用SSTables製作LSM樹)”中所提到的Lucene為其詞典使用了一個類似於SSTable的結構。這個結構需要一個小的記憶體索引告訴查詢需要在排序檔案中哪個偏移量查詢鍵。在LevelDB中這個記憶體中的索引是一些鍵的稀疏集合但在Lucene中記憶體中的索引是鍵中字元的有限狀態自動機類似於trie 【38】。這個自動機可以轉換成Levenshtein自動機它支援在給定的編輯距離內有效地搜尋單詞【39】。
其他的模糊搜尋技術正朝著文件分類和機器學習的方向發展。更多詳細資訊請參閱資訊檢索教科書例如【40】。
2020-10-06 01:21:28 +08:00
#### 在記憶體中儲存一切
本章到目前為止討論的資料結構都是對硬碟限制的應對。與主記憶體相比硬碟處理起來很麻煩。對於磁性硬碟和固態硬碟如果要在讀取和寫入時獲得良好效能則需要仔細地佈置硬碟上的資料。但是我們能容忍這種麻煩因為硬碟有兩個顯著的優點它們是持久的它們的內容在電源關閉時不會丟失並且每GB的成本比RAM低。
2021-08-09 22:26:03 +08:00
隨著RAM變得更便宜每GB成本的論據被侵蝕了。許多資料集不是那麼大所以將它們全部儲存在記憶體中是非常可行的包括可能分佈在多個機器上。這導致了記憶體資料庫的發展。
2021-08-09 22:26:03 +08:00
某些記憶體中的鍵值儲存如Memcached僅用於快取在重新啟動計算機時丟失的資料是可以接受的。但其他記憶體資料庫的目標是永續性可以透過特殊的硬體例如電池供電的RAM來實現也可以將更改日誌寫入硬碟還可以將定時快照寫入硬碟或者將記憶體中的狀態複製到其他機器上。
2021-08-09 22:26:03 +08:00
記憶體資料庫重新啟動時,需要從硬碟或透過網路從副本重新載入其狀態(除非使用特殊的硬體)。儘管寫入硬碟,它仍然是一個記憶體資料庫,因為硬碟僅出於永續性目的進行日誌追加,讀取請求完全由記憶體來處理。寫入硬碟同時還有運維上的好外:硬碟上的檔案可以很容易地由外部實用程式進行備份、檢查和分析。
2021-08-09 22:26:03 +08:00
諸如VoltDB、MemSQL和Oracle TimesTen等產品是具有關係模型的記憶體資料庫供應商聲稱透過消除與管理硬碟上的資料結構相關的所有開銷他們可以提供巨大的效能改進【41,42】。 RAM Cloud是一個開源的記憶體鍵值儲存器具有永續性對記憶體和硬碟上的資料都使用日誌結構化方法【43】。 Redis和Couchbase透過非同步寫入硬碟提供了較弱的永續性。
2020-10-06 01:21:28 +08:00
反直覺的是記憶體資料庫的效能優勢並不是因為它們不需要從硬碟讀取的事實。只要有足夠的記憶體即使是基於硬碟的儲存引擎也可能永遠不需要從硬碟讀取因為作業系統在記憶體中快取了最近使用的硬碟塊。相反它們更快的原因在於省去了將記憶體資料結構編碼為硬碟資料結構的開銷【44】。
2021-08-09 22:26:03 +08:00
除了效能記憶體資料庫的另一個有趣的地方是提供了難以用基於硬碟的索引實現的資料模型。例如Redis為各種資料結構如優先順序佇列和集合提供了類似資料庫的介面。因為它將所有資料儲存在記憶體中所以它的實現相對簡單。
2020-10-06 01:21:28 +08:00
最近的研究表明記憶體資料庫體系結構可以擴充套件到支援比可用記憶體更大的資料集而不必重新採用以硬碟為中心的體系結構【45】。所謂的 **反快取anti-caching** 方法透過在記憶體不足的情況下將最近最少使用的資料從記憶體轉移到硬碟並在將來再次訪問時將其重新載入到記憶體中。這與作業系統對虛擬記憶體和交換檔案的操作類似但資料庫可以比作業系統更有效地管理記憶體因為它可以按單個記錄的粒度工作而不是整個記憶體頁面。儘管如此這種方法仍然需要索引能完全放入記憶體中就像本章開頭的Bitcask例子
2020-10-06 01:21:28 +08:00
如果 **非易失性儲存器non-volatile memory, NVM** 技術得到更廣泛的應用可能還需要進一步改變儲存引擎設計【46】。目前這是一個新的研究領域值得關注。
2020-10-06 01:21:28 +08:00
## 事務處理還是分析?
在早期的業務資料處理過程中,一次典型的資料庫寫入通常與一筆 *商業交易commercial transaction* 相對應:賣個貨,向供應商下訂單,支付員工工資等等。但隨著資料庫開始應用到那些不涉及到錢的領域,術語**交易/事務transaction** 仍留了下來,用於指代一組讀寫操作構成的邏輯單元。
2021-08-09 22:26:03 +08:00
> 事務不一定具有ACID原子性一致性隔離性和永續性屬性。事務處理只是意味著允許客戶端進行低延遲的讀取和寫入 —— 而不是隻能定期執行(例如每天一次)的批處理作業。我們在[第七章](ch7.md)中討論ACID屬性在[第十章](ch10.md)中討論批處理。
2020-10-06 01:21:28 +08:00
即使資料庫開始被用於許多不同型別的資料,比如部落格文章的評論,遊戲中的動作,地址簿中的聯絡人等等,基本的訪問模式仍然類似於處理商業交易。應用程式通常使用索引透過某個鍵查詢少量記錄。根據使用者的輸入插入或更新記錄。由於這些應用程式是互動式的,這種訪問模式被稱為**線上事務處理OLTP, OnLine Transaction Processing**。
2021-08-09 22:26:03 +08:00
但是,資料庫也開始越來越多地用於資料分析,這些資料分析具有非常不同的訪問模式。通常,分析查詢需要掃描大量記錄,每個記錄只讀取幾列,並計算彙總統計資訊(如計數、總和或平均值),而不是將原始資料返回給使用者。例如,如果你的資料是一個銷售交易表,那麼分析查詢可能是:
2020-10-06 01:21:28 +08:00
* 一月份每個商店的總收入是多少?
2021-08-09 22:26:03 +08:00
* 在最近的推廣活動中多賣了多少香蕉?
2020-10-06 01:21:28 +08:00
* 哪個牌子的嬰兒食品最常與X品牌的尿布同時購買
這些查詢通常由業務分析師編寫,並提供報告以幫助公司管理層做出更好的決策(商業智慧)。為了將這種使用資料庫的模式和事務處理區分開,它被稱為**線上分析處理OLAP, OnLine Analytice Processing**【47】[^iv]。OLTP和OLAP之間的區別並不總是清晰的但是一些典型的特徵在[表3-1]()中列出。
2021-08-09 22:26:03 +08:00
**表3-1 比較事務處理和分析系統的特點**
2020-10-06 01:21:28 +08:00
| 屬性 | 事務處理系統 OLTP | 分析系統 OLAP |
2020-10-06 01:21:28 +08:00
| :----------: | :--------------------------: | :----------------------: |
| 主要讀取模式 | 查詢少量記錄,按鍵讀取 | 在大批次記錄上聚合 |
| 主要寫入模式 | 隨機訪問,寫入要求低延時 | 批次匯入ETL或者事件流 |
| 主要使用者 | 終端使用者透過Web應用 | 內部資料分析師,用於決策支援 |
2020-10-06 01:21:28 +08:00
| 處理的資料 | 資料的最新狀態(當前時間點) | 隨時間推移的歷史事件 |
| 資料集尺寸 | GB ~ TB | TB ~ PB |
[^iv]: OLAP中的首字母Oonline的含義並不明確它可能是指查詢並不是用來生成預定義好的報告的事實也可能是指分析師通常是互動式地使用OLAP系統來進行探索式的查詢。
起初,事務處理和分析查詢使用了相同的資料庫。 SQL在這方面已證明是非常靈活的對於OLTP型別的查詢以及OLAP型別的查詢來說效果都很好。儘管如此在二十世紀八十年代末和九十年代初期企業有停止使用OLTP系統進行分析的趨勢轉而在單獨的資料庫上執行分析。這個單獨的資料庫被稱為**資料倉庫data warehouse**。
2020-10-06 01:21:28 +08:00
### 資料倉庫
一個企業可能有幾十個不同的交易處理系統:面向終端客戶的網站,控制實體商店的收銀系統,倉庫庫存跟蹤,車輛路線規劃,供應鏈管理,員工管理等。這些系統中每一個都很複雜,需要專人維護,所以最終這些系統互相之間都是獨立執行的。
2020-10-06 01:21:28 +08:00
這些OLTP系統往往對業務運作至關重要因而通常會要求 **高可用****低延遲**。所以DBA會密切關注他們的OLTP資料庫他們通常不願意讓業務分析人員在OLTP資料庫上執行臨時的分析查詢因為這些查詢通常開銷巨大會掃描大部分資料集這會損害同時在執行的事務的效能。
2020-10-06 01:21:28 +08:00
2021-08-19 11:41:47 +08:00
相比之下資料倉庫是一個獨立的資料庫分析人員可以查詢他們想要的內容而不影響OLTP操作【48】。資料倉庫包含公司各種OLTP系統中所有的只讀資料副本。從OLTP資料庫中提取資料使用定期的資料轉儲或連續的更新流轉換成適合分析的模式清理並載入到資料倉庫中。將資料存入倉庫的過程稱為“**抽取-轉換-載入ETL**”,如[圖3-8](../img/fig3-8.png)所示。
2021-08-09 22:26:03 +08:00
2021-08-19 11:41:47 +08:00
![](../img/fig3-8.png)
2021-08-09 22:26:03 +08:00
2020-10-06 01:21:28 +08:00
**圖3-8 ETL至資料倉庫的簡化提綱**
幾乎所有的大型企業都有資料倉庫但在小型企業中幾乎聞所未聞。這可能是因為大多數小公司沒有這麼多不同的OLTP系統大多數小公司只有少量的資料 —— 可以在傳統的SQL資料庫中查詢甚至可以在電子表格中分析。在一家大公司裡要做一些在一家小公司很簡單的事情需要很多繁重的工作。
使用單獨的資料倉庫而不是直接查詢OLTP系統進行分析的一大優勢是資料倉庫可針對分析訪問模式進行最佳化。事實證明本章前半部分討論的索引演算法對於OLTP來說工作得很好但對於處理分析查詢並不是很好。在本章的其餘部分中我們將研究為分析而最佳化的儲存引擎。
2020-10-06 01:21:28 +08:00
#### OLTP資料庫和資料倉庫之間的分歧
資料倉庫的資料模型通常是關係型的因為SQL通常很適合分析查詢。有許多圖形資料分析工具可以生成SQL查詢視覺化結果並允許分析人員探索資料透過下鑽、切片和切塊等操作
2020-10-06 01:21:28 +08:00
表面上一個數據倉庫和一個關係型OLTP資料庫看起來很相似因為它們都有一個SQL查詢介面。然而系統的內部看起來可能完全不同因為它們針對非常不同的查詢模式進行了最佳化。現在許多資料庫供應商都只是重點支援事務處理負載和分析工作負載這兩者中的一個而不是都支援。
2020-10-06 01:21:28 +08:00
一些資料庫例如Microsoft SQL Server和SAP HANA支援在同一產品中進行事務處理和資料倉庫。但是它們也正日益成為兩個獨立的儲存和查詢引擎只是這些引擎正好可以透過一個通用的SQL介面訪問【49,50,51】。
2020-10-06 01:21:28 +08:00
Teradata、Vertica、SAP HANA和ParAccel等資料倉庫供應商通常使用昂貴的商業許可證銷售他們的系統。 Amazon RedShift是ParAccel的託管版本。最近大量的開源SQL-on-Hadoop專案已經出現它們還很年輕但是正在與商業資料倉庫系統競爭包括Apache Hive、Spark SQL、Cloudera Impala、Facebook Presto、Apache Tajo和Apache Drill【52,53】。其中一些基於了谷歌Dremel的想法【54】。
2020-10-06 01:21:28 +08:00
### 星型和雪花型:分析的模式
2021-10-11 19:55:23 +08:00
正如[第二章](ch2.md)所探討的根據應用程式的需要在事務處理領域中使用了大量不同的資料模型。另一方面在分析型業務中資料模型的多樣性則少得多。許多資料倉庫都以相當公式化的方式使用被稱為星型模式也稱為維度建模【55】
2020-10-06 01:21:28 +08:00
[圖3-9](../img/fig3-9.md)中的示例模式顯示了可能在食品零售商處找到的資料倉庫。在模式的中心是一個所謂的事實表(在這個例子中,它被稱為 `fact_sales`)。事實表的每一行代表在特定時間發生的事件(這裡,每一行代表客戶購買的產品)。如果我們分析的是網站流量而不是零售量,則每行可能代表一個使用者的頁面瀏覽或點選。
2020-10-06 01:21:28 +08:00
2021-08-19 11:41:47 +08:00
![](../img/fig3-9.png)
2021-08-09 22:26:03 +08:00
2020-10-06 01:21:28 +08:00
**圖3-9 用於資料倉庫的星型模式的示例**
2021-10-11 19:55:23 +08:00
通常情況下事實被視為單獨的事件因為這樣可以在以後分析中獲得最大的靈活性。但是這意味著事實表可以變得非常大。像蘋果沃爾瑪或eBay這樣的大企業在其資料倉庫中可能有幾十PB的交易歷史其中大部分儲存在事實表中【56】。
2020-10-06 01:21:28 +08:00
事實表中的一些列是屬性,例如產品銷售的價格和從供應商那裡購買的成本(可以用來計算利潤餘額)。事實表中的其他列是對其他表(稱為維度表)的外來鍵引用。由於事實表中的每一行都表示一個事件,因此這些維度代表事件發生的物件、內容、地點、時間、方式和原因。
2021-08-09 22:26:03 +08:00
例如,在[圖3-9](../img/fig3-9.md)中,其中一個維度是已售出的產品。 `dim_product` 表中的每一行代表一種待售產品包括庫存單位SKU、產品描述、品牌名稱、類別、脂肪含量、包裝尺寸等。`fact_sales` 表中的每一行都使用外來鍵表明在特定交易中銷售了什麼產品。 (簡單起見,如果客戶一次購買了幾種不同的產品,則它們在事實表中被表示為單獨的行)。
2021-08-09 22:26:03 +08:00
甚至日期和時間也通常使用維度表來表示,因為這允許對日期的附加資訊(諸如公共假期)進行編碼,從而允許區分假期和非假期的銷售查詢。
2021-08-09 22:26:03 +08:00
“星型模式”這個名字來源於這樣一個事實,即當我們對錶之間的關係進行視覺化時,事實表在中間,被維度表包圍;與這些表的連線就像星星的光芒。
2021-08-09 22:26:03 +08:00
這個模板的變體被稱為雪花模式,其中維度被進一步分解為子維度。例如,品牌和產品類別可能有單獨的表格,並且 `dim_product` 表格中的每一行都可以將品牌和類別作為外來鍵引用,而不是將它們作為字串儲存在 `dim_product` 表格中。雪花模式比星形模式更規範化但是星形模式通常是首選因為分析師使用它更簡單【55】。
在典型的資料倉庫中表格通常非常寬事實表通常有100列以上有時甚至有數百列【51】。維度表也可以是非常寬的因為它們包括了所有可能與分析相關的元資料——例如`dim_store` 表可以包括在每個商店提供哪些服務的細節,它是否具有店內麵包房,店面面積,商店第一次開張的日期,最近一次改造的時間,離最近的高速公路的距離等等。
2021-08-09 22:26:03 +08:00
## 列式儲存
2021-08-09 22:26:03 +08:00
2021-10-11 19:55:23 +08:00
如果事實表中有萬億行和數PB的資料那麼高效地儲存和查詢它們就成為一個具有挑戰性的問題。維度表通常要小得多數百萬行所以在本節中我們將主要關注事實表的儲存。
2021-08-09 22:26:03 +08:00
儘管事實表通常超過100列但典型的資料倉庫查詢一次只會訪問其中4個或5個列`SELECT *` ” 查詢很少用於分析【51】。以[例3-1]()中的查詢為例它訪問了大量的行在2013日曆年中每次都有人購買水果或糖果但只需訪問`fact_sales`表的三列:`date_key, product_sk, quantity`。該查詢忽略了所有其他的列。
2021-08-09 22:26:03 +08:00
**例3-1 分析人們是否更傾向於在一週的某一天購買新鮮水果或糖果**
2021-08-09 22:26:03 +08:00
```sql
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;
2021-08-09 22:26:03 +08:00
```
我們如何有效地執行這個查詢?
在大多數OLTP資料庫中儲存都是以面向行的方式進行佈局的表格的一行中的所有值都相鄰儲存。文件資料庫也是相似的整個文件通常儲存為一個連續的位元組序列。你可以在[圖3-1](../img/fig3-1.png)的CSV例子中看到這個。
2021-08-09 22:26:03 +08:00
為了處理像[例3-1]()這樣的查詢,你可能在 `fact_sales.date_key`、`fact_sales.product_sk`上有索引它們告訴儲存引擎在哪裡查詢特定日期或特定產品的所有銷售情況。但是面向行的儲存引擎仍然需要將所有這些行每個包含超過100個屬性從硬碟載入到記憶體中解析它們並過濾掉那些不符合要求的屬性。這可能需要很長時間。
2021-08-09 22:26:03 +08:00
列式儲存背後的想法很簡單:不要將所有來自一行的值儲存在一起,而是將來自每一列的所有值儲存在一起。如果每個列式儲存在一個單獨的檔案中,查詢只需要讀取和解析查詢中使用的那些列,這可以節省大量的工作。這個原理如[圖3-10](../img/fig3-10.png)所示。
2021-08-09 22:26:03 +08:00
2021-08-19 11:41:47 +08:00
![](../img/fig3-10.png)
2021-08-09 22:26:03 +08:00
**圖3-10 按列儲存關係型資料,而不是行**
2021-08-09 22:26:03 +08:00
> 列式儲存在關係資料模型中是最容易理解的但它同樣適用於非關係資料。例如Parquet【57】是一種列式儲存格式支援基於Google的Dremel的文件資料模型【54】。
2021-08-09 22:26:03 +08:00
列式儲存佈局依賴於每個列檔案包含相同順序的行。 因此如果你需要重新組裝完整的行你可以從每個單獨的列檔案中獲取第23項並將它們放在一起形成表的第23行。
2021-08-09 22:26:03 +08:00
### 列壓縮
除了僅從硬碟載入查詢所需的列以外,我們還可以透過壓縮資料來進一步降低對硬碟吞吐量的需求。幸運的是,列式儲存通常很適合壓縮。
2021-08-09 22:26:03 +08:00
2021-08-19 11:41:47 +08:00
看看[圖3-10](../img/fig3-10.png)中每一列的值序列:它們通常看起來是相當重複的,這是壓縮的好兆頭。根據列中的資料,可以使用不同的壓縮技術。在資料倉庫中特別有效的一種技術是點陣圖編碼,如[圖3-11](../img/fig3-11.png)所示。
2021-08-09 22:26:03 +08:00
2021-08-19 11:41:47 +08:00
![](../img/fig3-11.png)
2021-08-09 22:26:03 +08:00
**圖3-11 壓縮的點陣圖索引儲存佈局**
2021-08-09 22:26:03 +08:00
通常情況下一列中不同值的數量與行數相比要小得多例如零售商可能有數十億的銷售交易但只有100,000個不同的產品。現在我們可以拿一個有 n 個不同值的列,並把它轉換成 n 個獨立的點陣圖每個不同值對應一個位圖每行對應一個位元位。如果該行具有該值則該位為1否則為0。
2021-08-09 22:26:03 +08:00
如果n非常小例如國家/地區列可能有大約200個不同的值則這些點陣圖可以將每行儲存成一個位元位。但是如果n更大大部分點陣圖中將會有很多的零我們說它們是稀疏的。在這種情況下點陣圖可以另外再進行遊程編碼如[圖3-11](fig3-11.png)底部所示。這可以使列的編碼非常緊湊。
2021-08-09 22:26:03 +08:00
這些點陣圖索引非常適合資料倉庫中常見的各種查詢。例如:
```sql
WHERE product_sk IN306869
2021-08-09 22:26:03 +08:00
```
載入`product_sk = 30`、`product_sk = 68`和`product_sk = 69`這三個點陣圖並計算三個點陣圖的按位或OR這可以非常有效地完成。
2021-08-09 22:26:03 +08:00
```sql
WHERE product_sk = 31 AND store_sk = 3
2021-08-09 22:26:03 +08:00
```
載入`product_sk = 31`和`store_sk = 3`的點陣圖並計算按位與AND。這是因為列按照相同的順序包含行因此一列的點陣圖中的第k位和另一列的點陣圖中的第k位對應相同的行。
2021-08-09 22:26:03 +08:00
對於不同種類的資料也有各種不同的壓縮方案但我們不會詳細討論它們請參閱【58】的概述。
2021-08-09 22:26:03 +08:00
> #### 列式儲存和列族
2021-08-09 22:26:03 +08:00
>
> Cassandra和HBase有一個列族column families的概念他們從Bigtable繼承【9】。然而把它們稱為列式column-oriented是非常具有誤導性的在每個列族中它們將一行中的所有列與行鍵一起儲存並且不使用列壓縮。因此Bigtable模型仍然主要是面向行的。
2021-08-09 22:26:03 +08:00
>
#### 記憶體頻寬和向量化處理
2021-08-09 22:26:03 +08:00
對於需要掃描數百萬行的資料倉庫查詢來說一個巨大的瓶頸是從硬盤獲取資料到記憶體的頻寬。但是這不是唯一的瓶頸。分析型資料庫的開發人員還需要有效地利用主儲存器到CPU快取的頻寬避免CPU指令處理流水線中的分支預測錯誤和氣泡以及在現代CPU上使用單指令多資料SIMD指令【59,60】。
2021-08-09 22:26:03 +08:00
除了減少需要從硬碟載入的資料量以外列式儲存佈局也可以有效利用CPU週期。例如查詢引擎可以將大量壓縮的列資料放在CPU的L1快取中然後在緊密的迴圈即沒有函式呼叫中遍歷。相比較每個記錄的處理都需要大量函式呼叫和條件判斷的程式碼CPU執行這樣一個迴圈要快得多。列壓縮允許列中的更多行被放進相同數量的L1快取。前面描述的按位“與”和“或”運算子可以被設計為直接在這樣的壓縮列資料塊上操作。這種技術被稱為向量化處理【58,49】。
2021-08-09 22:26:03 +08:00
### 列式儲存中的排序順序
2021-08-09 22:26:03 +08:00
在列式儲存中儲存行的順序並不一定很重要。按插入順序儲存它們是最簡單的因為插入一個新行只需要追加到每個列檔案。但是我們可以選擇增加一個特定的順序就像我們之前對SSTables所做的那樣並將其用作索引機制。
2021-08-09 22:26:03 +08:00
注意每列獨自排序是沒有意義的因為那樣我們就沒法知道不同列中的哪些項屬於同一行。我們只能在知道一列中的第k項與另一列中的第k項屬於同一行的情況才能重建出完整的行。
2021-08-09 22:26:03 +08:00
相反,即使按列式儲存資料,也需要一次對整行進行排序。資料庫的管理員可以根據他們對常用查詢的瞭解來選擇表格應該被排序的列。例如,如果查詢通常以日期範圍為目標,例如上個月,則可以將 `date_key` 作為第一個排序鍵。這樣查詢最佳化器就可以只掃描上個月的行了,這比掃描所有行要快得多。
2021-08-09 22:26:03 +08:00
對於第一排序列中具有相同值的行,可以用第二排序列來進一步排序。例如,如果 `date_key` 是[圖3-10](../img/fig3-10.png)中的第一個排序關鍵字,那麼 `product_sk` 可能是第二個排序關鍵字,以便同一天的同一產品的所有銷售都將在儲存中組合在一起。這將有助於需要在特定日期範圍內按產品對銷售進行分組或過濾的查詢。
2020-10-06 01:21:28 +08:00
排序順序的另一個好處是它可以幫助壓縮列。如果主要排序列沒有太多個不同的值,那麼在排序之後,它將具有很長的序列,其中相同的值連續重複多次。一個簡單的遊程編碼(就像我們用於[圖3-11](../img/fig3-11.png)中的點陣圖一樣)可以將該列壓縮到幾千位元組 —— 即使表中有數十億行。
2020-10-06 01:21:28 +08:00
第一個排序鍵的壓縮效果最強。第二和第三個排序鍵會更混亂,因此不會有這麼長的連續的重複值。排序優先順序更低的列以基本上隨機的順序出現,所以它們可能不會被壓縮。但前幾列排序在整體上仍然是有好處的。
2020-10-06 01:21:28 +08:00
2021-08-09 22:26:03 +08:00
#### 幾個不同的排序順序
2020-10-06 01:21:28 +08:00
C-Store中引入了這個想法的一個巧妙擴充套件並在商業資料倉庫Vertica中被採用【61,62】。不同的查詢受益於不同的排序順序為什麼不以幾種不同的方式來儲存相同的資料呢無論如何資料需要複製到多臺機器這樣如果一臺機器發生故障你不會丟失資料。你可能還需要儲存以不同方式排序的冗餘資料以便在處理查詢時可以使用最適合查詢模式的版本。
2020-10-06 01:21:28 +08:00
在一個列式儲存中有多個排序順序有點類似於在一個面向行的儲存中有多個次級索引。但最大的區別在於面向行的儲存將每一行儲存在一個地方(在堆檔案或聚集索引中),次級索引只包含指向匹配行的指標。在列式儲存中,通常在其他地方沒有任何指向資料的指標,只有包含值的列。
2020-10-06 01:21:28 +08:00
### 寫入列式儲存
2020-10-06 01:21:28 +08:00
這些最佳化在資料倉庫中是有意義的,因為其負載主要由分析人員執行的大型只讀查詢組成。列式儲存、壓縮和排序都有助於更快地讀取這些查詢。然而,他們的缺點是寫入更加困難。
2020-10-06 01:21:28 +08:00
使用B樹的就地更新方法對於壓縮的列是不可能的。如果你想在排序表的中間插入一行你很可能不得不重寫所有的列檔案。由於行由列中的位置標識因此插入必須對所有列進行一致地更新。
2020-10-06 01:21:28 +08:00
幸運的是本章前面已經看到了一個很好的解決方案LSM樹。所有的寫操作首先進入一個記憶體中的儲存在這裡它們被新增到一個已排序的結構中並準備寫入硬碟。記憶體中的儲存是面向行還是列的並不重要。當已經積累了足夠的寫入資料時它們將與硬碟上的列檔案合併並批次寫入新檔案。這基本上是Vertica所做的【62】。
2020-10-06 01:21:28 +08:00
查詢需要檢查硬碟上的列資料和最近在記憶體中的寫入,並將兩者結合起來。但是,查詢最佳化器對使用者隱藏了這個細節。從分析師的角度來看,透過插入、更新或刪除操作進行修改的資料會立即反映在後續的查詢中。
2020-10-06 01:21:28 +08:00
2021-08-09 22:26:03 +08:00
### 聚合:資料立方體和物化檢視
2020-10-06 01:21:28 +08:00
並不是每個資料倉庫都必定是一個列式儲存傳統的面向行的資料庫和其他一些架構也被使用。然而列式儲存可以顯著加快專門的分析查詢所以它正在迅速變得流行起來【51,63】。
2020-10-06 01:21:28 +08:00
資料倉庫的另一個值得一提的方面是物化彙總materialized aggregates。如前所述資料倉庫查詢通常涉及一個聚合函式如SQL中的COUNT、SUM、AVG、MIN或MAX。如果相同的聚合被許多不同的查詢使用那麼每次都透過原始資料來處理可能太浪費了。為什麼不將一些查詢使用最頻繁的計數或總和快取起來
2020-10-06 01:21:28 +08:00
建立這種快取的一種方式是物化檢視Materialized View。在關係資料模型中它通常被定義為一個標準虛擬檢視一個類似於表的物件其內容是一些查詢的結果。不同的是物化檢視是查詢結果的實際副本會被寫入硬碟而虛擬檢視只是編寫查詢的一個捷徑。從虛擬檢視讀取時SQL引擎會將其展開到檢視的底層查詢中然後再處理展開的查詢。
2020-10-06 01:21:28 +08:00
當底層資料發生變化時物化檢視需要更新因為它是資料的非規範化副本。資料庫可以自動完成該操作但是這樣的更新使得寫入成本更高這就是在OLTP資料庫中不經常使用物化檢視的原因。在讀取繁重的資料倉庫中它們可能更有意義它們是否實際上改善了讀取效能取決於個別情況
2020-10-06 01:21:28 +08:00
2021-08-19 11:41:47 +08:00
物化檢視的常見特例稱為資料立方體或OLAP立方【64】。它是按不同維度分組的聚合網格。[圖3-12](../img/fig3-12.png)顯示了一個例子。
2020-10-06 01:21:28 +08:00
2021-08-19 11:41:47 +08:00
![](../img/fig3-12.png)
2020-10-06 01:21:28 +08:00
2021-08-09 22:26:03 +08:00
**圖3-12 資料立方的兩個維度,透過求和聚合**
2020-10-06 01:21:28 +08:00
想象一下,現在每個事實都只有兩個維度表的外來鍵 —— 在[圖3-12](../img/fig-3-12.png)中分別是日期和產品。你現在可以繪製一個二維表格,一個軸線上是日期,另一個軸線上是產品。每個單元格包含具有該日期-產品組合的所有事實的屬性(例如`net_price`)的聚集(例如`SUM`)。然後,你可以沿著每行或每列應用相同的彙總,並獲得減少了一個維度的彙總(按產品的銷售額,無論日期,或者按日期的銷售額,無論產品)。
2020-10-06 01:21:28 +08:00
一般來說事實往往有兩個以上的維度。在圖3-9中有五個維度日期、產品、商店、促銷和客戶。要想象一個五維超立方體是什麼樣子是很困難的但是原理是一樣的每個單元格都包含特定日期-產品-商店-促銷-客戶組合的銷售額。這些值可以在每個維度上求和彙總。
2020-10-06 01:21:28 +08:00
物化資料立方體的優點是可以讓某些查詢變得非常快,因為它們已經被有效地預先計算了。例如,如果你想知道每個商店的總銷售額,則只需檢視合適維度的總計,而無需掃描數百萬行的原始資料。
2020-10-06 01:21:28 +08:00
資料立方體的缺點是不具有查詢原始資料的靈活性。例如沒有辦法計算有多少比例的銷售來自成本超過100美元的專案因為價格不是其中的一個維度。因此大多數資料倉庫試圖保留儘可能多的原始資料並將聚合資料如資料立方體僅用作某些查詢的效能提升手段。
2020-10-06 01:21:28 +08:00
## 本章小結
在本章中,我們試圖深入瞭解資料庫是如何處理儲存和檢索的。將資料儲存在資料庫中會發生什麼?稍後再次查詢資料時資料庫會做什麼?
2020-10-06 01:21:28 +08:00
在高層次上,我們看到儲存引擎分為兩大類:針對**事務處理OLTP** 最佳化的儲存引擎和針對**線上分析OLAP** 最佳化的儲存引擎。這兩類使用場景的訪問模式之間有很大的區別:
2020-10-06 01:21:28 +08:00
* OLTP系統通常面向終端使用者這意味著系統可能會收到大量的請求。為了處理負載應用程式在每個查詢中通常只訪問少量的記錄。應用程式使用某種鍵來請求記錄儲存引擎使用索引來查詢所請求的鍵的資料。硬碟查詢時間往往是這裡的瓶頸。
* 資料倉庫和類似的分析系統會低調一些因為它們主要由業務分析人員使用而不是終端使用者。它們的查詢量要比OLTP系統少得多但通常每個查詢開銷高昂需要在短時間內掃描數百萬條記錄。硬碟頻寬而不是查詢時間往往是瓶頸列式儲存是針對這種工作負載的日益流行的解決方案。
2020-10-06 01:21:28 +08:00
在OLTP這一邊我們能看到兩派主流的儲存引擎
2020-10-06 01:21:28 +08:00
* 日誌結構學派只允許追加到檔案和刪除過時的檔案但不會更新已經寫入的檔案。Bitcask、SSTables、LSM樹、LevelDB、Cassandra、HBase、Lucene等都屬於這個類別。
* 就地更新學派:將硬碟視為一組可以覆寫的固定大小的頁面。 B樹是這種理念的典範用在所有主要的關係資料庫和許多非關係型資料庫中。
2020-10-06 01:21:28 +08:00
日誌結構的儲存引擎是相對較新的技術。他們的主要想法是,透過系統性地將隨機訪問寫入轉換為硬碟上的順序寫入,由於硬碟驅動器和固態硬碟的效能特點,可以實現更高的寫入吞吐量。
2020-10-06 01:21:28 +08:00
關於OLTP我們最後還介紹了一些更復雜的索引結構以及針對所有資料都放在記憶體裡而最佳化的資料庫。
2020-10-06 01:21:28 +08:00
然後我們暫時放下了儲存引擎的內部細節查看了典型資料倉庫的高階架構並說明了為什麼分析工作負載與OLTP差別很大當你的查詢需要在大量行中順序掃描時索引的重要性就會降低很多。相反非常緊湊地編碼資料變得非常重要以最大限度地減少查詢需要從硬碟讀取的資料量。我們討論了列式儲存如何幫助實現這一目標。
2020-10-06 01:21:28 +08:00
作為一名應用程式開發人員,如果你掌握了有關儲存引擎內部的知識,那麼你就能更好地瞭解哪種工具最適合你的特定應用程式。如果你需要調整資料庫的調整引數,這種理解可以讓你設想一個更高或更低的值可能會產生什麼效果。
2020-10-06 01:21:28 +08:00
儘管本章不能讓你成為一個特定儲存引擎的調參專家,但它至少大概率使你有了足夠的概念與詞彙儲備去讀懂你所選擇的資料庫的文件。
2020-10-06 01:21:28 +08:00
## 參考文獻
1. Alfred V. Aho, John E. Hopcroft, and Jeffrey D. Ullman: *Data Structures and Algorithms*. Addison-Wesley, 1983. ISBN: 978-0-201-00023-8
1. Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein: *Introduction to Algorithms*, 3rd edition. MIT Press, 2009. ISBN: 978-0-262-53305-8
1. Justin Sheehy and David Smith: “[Bitcask: A Log-Structured Hash Table for Fast Key/Value Data](http://basho.com/wp-content/uploads/2015/05/bitcask-intro.pdf),” Basho Technologies, April 2010.
1. Yinan Li, Bingsheng He, Robin Jun Yang, et al.: “[Tree Indexing on Solid State Drives](http://www.vldb.org/pvldb/vldb2010/papers/R106.pdf),” *Proceedings of the VLDB Endowment*, volume 3, number 1, pages 11951206, September 2010.
1. Goetz Graefe: “[Modern B-Tree Techniques](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.219.7269&rep=rep1&type=pdf),” *Foundations and Trends in Databases*, volume 3, number 4, pages 203402, August 2011. [doi:10.1561/1900000028](http://dx.doi.org/10.1561/1900000028)
1. Jeffrey Dean and Sanjay Ghemawat: “[LevelDB Implementation Notes](https://github.com/google/leveldb/blob/master/doc/impl.html),” *leveldb.googlecode.com*.
1. Dhruba Borthakur: “[The History of RocksDB](http://rocksdb.blogspot.com/),” *rocksdb.blogspot.com*, November 24, 2013.
1. Matteo Bertozzi: “[Apache HBase I/O HFile](http://blog.cloudera.com/blog/2012/06/hbase-io-hfile-input-output/),” *blog.cloudera.com*, June, 29 2012.
1. Fay Chang, Jeffrey Dean, Sanjay Ghemawat, et al.: “[Bigtable: A Distributed Storage System for Structured Data](http://research.google.com/archive/bigtable.html),” at *7th USENIX Symposium on Operating System Design and Implementation* (OSDI), November 2006.
1. Patrick O'Neil, Edward Cheng, Dieter Gawlick, and Elizabeth O'Neil: “[The Log-Structured Merge-Tree (LSM-Tree)](http://www.cs.umb.edu/~poneil/lsmtree.pdf),” *Acta Informatica*, volume 33, number 4, pages 351385, June 1996. [doi:10.1007/s002360050048](http://dx.doi.org/10.1007/s002360050048)
1. Mendel Rosenblum and John K. Ousterhout: “[The Design and Implementation of a Log-Structured File System](http://research.cs.wisc.edu/areas/os/Qual/papers/lfs.pdf),” *ACM Transactions on Computer Systems*, volume 10, number 1, pages 2652, February 1992. [doi:10.1145/146941.146943](http://dx.doi.org/10.1145/146941.146943)
2020-10-06 01:21:28 +08:00
1. Adrien Grand: “[What Is in a Lucene Index?](http://www.slideshare.net/lucenerevolution/what-is-inaluceneagrandfinal),” at *Lucene/Solr Revolution*, November 14, 2013.
1. Deepak Kandepet: “[Hacking Lucene—The Index Format]( http://hackerlabs.github.io/blog/2011/10/01/hacking-lucene-the-index-format/index.html),” *hackerlabs.org*, October 1, 2011.
1. Michael McCandless: “[Visualizing Lucene's Segment Merges](http://blog.mikemccandless.com/2011/02/visualizing-lucenes-segment-merges.html),” *blog.mikemccandless.com*, February 11, 2011.
1. Burton H. Bloom: “[Space/Time Trade-offs in Hash Coding with Allowable Errors](http://www.cs.upc.edu/~diaz/p422-bloom.pdf),” *Communications of the ACM*, volume 13, number 7, pages 422426, July 1970. [doi:10.1145/362686.362692](http://dx.doi.org/10.1145/362686.362692)
1. “[Operating Cassandra: Compaction](https://cassandra.apache.org/doc/latest/operating/compaction.html),” Apache Cassandra Documentation v4.0, 2016.
1. Rudolf Bayer and Edward M. McCreight: “[Organization and Maintenance of Large Ordered Indices](http://www.dtic.mil/cgi-bin/GetTRDoc?AD=AD0712079),” Boeing Scientific Research Laboratories, Mathematical and Information Sciences Laboratory, report no. 20, July 1970.
1. Douglas Comer: “[The Ubiquitous B-Tree](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.96.6637&rep=rep1&type=pdf),” *ACM Computing Surveys*, volume 11, number 2, pages 121137, June 1979. [doi:10.1145/356770.356776](http://dx.doi.org/10.1145/356770.356776)
1. Emmanuel Goossaert: “[Coding for SSDs](http://codecapsule.com/2014/02/12/coding-for-ssds-part-1-introduction-and-table-of-contents/),” *codecapsule.com*, February 12, 2014.
1. C. Mohan and Frank Levine: “[ARIES/IM: An Efficient and High Concurrency Index Management Method Using Write-Ahead Logging](http://www.ics.uci.edu/~cs223/papers/p371-mohan.pdf),” at *ACM International Conference on Management of Data* (SIGMOD), June 1992. [doi:10.1145/130283.130338](http://dx.doi.org/10.1145/130283.130338)
1. Howard Chu: “[LDAP at Lightning Speed]( https://buildstuff14.sched.com/event/08a1a368e272eb599a52e08b4c3c779d),” at *Build Stuff '14*, November 2014.
1. Bradley C. Kuszmaul: “[A Comparison of Fractal Trees to Log-Structured Merge (LSM) Trees](http://insideanalysis.com/wp-content/uploads/2014/08/Tokutek_lsm-vs-fractal.pdf),” *tokutek.com*, April 22, 2014.
1. Manos Athanassoulis, Michael S. Kester, Lukas M. Maas, et al.: “[Designing Access Methods: The RUM Conjecture](http://openproceedings.org/2016/conf/edbt/paper-12.pdf),” at *19th International Conference on Extending Database Technology* (EDBT), March 2016. [doi:10.5441/002/edbt.2016.42](http://dx.doi.org/10.5441/002/edbt.2016.42)
2020-10-06 01:21:28 +08:00
1. Peter Zaitsev: “[Innodb Double Write](https://www.percona.com/blog/2006/08/04/innodb-double-write/),” *percona.com*, August 4, 2006.
1. Tomas Vondra: “[On the Impact of Full-Page Writes](http://blog.2ndquadrant.com/on-the-impact-of-full-page-writes/),” *blog.2ndquadrant.com*, November 23, 2016.
1. Mark Callaghan: “[The Advantages of an LSM vs a B-Tree](http://smalldatum.blogspot.co.uk/2016/01/summary-of-advantages-of-lsm-vs-b-tree.html),” *smalldatum.blogspot.co.uk*, January 19, 2016.
1. Mark Callaghan: “[Choosing Between Efficiency and Performance with RocksDB](http://www.codemesh.io/codemesh/mark-callaghan),” at *Code Mesh*, November 4, 2016.
1. Michi Mutsuzaki: “[MySQL vs. LevelDB](https://github.com/m1ch1/mapkeeper/wiki/MySQL-vs.-LevelDB),” *github.com*, August 2011.
1. Benjamin Coverston, Jonathan Ellis, et al.: “[CASSANDRA-1608: Redesigned Compaction](https://issues.apache.org/jira/browse/CASSANDRA-1608), *issues.apache.org*, July 2011.
1. Igor Canadi, Siying Dong, and Mark Callaghan: “[RocksDB Tuning Guide](https://github.com/facebook/rocksdb/wiki/RocksDB-Tuning-Guide),” *github.com*, 2016.
2020-10-06 01:21:28 +08:00
1. [*MySQL 5.7 Reference Manual*](http://dev.mysql.com/doc/refman/5.7/en/index.html). Oracle, 2014.
1. [*Books Online for SQL Server 2012*](http://msdn.microsoft.com/en-us/library/ms130214.aspx). Microsoft, 2012.
1. Joe Webb: “[Using Covering Indexes to Improve Query Performance](https://www.simple-talk.com/sql/learn-sql-server/using-covering-indexes-to-improve-query-performance/),” *simple-talk.com*, 29 September 2008.
1. Frank Ramsak, Volker Markl, Robert Fenk, et al.: “[Integrating the UB-Tree into a Database System Kernel](http://www.vldb.org/conf/2000/P263.pdf),” at *26th International Conference on Very Large Data Bases* (VLDB), September 2000.
1. The PostGIS Development Group: “[PostGIS 2.1.2dev Manual](http://postgis.net/docs/manual-2.1/),” *postgis.net*, 2014.
1. Robert Escriva, Bernard Wong, and Emin Gün Sirer: “[HyperDex: A Distributed, Searchable Key-Value Store](http://www.cs.princeton.edu/courses/archive/fall13/cos518/papers/hyperdex.pdf),” at *ACM SIGCOMM Conference*, August 2012. [doi:10.1145/2377677.2377681](http://dx.doi.org/10.1145/2377677.2377681)
1. Michael McCandless: “[Lucene's FuzzyQuery Is 100 Times Faster in 4.0](http://blog.mikemccandless.com/2011/03/lucenes-fuzzyquery-is-100-times-faster.html),” *blog.mikemccandless.com*, March 24, 2011.
1. Steffen Heinz, Justin Zobel, and Hugh E. Williams: “[Burst Tries: A Fast, Efficient Data Structure for String Keys](http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.18.3499),” *ACM Transactions on Information Systems*, volume 20, number 2, pages 192223, April 2002. [doi:10.1145/506309.506312](http://dx.doi.org/10.1145/506309.506312)
1. Klaus U. Schulz and Stoyan Mihov: “[Fast String Correction with Levenshtein Automata](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.16.652),” *International Journal on Document Analysis and Recognition*, volume 5, number 1, pages 6785, November 2002. [doi:10.1007/s10032-002-0082-8](http://dx.doi.org/10.1007/s10032-002-0082-8)
1. Christopher D. Manning, Prabhakar Raghavan, and Hinrich Schütze: [*Introduction to Information Retrieval*](http://nlp.stanford.edu/IR-book/). Cambridge University Press, 2008. ISBN: 978-0-521-86571-5, available online at *nlp.stanford.edu/IR-book*
1. Michael Stonebraker, Samuel Madden, Daniel J. Abadi, et al.: “[The End of an Architectural Era (Its Time for a Complete Rewrite)](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.137.3697&rep=rep1&type=pdf),” at *33rd International Conference on Very Large Data Bases* (VLDB), September 2007.
1. “[VoltDB Technical Overview White Paper](https://www.voltdb.com/wptechnicaloverview),” VoltDB, 2014.
1. Stephen M. Rumble, Ankita Kejriwal, and John K. Ousterhout: “[Log-Structured Memory for DRAM-Based Storage](https://www.usenix.org/system/files/conference/fast14/fast14-paper_rumble.pdf),” at *12th USENIX Conference on File and Storage Technologies* (FAST), February 2014.
1. Stavros Harizopoulos, Daniel J. Abadi, Samuel Madden, and Michael Stonebraker: “[OLTP Through the Looking Glass, and What We Found There](http://hstore.cs.brown.edu/papers/hstore-lookingglass.pdf),” at *ACM International Conference on Management of Data* (SIGMOD), June 2008. [doi:10.1145/1376616.1376713](http://dx.doi.org/10.1145/1376616.1376713)
2020-10-06 01:21:28 +08:00
1. Justin DeBrabant, Andrew Pavlo, Stephen Tu, et al.: “[Anti-Caching: A New Approach to Database Management System Architecture](http://www.vldb.org/pvldb/vol6/p1942-debrabant.pdf),” *Proceedings of the VLDB Endowment*, volume 6, number 14, pages 19421953, September 2013.
1. Joy Arulraj, Andrew Pavlo, and Subramanya R. Dulloor: “[Let's Talk About Storage & Recovery Methods for Non-Volatile Memory Database Systems](http://www.pdl.cmu.edu/PDL-FTP/NVM/storage.pdf),” at *ACM International Conference on Management of Data* (SIGMOD), June 2015. [doi:10.1145/2723372.2749441](http://dx.doi.org/10.1145/2723372.2749441)
1. Edgar F. Codd, S. B. Codd, and C. T. Salley: “[Providing OLAP to User-Analysts: An IT Mandate](http://www.minet.uni-jena.de/dbis/lehre/ss2005/sem_dwh/lit/Cod93.pdf),” E. F. Codd Associates, 1993.
1. Surajit Chaudhuri and Umeshwar Dayal: “[An Overview of Data Warehousing and OLAP Technology](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/sigrecord.pdf),” *ACM SIGMOD Record*, volume 26, number 1, pages 6574, March 1997. [doi:10.1145/248603.248616](http://dx.doi.org/10.1145/248603.248616)
1. Per-Åke Larson, Cipri Clinciu, Campbell Fraser, et al.: “[Enhancements to SQL Server Column Stores](http://research.microsoft.com/pubs/193599/Apollo3%20-%20Sigmod%202013%20-%20final.pdf),” at *ACM International Conference on Management of Data* (SIGMOD), June 2013.
1. Franz Färber, Norman May, Wolfgang Lehner, et al.: “[The SAP HANA Database An Architecture Overview](http://sites.computer.org/debull/A12mar/hana.pdf),” *IEEE Data Engineering Bulletin*, volume 35, number 1, pages 2833, March 2012.
1. Michael Stonebraker: “[The Traditional RDBMS Wisdom Is (Almost Certainly) All Wrong](http://slideshot.epfl.ch/talks/166),” presentation at *EPFL*, May 2013.
1. Daniel J. Abadi: “[Classifying the SQL-on-Hadoop Solutions](https://web.archive.org/web/20150622074951/http://hadapt.com/blog/2013/10/02/classifying-the-sql-on-hadoop-solutions/),” *hadapt.com*, October 2, 2013.
1. Marcel Kornacker, Alexander Behm, Victor Bittorf, et al.: “[Impala: A Modern, Open-Source SQL Engine for Hadoop](http://pandis.net/resources/cidr15impala.pdf),” at *7th Biennial Conference on Innovative Data Systems Research* (CIDR), January 2015.
1. Sergey Melnik, Andrey Gubarev, Jing Jing Long, et al.: “[Dremel: Interactive Analysis of Web-Scale Datasets](http://research.google.com/pubs/pub36632.html),” at *36th International Conference on Very Large Data Bases* (VLDB), pages 330339, September 2010.
2020-10-06 01:21:28 +08:00
1. Ralph Kimball and Margy Ross: *The Data Warehouse Toolkit: The Definitive Guide to Dimensional Modeling*, 3rd edition. John Wiley & Sons, July 2013. ISBN: 978-1-118-53080-1
1. Derrick Harris: “[Why Apple, eBay, and Walmart Have Some of the Biggest Data Warehouses Youve Ever Seen](http://gigaom.com/2013/03/27/why-apple-ebay-and-walmart-have-some-of-the-biggest-data-warehouses-youve-ever-seen/),” *gigaom.com*, March 27, 2013.
1. Julien Le Dem: “[Dremel Made Simple with Parquet](https://blog.twitter.com/2013/dremel-made-simple-with-parquet),” *blog.twitter.com*, September 11, 2013.
1. Daniel J. Abadi, Peter Boncz, Stavros Harizopoulos, et al.: “[The Design and Implementation of Modern Column-Oriented Database Systems](http://cs-www.cs.yale.edu/homes/dna/papers/abadi-column-stores.pdf),” *Foundations and Trends in Databases*, volume 5, number 3, pages 197280, December 2013. [doi:10.1561/1900000024](http://dx.doi.org/10.1561/1900000024)
1. Peter Boncz, Marcin Zukowski, and Niels Nes: “[MonetDB/X100: Hyper-Pipelining Query Execution](http://www.cidrdb.org/cidr2005/papers/P19.pdf),” at *2nd Biennial Conference on Innovative Data Systems Research* (CIDR), January 2005.
1. Jingren Zhou and Kenneth A. Ross: “[Implementing Database Operations Using SIMD Instructions](http://www1.cs.columbia.edu/~kar/pubsk/simd.pdf),” at *ACM International Conference on Management of Data* (SIGMOD), pages 145156, June 2002. [doi:10.1145/564691.564709](http://dx.doi.org/10.1145/564691.564709)
1. Michael Stonebraker, Daniel J. Abadi, Adam Batkin, et al.: “[C-Store: A Column-oriented DBMS](http://www.vldb2005.org/program/paper/thu/p553-stonebraker.pdf),” at *31st International Conference on Very Large Data Bases* (VLDB), pages 553564, September 2005.
2020-10-06 01:21:28 +08:00
1. Andrew Lamb, Matt Fuller, Ramakrishna Varadarajan, et al.: “[The Vertica Analytic Database: C-Store 7 Years Later](http://vldb.org/pvldb/vol5/p1790_andrewlamb_vldb2012.pdf),” *Proceedings of the VLDB Endowment*, volume 5, number 12, pages 17901801, August 2012.
1. Julien Le Dem and Nong Li: “[Efficient Data Storage for Analytics with Apache Parquet 2.0](http://www.slideshare.net/julienledem/th-210pledem),” at *Hadoop Summit*, San Jose, June 2014.
1. Jim Gray, Surajit Chaudhuri, Adam Bosworth, et al.: “[Data Cube: A Relational Aggregation Operator Generalizing Group-By, Cross-Tab, and Sub-Totals](http://arxiv.org/pdf/cs/0701155.pdf),” *Data Mining and Knowledge Discovery*, volume 1, number 1, pages 2953, March 2007. [doi:10.1023/A:1009726021843](http://dx.doi.org/10.1023/A:1009726021843)
------
| 上一章 | 目錄 | 下一章 |
| ------------------------------------ | ------------------------------- | ---------------------------- |
| [第二章:資料模型與查詢語言](ch2.md) | [設計資料密集型應用](README.md) | [第四章:編碼與演化](ch4.md) |