CMU Database Systems - Indexes

這章主要描述索引,即經過什麼樣的數據結構能夠更加快速的查詢到數據 html

介紹Hash Tables,B+tree,SkipListnode

以及索引的並行訪問數據庫

Hash Tables

hash tables能夠實現O(1)的查詢,設計主要考慮兩點數組

 

首先用什麼hash function?底下列出經常使用的hash function數據結構

 

而後怎麼解決collisions?即hash schemes多線程

首先是static hash schemes併發

第一個方法是Linear probe hashingide

方法,若是發現衝突,就日後找,直到找到一個free的slot,因此要同時記錄下key和value,這樣纔好去比對每一個key是否是要找的函數

問題如圖,會出現bad case,好比對於E,須要跳不少步才能找到,這樣查詢就從O(1)變成O(n)了性能

Robin Hood Hashing

像名字同樣,羅賓漢,劫富濟貧,解決badcase

首先存儲到時候,要加上跳數,jump幾回;而後根據jump數比較,來判斷是否要作平均

左圖,若是不用robin hood方式,D爲1跳,E爲3跳

右圖,用robin hood後,D和E都變成2跳

Cuckoo Hashing

簡單的說,用多個hash table,對於一個數據,哪邊空就存哪邊

可是對於當前這個C,兩邊都衝突,他解決的思路是,

C兩邊都衝突那確定是解不了,那我先把C隨便存一邊,這樣如圖,咱們就要解決B的衝突

B只能去替換A,最終A能夠存在另外一張表裏面,因此衝突解決

這個方法明顯的好處是查詢路徑短,最多兩次

問題是,插入性能容易比較差,若是衝突比較多,有可能死循環,因此若是出現這些狀況,就要去下降衝突率,好比增長hash table的大小,或者增長hashtable的個數

但這樣變化後,須要徹底從新rebuild

 

 

static hash schemes的問題就是,容量有限,一旦超出擴容的話,就須要整個索引徹底rebuild

因此就須要Dynamic hash table

最直白的想法是Chained Hashing,hash值對應的是buckets,不存在collision,由於buckets是能夠無限擴展的

問題是,數據多了,就變成O(n)了

上面的方法的問題是,隨着數據的增多不斷的增長bucket,可是沒有沒有增長目錄大小,最終對應到目錄中一條的數據愈來愈多,失去了index的意義

 

Extendible hashing

這個方法難理解些,

首先這裏的hash函數是根據前幾位去分bucket,depth的意思是幾位

好比,用2位分bucket,目錄大小爲4,不夠了就用3位去分bucket,目錄大小就是8

Global depth是目錄用幾位,local depth其實只是個標識,表示這個bucket用的是幾位,

由於只要有一個分區的bucket滿了,而且若是這個分區只對應一個目錄item,那麼目錄就須要擴展,global depth會增長

好比下圖中,00指向bucketA,已經4個數滿了,還要加一個,只有分離bucket,這個時候就須要擴展目錄,global depth=3,其中000,001分別指向一個bucket

可是其餘的bucket沒滿啊,因此他們的local depth仍是2,而且在新的目錄中,有兩個item指向depth爲2的bucket

但這時來個9,落在bucketB,B也滿了,須要分裂,可是這個時候就不須要擴展目錄,由於B自己就有兩個目錄item指向,正好能夠分開

Linear Hashing

這個的思路也是逐步的分裂bucket,但他和extendible hashing相比,不須要維護這個目錄

http://queper.in/drupal/blogs/dbsys/linear_hashing,這個連接裏面的例子很是清楚

基本思想,

初始bucket數是4,按4取模分bucket,很容易理解
問題是,若是有某個bucket滿了,怎麼處理?

首先把overflow的數據用一個臨時bucket存下來

而後接着要作bucket分裂,

這裏bucket分裂的過程比較trick是逐步完成的,最終達到的是bucket翻倍

由於要一個個bucket分裂,因此這裏有個split point,表示當前分裂到哪一個bucket了

這裏很難理解的是,他不是分裂overflow的那個bucket,並且按順序一個個分裂;感受一次把全部bucket全split掉,也沒啥問題

如圖,bucket 1 滿了,但他是分裂當前split point指向的bucket 0,分紅0和4,而且split point + 1

分裂的時候用的hash函數是下一輪的函數,如圖的hash2

這裏每overflow一次,就按順序分裂一個bucket,當split point爲4的時候,即分裂完一輪了,當前bucket=8

把split point重置成0,開始下一輪,每輪的bucket數翻倍,因此hash函數中的取餘的數也要翻倍,很容易理解

這樣就實現了動態擴展

 

Tree Indexes

https://www.cnblogs.com/fxjwind/archive/2012/06/09/2543357.html

咱們說的B tree,每每說的都是B+ tree;B-tree和B+tree的區別就在於inner node是否存儲數據

 

B+樹有以下的特性,

m叉而非二叉,能夠有效下降樹高

每一個inner node至少是half-full,這樣提升讀取效率,讀取一個節點,每每是一個page,能夠讀取儘量多的數據

inner node,對於k個keys,要有k+1個非null子節點

B+數據的結構以下,

分爲inner nodes和leaf nodes

inner nodes只有索引,而leaf nodes包含真實數據,並且還有sibling pointers,這是爲了更有效的range 查詢

Leaf node的內容也分爲兩種

Leaf Node的結構以下,

 兩種kv不一樣的存儲格式,這裏pageId,須要理解一下,由於每每B+數的一個節點對應於一個page,因此跳到下一個節點,就是跳到另外一個page

B+樹的insert和delete,

Insert,關鍵就是node滿了,須要分裂,分裂完要把middle key放到上一層節點中作索引,若是上一層節點也滿了,就須要進一步分裂

delete,關鍵是若是delete後,節點小於half-full,須要先試圖從sibling去借一些達到,half-full,若是sibling也達不到half-full,那麼就merge 

 

Clustered Indexes

http://baijiahao.baidu.com/s?id=1598257553176708891&wfr=spider&for=pc

clustered indexes是B+樹的應用,

在Innodb裏面,每一個表都有一個聚簇索引,該索引是根據primary key對行記錄生成的B+樹索引,若是沒有primary key,會生成自增id做爲替代;

葉子節點存放的是行數據,稱之爲數據頁,故表中的數據也是聚簇索引中的一部分,數據頁之間經過一個雙向鏈表來連接

除了Clustered Indexes之外,都稱爲Secondary Indexes,與聚簇索引的區別在於輔助索引的葉子節點中存放的是主鍵的鍵值

Clustered indexes只有一個,可是輔助索引卻能夠有多個

可想而知,經過輔助索引只能查到主鍵id,因此若是你要讀到數據,還要再查一次聚簇索引;好處是由於輔助索引不包含數據,因此遠小於聚簇索引,查詢效率比較高

能夠用一列,也能夠用多列來建立輔助索引,稱爲聯合索引,
聯合索引和普通索引的結構沒有不一樣,只是會在節點中同時記錄下多個列的值,遵循最左原則,就是先按第一個列排序,再按第二個列排序。。。。。。

因此查詢條件,也須要知足最左原則,不然沒法使用索引

對於變長的keys和重複的keys

節點內的search方法,

其餘B+樹還有些優化,

索引覆蓋,CoveringIndexes

查詢須要的數據,均可以在索引中獲取到,不須要讀取原始tuple

 

SkipList,跳錶

http://www.cnblogs.com/seniusen/p/9870398.html

若是用有序的數組來實現索引,能夠簡單的用二分查找,可是插入和刪除數據會比較麻煩;

最簡單的方法實現動態保序的index的方法是用有序鏈表,但鏈表的只支持線性搜索,時間複雜度爲O(n)

如何讓鏈表也能二分查找,提升查詢效率,這就是skiplist

跳錶的數據存在第一層,上面的都是索引,想法很簡單,避免一個個遍歷,越往上層建的索引越稀疏,總之就是爲了模擬出二分查找,空間換時間,因此時間複雜度能夠近似O(logn)

跳錶比較有意思的是他的insert過程,

好比插入,k5v5,這裏關鍵是如何創建索引?即要把K5加到哪幾層裏面

這裏的答案是flip coin,就是一個伯努利過程

連續拋硬幣,連續出現正面的次數爲k,咱們就會對前k層創建索引

若是k大於當前最大的level,就須要建立新的level

這有兩個好處,

由於是伯努利過程,因此天然約高的level出現機率越低,以1/2下降

而且插入的數據越多,出現較大k的機率越大,由於較大的k是小几率事件

這個設計的仍是很是精巧的,和loglogcounting相似的思路

跳錶的查詢就比較直觀了,二分查找下來就ok

跳錶的刪除,也不難理解,關鍵是須要一個標識,先邏輯刪除,再物理刪除

圖中就是剛完成邏輯刪除,物理刪除就是把這些節點真正刪掉

跳錶的優缺點

 

Radix Tree

核心的思路,前綴樹,節點的path就表明key,能夠reconstructed

樹高,取決於key的length,而不是key的多少;其實key多了,表示key的length確定要變長,同樣的

不須要rebalance

Radix Tree的例子,看出和Trie的區別

Radix只有共享的才須要單獨的節點

Radix的優勢,就是插入和刪除特別簡單

 

Index Concurrency Control

討論多線程併發訪問索引
其中Logical correctness是指應用層的,transaction
而這裏主要討論的是Physical correctness,內部數據結構的併發訪問

這裏再解釋在數據庫領域,lock和latch的區別

既然討論的是內部數據結構的併發控制,那用的就是latch

Latch分爲兩種類型,讀和寫

從表中能夠看出來,咱們要解決寫寫,和讀寫衝突,讀讀是沒有衝突的

對於數據庫中主要的索引結構,B+tree

解決衝突的方式稱爲,Latch Crabbing/Coupling

這個其實很容易理解,

讀比較簡單,從root開始,只要能獲取到child的latch,就能夠釋放parent

更新複雜些,由於我更新當前節點,可能會致使split和merge,這樣父節點也須要更新
因此要同時獲取父子兩層的latch,只有當不會發生split和merge,因此沒有必要改動父節點,safe,才釋放父節點的latch

Delete的例子,對於delete咱們要考慮的是否要merge

因此在B的時候,咱們不能釋放A的Latch,由於B只有一個35,可能在delete的過程當中須要merge
而到了C,C有38,44,刪除一個也不會致使merge,因此能夠釋放A,B的latch

一樣對於Insert,咱們要考慮的是split
B的時候,有一個空,沒有split的風險,釋放A
但到D的時候,有split的風險,不能釋放B,到I發現有空,不須要split,釋放B,D

 這個Case到F的時候,發現要split,因此不能釋放C的latch,C也要增長節點

這個方法有個顯著的問題,

Root會成爲明顯的瓶頸,覺得全部鎖都要從root開始鎖起

優化的思路,

這有個假設,就是大部分更新是不須要,split和merge的,不然效率反而更低了

若是不須要split和merge,就沒有必要給parent加寫latch,用讀latch就能夠;用讀latch,首先避免每次都在root寫寫衝突,由於讀讀是不衝突的,並且又保證了讀寫衝突,由於別人在更新的同時,你須要等待的

 

Leaf Node Scan

前面說的latch的方式都是Top-down的,因此不會產生死鎖,你們加鎖的方向一致的,不會造成環

可是B+tree在leaf node之間也是有pointer的,這就會造成環

讀的場景,不衝突

寫的場景,就會產生衝突

T2,衝突的時候,有兩個選擇,一個是等,一個是自殺(防止死鎖)
由於T2不知道T1在幹啥,因此合理的方式是,等一個timeout,而後自殺,這樣能夠有效避免死鎖

 

Delayed parent updates

 延遲對於parent的變動,這樣會更有效

後續會有線程單獨的來更新parent

對於parent的更新能夠批量,而且下降寫latch的衝突的機率

相關文章
相關標籤/搜索