這章主要描述索引,即經過什麼樣的數據結構能夠更加快速的查詢到數據 html
介紹Hash Tables,B+tree,SkipListnode
以及索引的並行訪問數據庫
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函數中的取餘的數也要翻倍,很容易理解
這樣就實現了動態擴展
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
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
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
跳錶的刪除,也不難理解,關鍵是須要一個標識,先邏輯刪除,再物理刪除
圖中就是剛完成邏輯刪除,物理刪除就是把這些節點真正刪掉
跳錶的優缺點
核心的思路,前綴樹,節點的path就表明key,能夠reconstructed
樹高,取決於key的length,而不是key的多少;其實key多了,表示key的length確定要變長,同樣的
不須要rebalance
Radix Tree的例子,看出和Trie的區別
Radix只有共享的才須要單獨的節點
Radix的優勢,就是插入和刪除特別簡單
討論多線程併發訪問索引
其中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的衝突的機率