Mysql存储结构B树与B+树与索引
首先要說明的是,B-樹和B樹是指同一個結(jié)構(gòu),并沒有所謂的B減樹,兩種樹是B-樹和B+樹。
Mysql存儲結(jié)構(gòu)是一個B+樹。
1.存儲結(jié)構(gòu)與索引
眾所周知,索引是關(guān)系型數(shù)據(jù)庫中給數(shù)據(jù)庫表中一列或多列的值排序后的存儲結(jié)構(gòu),它是一種加快查詢速度的數(shù)據(jù)結(jié)構(gòu),常用索引結(jié)構(gòu)有hash、B-Tree和B+Tree,Mysql選用的是B+樹索引。
1)Hash
hash是基于哈希表完成索引存儲,哈希表特性是數(shù)據(jù)存放是散列的。
優(yōu)點:
等值查詢快,通過hash值直接定位到具體的數(shù)據(jù)。
缺點:
2)B-Tree
a)
B樹(或B-樹、B_樹),它是一種m階平衡多叉樹。當m取2時,便是二叉搜索樹,其中m指的是一個結(jié)點最多有多少個孩子結(jié)點。
? ? 對于m階B樹,其具有如下性質(zhì):
如下圖所示:
上圖為3階B樹,在實際應用中的B樹的階數(shù)m都非常大(通常大于100),所以即使存儲大量的數(shù)據(jù),B樹的高度仍然比較小,這有利于樹的插入刪除。在數(shù)據(jù)庫中我們將B樹(和B+樹)作為索引結(jié)構(gòu),可以加快查詢速速。
? B樹的插入
?如果插入的結(jié)點只有一個數(shù)值,直接在該結(jié)點插入即可。例如,在上圖中插入9,則直接在10結(jié)點前面插入9即可。但如果插入44,此時便需要通過結(jié)點的向上分裂來完成插入。
? ?? 插入44:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ???
? ? 發(fā)現(xiàn)此結(jié)點有3個值,不滿足3階B樹,因此要進行分裂,將中間的40向上結(jié)點移動:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ???
? ? 分裂后此B樹變成了4階B樹,不滿足3階B樹條件,原因是40移動到上結(jié)點所致,因此繼續(xù)向上結(jié)點移動,將50移動到上節(jié)點:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
? ?? 此時發(fā)現(xiàn)又出現(xiàn)3個值的結(jié)點,繼續(xù)進行分裂:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
? ? ? 此時便滿足條件,完成。
B樹的刪除按照插入的方法反過來操作即可,即父結(jié)點(如果不符合父結(jié)點大于左結(jié)點小于右結(jié)點的條件,則與上層父節(jié)點位置調(diào)換,直到符合條件為止)不斷下移合并,直到符合條件為止。
b)
B樹在磁盤存儲中:
B-Tree特點:
3)B+Tree
a)
在B+樹中,只有葉子節(jié)點存儲數(shù)據(jù),其它中間結(jié)點全部是索引。在數(shù)據(jù)庫的聚集索引中,葉子節(jié)點直接包含數(shù)據(jù)庫中某一行數(shù)據(jù)。在非聚集索引中,葉子節(jié)點帶有指向數(shù)據(jù)庫行的指針。
B+樹是B樹的一種變體,有著比B樹更高的查詢性能,B+樹和B樹除了有一些共同特點外,還有一些新的特點:
?? 下面使用數(shù)值來表示一棵B+樹:
可以看出,B+樹的每個結(jié)點的最大或最小元素都出現(xiàn)在下一個結(jié)點的首或尾
B+樹的查找
? ?? B+樹的查找有兩種方式:從最小值進行順序查找;從根結(jié)點開始,進行隨機查找。在查找時,若非終端結(jié)點上的關(guān)鍵值等于給定值,并不終止,而是繼續(xù)向下直到葉子結(jié)點(因為葉子結(jié)點才存數(shù)據(jù))。因此,在B+樹中,不管查找成功與否,每次查找都是走了一條從根到葉子結(jié)點的路徑。其余同B-樹的查找類似。
? ?? 由于B+樹的數(shù)據(jù)都存儲在葉子結(jié)點中,分支結(jié)點均為索引,方便掃庫,只需要掃一遍葉子結(jié)點即可,但是B樹因為其分支結(jié)點同樣存儲著數(shù)據(jù),我們要找到具體的數(shù)據(jù),需要進行一次中序遍歷按序來掃,所以B+樹更加適合在區(qū)間查詢的情況,所以通常B+樹用于數(shù)據(jù)庫索引,而B樹則常用于文件索引。
B+樹的插入
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
假設我們要向上圖插入0,發(fā)現(xiàn)沒有破壞B+樹結(jié)構(gòu),直接在1,2結(jié)點處插入即可。
如果在結(jié)點的中間插入并破壞了B+樹的結(jié)構(gòu):
但是如果我們要插入12,則發(fā)現(xiàn)破壞了B+樹的結(jié)構(gòu),則:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
分裂破壞了結(jié)構(gòu)的結(jié)點,并將12移到上結(jié)點:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ???
插入完畢。
如果在端點處插入并破壞了B+樹的結(jié)構(gòu):
假如插入16:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ???
分裂后,父結(jié)點要配合子結(jié)點的端點值:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
刪除操作,只需將插入操作進行反向操作即可。讀者可以想想如何刪除16。
B+數(shù)的優(yōu)勢:
b)
Mysql存儲中
B+Tree 是在B-Tree的基礎(chǔ)之上做的一種優(yōu)化,變化如下:
Mysql為什么選擇B+Tree
Mysql官網(wǎng)文檔中寫到InnoDB索引用的是 B-tree,但是底層用的是B+Tree。Mysql存儲數(shù)據(jù)是以頁為單位,默認一個頁可以存放16K數(shù)據(jù)。假設B-Tree和B+Tree都是3層深度,表中每個記錄為1K(假設的,一般不會這么大,別較真),那么三層深度的B-Tree存儲 16 x 16 x 16 = 4096(比這個數(shù)還要少,因為每個頁中還要存放指針和其它的數(shù)據(jù))。B+Tree第一、二層存放的是key,假設是Long類型的主鍵,那么第一、二層每頁存放數(shù)據(jù)約為 16 x 1024 / 8 = 2048,三層深度可以存放 2048 x 2048 x 16 = 6700W。MySQL查詢過程是按頁加載數(shù)據(jù)的,每加載一頁就是一次IO操作,B+Tree進行三次IO可以查詢6700W數(shù)據(jù)量。從這里也可以知道Mysql一般設置三層深度就足夠了。
2.聚集索引與非聚集索引
聚集(clustered)索引,也叫聚簇索引
定義:數(shù)據(jù)行的物理順序與列值(一般是主鍵的那一列)的邏輯順序相同,一個表中只能擁有一個聚集索引。
打個比方,把數(shù)據(jù)表比作新華字典,聚集索引就是拼音目錄,而每個字存放的頁碼就是實際的數(shù)據(jù)物理地址,如果要查詢一個“草”字,我們只需要查詢“草”字對應在新華字典拼音目錄對應的頁碼,就可以查詢到對應的“草”字所在的位置,而拼音目錄對應的A-Z的字順序,和新華字典實際存儲的字的順序A-Z也是一樣的,如果我們中文新出了一個字,拼音開頭第一個是B,那么他插入的時候也要按照拼音目錄順序插入到A字的后面。
如下表所示:
第一列的地址表示該行數(shù)據(jù)在磁盤中的物理地址,后面三列表示數(shù)據(jù)庫表的真實數(shù)據(jù),其中id是主鍵,建立了聚集索引
數(shù)據(jù)行的物理順序與列值的順序相同,如果我們查詢id比較靠后的數(shù)據(jù),那么這行數(shù)據(jù)的地址在磁盤中的物理地址也會比較靠后。而且由于物理排列方式與聚集索引的順序相同,所以只能建立一個聚集索引。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 聚集索引實際存放的示意圖
從上圖可以看出聚集索引的好處了,索引的葉子節(jié)點就是對應的數(shù)據(jù)節(jié)點(MySQL的MyISAM除外,此存儲引擎的聚集索引和非聚集索引只多了個唯一約束,其他沒什么區(qū)別),可以直接獲取到對應的全部列的數(shù)據(jù),而非聚集索引在索引沒有覆蓋到對應的列的時候需要進行二次查詢。因此在查詢方面,聚集索引的速度往往會更占優(yōu)勢
非聚集索引
定義:該索引中索引的邏輯順序與磁盤上行的物理存儲順序不同,一個表中可以擁有多個非聚集索引。
按照上述比喻,非聚集索引就像新華字典的偏旁字典,存放的結(jié)構(gòu)順序與實際存放順序不一定一致。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 非聚集索引實際存放的示意圖
非聚集索引的二次查詢問題
非聚集索引葉節(jié)點仍然是索引節(jié)點,只是有一個指針指向?qū)臄?shù)據(jù)塊,此如果使用非聚集索引查詢,而查詢列中包含了其他該索引沒有覆蓋的列,那么他還要進行第二次的查詢,查詢節(jié)點上對應的數(shù)據(jù)行的數(shù)據(jù)。
如有以下表t1
以及聚集索引clustered index(id), 非聚集索引index(username)。
使用以下語句進行查詢,不需要進行二次查詢,直接就可以從非聚集索引的節(jié)點里面就可以獲取到查詢列的數(shù)據(jù)。
select id, username from t1 where username = '小明' select username from t1 where username = '小明'但是使用以下語句進行查詢,就需要二次的查詢?nèi)カ@取原數(shù)據(jù)行的score:
select username, score from t1 where username = '小明'如何解決非聚集索引的二次查詢(回表查詢)問題
復合索引(覆蓋索引):將被查詢的字段,建立到聯(lián)合索引里去。
建立兩列以上的索引,即可查詢復合索引里的列的數(shù)據(jù)而不需要進行回表二次查詢,如index(col1, col2),執(zhí)行下面的語句
select col1, col2 from t1 where col1 = '213';要注意使用復合索引需要滿足最左側(cè)索引的原則,也就是查詢的時候如果where條件里面沒有最左邊的一到多列,索引就不會起作用。
總結(jié):
下面舉一個具體的例子
?
InnoDB聚集索引和普通索引有什么差異?
InnoDB聚集索引的葉子節(jié)點存儲行記錄,因此, InnoDB必須要有,且只有一個聚集索引:
(1)如果表定義了PK,則PK就是聚集索引;
(2)如果表沒有定義PK,則第一個not NULL unique列是聚集索引;
(3)否則,InnoDB會創(chuàng)建一個隱藏的row-id作為聚集索引;
畫外音:所以PK查詢非常快,直接定位行記錄。
InnoDB普通索引的葉子節(jié)點存儲主鍵值。
畫外音:注意,不是存儲行記錄頭指針,MyISAM的索引葉子節(jié)點存儲記錄指針。
舉個栗子,不妨設有表:
t(id PK, name KEY, sex, flag);
畫外音:id是聚集索引,name是普通索引。
?
表中有四條記錄:
1, shenjian, m, A
3, zhangsan, m, A
5, lisi, m, A
9, wangwu, f, B
兩個B+樹索引分別如上圖:
(1)id為PK,聚集索引,葉子節(jié)點存儲行記錄;
(2)name為KEY,普通索引,葉子節(jié)點存儲PK值,即id;
?
既然從普通索引無法直接定位行記錄,那普通索引的查詢過程是怎么樣的呢?
通常情況下,需要掃碼兩遍索引樹。
?
例如:
| 1 | select?*?from?t?where?name='lisi'; |
是如何執(zhí)行的呢?
如粉紅色路徑,需要掃碼兩遍索引樹:
(1)先通過普通索引定位到主鍵值id=5;
(2)在通過聚集索引定位到行記錄;
?
這就是所謂的回表查詢,先定位主鍵值,再定位行記錄,它的性能較掃一遍索引樹更低。
提高效率,索引覆蓋!如何實現(xiàn)索引覆蓋:常見的方法是:將被查詢的字段,建立到聯(lián)合索引里去。
仍是之前中的例子:
| 1 2 3 4 5 6 | create?table?user?( ????id?int?primary?key, ????name?varchar(20), ????sex?varchar(5), ????index(name) )engine=innodb; |
1. 第一個sql?
| 1 | select?id,name?from?user?where?name='shenjian'; |
能夠命中name索引,索引葉子節(jié)點存儲了主鍵id,通過name的索引樹即可獲取id和name,無需回表,符合索引覆蓋,效率較高。
2. 第二個sql?
| 1 | select?id,name,sex?from?user?where?name='shenjian'; |
能夠命中name索引,索引葉子節(jié)點存儲了主鍵id,但sex字段必須回表查詢才能獲取到,不符合索引覆蓋,需要再次通過id值掃碼聚集索引獲取sex字段,效率會降低。
如果把(name)單列索引升級為聯(lián)合索引(name, sex)就不同了。
| 1 2 3 4 5 6 | create?table?user?( ????id?int?primary?key, ????name?varchar(20), ????sex?varchar(5), ????index(name, sex) )engine=innodb; |
可以看到:
| 1 2 3 | select?id,name?...?where?name='shenjian'; ? select?id,name,sex ...?where?name='shenjian'; |
都能夠命中索引覆蓋,無需回表。
?
索引覆蓋優(yōu)化SQL場景
場景1:全表count查詢優(yōu)化?
關(guān)于這點的解釋,可以參考這里,count(*)的查詢:
其原因是:在innodb中,非主鍵索引葉子節(jié)點存儲的結(jié)構(gòu)是:索引+主鍵;主鍵索引葉子節(jié)點是:主鍵+表數(shù)據(jù)。在1個page里面,非主鍵索引可以存儲更多的條目,例如:
對于一張表,如果有1000000數(shù)據(jù),使用非主鍵索引掃描的page數(shù)可能是100 ,而使用主鍵索引page數(shù)可能是500,此時使用非主鍵索引的性能會更好。同理如果存在多個非主鍵索引,會使用一個最小的非主鍵索引,也是為了在一個page里存儲更多的數(shù)據(jù),從而減少掃描次數(shù),提高性能。
這里使用的是單字段count()查詢,意思差不多。
原表為:
user(PK id, name, sex);
直接:
| 1 | select?count(name)?from?user; |
不能利用索引覆蓋。
添加索引:
| 1 | alter?table?user?add?key(name); |
就能夠利用索引覆蓋提效。
場景2:列查詢回表優(yōu)化
| 1 | select?id,name,sex ...?where?name='shenjian'; |
這個例子不再贅述,將單列索引(name)升級為聯(lián)合索引(name, sex),即可避免回表。
場景3:分頁查詢
| 1 | select?id,name,sex ...?order?by?name?limit 500,100; |
將單列索引(name)升級為聯(lián)合索引(name, sex),也可以避免回表。
最后這一段
總結(jié)
以上是生活随笔為你收集整理的Mysql存储结构B树与B+树与索引的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: dosbox运行C语言,DOSBox怎么
- 下一篇: 在循环里创建数据库连接,严重影响数据库性