日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

04.深入淺出索引(上)undefined

 終為始 2019-08-05

提到數(shù)據(jù)庫索引,我想你并不陌生,在日常工作中會(huì)經(jīng)常接觸到。比如某一個(gè)SQL查詢比較慢,分析完原因之后,你可能就會(huì)說“給某個(gè)字段加個(gè)索引吧”之類的解決方案。但到底什么是索引,索引又是如何工作的呢?今天就讓我們一起來聊聊這個(gè)話題吧。

數(shù)據(jù)庫索引的內(nèi)容比較多,我分成了上下兩篇文章。索引是數(shù)據(jù)庫系統(tǒng)里面最重要的概念之一,所以我希望你能夠耐心看完。在后面的實(shí)戰(zhàn)文章中,我也會(huì)經(jīng)常引用這兩篇文章中提到的知識(shí)點(diǎn),加深你對(duì)數(shù)據(jù)庫索引的理解。

一句話簡單來說,索引的出現(xiàn)其實(shí)就是為了提高數(shù)據(jù)查詢的效率,就像書的目錄一樣。一本500頁的書,如果你想快速找到其中的某一個(gè)知識(shí)點(diǎn),在不借助目錄的情況下,那我估計(jì)你可得找一會(huì)兒。同樣,對(duì)于數(shù)據(jù)庫的表而言,索引其實(shí)就是它的“目錄”。

索引的常見模型

索引的出現(xiàn)是為了提高查詢效率,但是實(shí)現(xiàn)索引的方式卻有很多種,所以這里也就引入了索引模型的概念??梢杂糜谔岣咦x寫效率的數(shù)據(jù)結(jié)構(gòu)很多,這里我先給你介紹三種常見、也比較簡單的數(shù)據(jù)結(jié)構(gòu),它們分別是哈希表、有序數(shù)組和搜索樹。

下面我主要從使用的角度,為你簡單分析一下這三種模型的區(qū)別。

哈希表是一種以鍵-值(key-value)存儲(chǔ)數(shù)據(jù)的結(jié)構(gòu),我們只要輸入待查找的值即key,就可以找到其對(duì)應(yīng)的值即Value。哈希的思路很簡單,把值放在數(shù)組里,用一個(gè)哈希函數(shù)把key換算成一個(gè)確定的位置,然后把value放在數(shù)組的這個(gè)位置。

不可避免地,多個(gè)key值經(jīng)過哈希函數(shù)的換算,會(huì)出現(xiàn)同一個(gè)值的情況。處理這種情況的一種方法是,拉出一個(gè)鏈表。

假設(shè),你現(xiàn)在維護(hù)著一個(gè)身份證信息和姓名的表,需要根據(jù)身份證號(hào)查找對(duì)應(yīng)的名字,這時(shí)對(duì)應(yīng)的哈希索引的示意圖如下所示:

圖1 哈希表示意圖

圖中,User2和User4根據(jù)身份證號(hào)算出來的值都是N,但沒關(guān)系,后面還跟了一個(gè)鏈表。假設(shè),這時(shí)候你要查ID_card_n2對(duì)應(yīng)的名字是什么,處理步驟就是:首先,將ID_card_n2通過哈希函數(shù)算出N;然后,按順序遍歷,找到User2。

需要注意的是,圖中四個(gè)ID_card_n的值并不是遞增的,這樣做的好處是增加新的User時(shí)速度會(huì)很快,只需要往后追加。但缺點(diǎn)是,因?yàn)椴皇怯行虻?,所以哈希索引做區(qū)間查詢的速度是很慢的。

你可以設(shè)想下,如果你現(xiàn)在要找身份證號(hào)在[ID_card_X, ID_card_Y]這個(gè)區(qū)間的所有用戶,就必須全部掃描一遍了。

所以,哈希表這種結(jié)構(gòu)適用于只有等值查詢的場景,比如Memcached及其他一些NoSQL引擎。

有序數(shù)組在等值查詢和范圍查詢場景中的性能就都非常優(yōu)秀。還是上面這個(gè)根據(jù)身份證號(hào)查名字的例子,如果我們使用有序數(shù)組來實(shí)現(xiàn)的話,示意圖如下所示:

圖2 有序數(shù)組示意圖

這里我們假設(shè)身份證號(hào)沒有重復(fù),這個(gè)數(shù)組就是按照身份證號(hào)遞增的順序保存的。這時(shí)候如果你要查ID_card_n2對(duì)應(yīng)的名字,用二分法就可以快速得到,這個(gè)時(shí)間復(fù)雜度是O(log(N))。

同時(shí)很顯然,這個(gè)索引結(jié)構(gòu)支持范圍查詢。你要查身份證號(hào)在[ID_card_X, ID_card_Y]區(qū)間的User,可以先用二分法找到ID_card_X(如果不存在ID_card_X,就找到大于ID_card_X的第一個(gè)User),然后向右遍歷,直到查到第一個(gè)大于ID_card_Y的身份證號(hào),退出循環(huán)。

如果僅僅看查詢效率,有序數(shù)組就是最好的數(shù)據(jù)結(jié)構(gòu)了。但是,在需要更新數(shù)據(jù)的時(shí)候就麻煩了,你往中間插入一個(gè)記錄就必須得挪動(dòng)后面所有的記錄,成本太高。

所以,有序數(shù)組索引只適用于靜態(tài)存儲(chǔ)引擎,比如你要保存的是2017年某個(gè)城市的所有人口信息,這類不會(huì)再修改的數(shù)據(jù)。

二叉搜索樹也是課本里的經(jīng)典數(shù)據(jù)結(jié)構(gòu)了。還是上面根據(jù)身份證號(hào)查名字的例子,如果我們用二叉搜索樹來實(shí)現(xiàn)的話,示意圖如下所示:

圖3 二叉搜索樹示意圖

二叉搜索樹的特點(diǎn)是:每個(gè)節(jié)點(diǎn)的左兒子小于父節(jié)點(diǎn),父節(jié)點(diǎn)又小于右兒子。這樣如果你要查ID_card_n2的話,按照?qǐng)D中的搜索順序就是按照UserA -> UserC -> UserF -> User2這個(gè)路徑得到。這個(gè)時(shí)間復(fù)雜度是O(log(N))。

當(dāng)然為了維持O(log(N))的查詢復(fù)雜度,你就需要保持這棵樹是平衡二叉樹。為了做這個(gè)保證,更新的時(shí)間復(fù)雜度也是O(log(N))。

樹可以有二叉,也可以有多叉。多叉樹就是每個(gè)節(jié)點(diǎn)有多個(gè)兒子,兒子之間的大小保證從左到右遞增。二叉樹是搜索效率最高的,但是實(shí)際上大多數(shù)的數(shù)據(jù)庫存儲(chǔ)卻并不使用二叉樹。其原因是,索引不止存在內(nèi)存中,還要寫到磁盤上。

你可以想象一下一棵100萬節(jié)點(diǎn)的平衡二叉樹,樹高20。一次查詢可能需要訪問20個(gè)數(shù)據(jù)塊。在機(jī)械硬盤時(shí)代,從磁盤隨機(jī)讀一個(gè)數(shù)據(jù)塊需要10 ms左右的尋址時(shí)間。也就是說,對(duì)于一個(gè)100萬行的表,如果使用二叉樹來存儲(chǔ),單獨(dú)訪問一個(gè)行可能需要20個(gè)10 ms的時(shí)間,這個(gè)查詢可真夠慢的。

為了讓一個(gè)查詢盡量少地讀磁盤,就必須讓查詢過程訪問盡量少的數(shù)據(jù)塊。那么,我們就不應(yīng)該使用二叉樹,而是要使用“N叉”樹。這里,“N叉”樹中的“N”取決于數(shù)據(jù)塊的大小。

以InnoDB的一個(gè)整數(shù)字段索引為例,這個(gè)N差不多是1200。這棵樹高是4的時(shí)候,就可以存1200的3次方個(gè)值,這已經(jīng)17億了??紤]到樹根的數(shù)據(jù)塊總是在內(nèi)存中的,一個(gè)10億行的表上一個(gè)整數(shù)字段的索引,查找一個(gè)值最多只需要訪問3次磁盤。其實(shí),樹的第二層也有很大概率在內(nèi)存中,那么訪問磁盤的平均次數(shù)就更少了。

N叉樹由于在讀寫上的性能優(yōu)點(diǎn),以及適配磁盤的訪問模式,已經(jīng)被廣泛應(yīng)用在數(shù)據(jù)庫引擎中了。

不管是哈希還是有序數(shù)組,或者N叉樹,它們都是不斷迭代、不斷優(yōu)化的產(chǎn)物或者解決方案。數(shù)據(jù)庫技術(shù)發(fā)展到今天,跳表、LSM樹等數(shù)據(jù)結(jié)構(gòu)也被用于引擎設(shè)計(jì)中,這里我就不再一一展開了。

你心里要有個(gè)概念,數(shù)據(jù)庫底層存儲(chǔ)的核心就是基于這些數(shù)據(jù)模型的。每碰到一個(gè)新數(shù)據(jù)庫,我們需要先關(guān)注它的數(shù)據(jù)模型,這樣才能從理論上分析出這個(gè)數(shù)據(jù)庫的適用場景。

截止到這里,我用了半篇文章的篇幅和你介紹了不同的數(shù)據(jù)結(jié)構(gòu),以及它們的適用場景,你可能會(huì)覺得有些枯燥。但是,我建議你還是要多花一些時(shí)間來理解這部分內(nèi)容,畢竟這是數(shù)據(jù)庫處理數(shù)據(jù)的核心概念之一,在分析問題的時(shí)候會(huì)經(jīng)常用到。當(dāng)你理解了索引的模型后,就會(huì)發(fā)現(xiàn)在分析問題的時(shí)候會(huì)有一個(gè)更清晰的視角,體會(huì)到引擎設(shè)計(jì)的精妙之處。

現(xiàn)在,我們一起進(jìn)入相對(duì)偏實(shí)戰(zhàn)的內(nèi)容吧。

在MySQL中,索引是在存儲(chǔ)引擎層實(shí)現(xiàn)的,所以并沒有統(tǒng)一的索引標(biāo)準(zhǔn),即不同存儲(chǔ)引擎的索引的工作方式并不一樣。而即使多個(gè)存儲(chǔ)引擎支持同一種類型的索引,其底層的實(shí)現(xiàn)也可能不同。由于InnoDB存儲(chǔ)引擎在MySQL數(shù)據(jù)庫中使用最為廣泛,所以下面我就以InnoDB為例,和你分析一下其中的索引模型。

InnoDB 的索引模型

在InnoDB中,表都是根據(jù)主鍵順序以索引的形式存放的,這種存儲(chǔ)方式的表稱為索引組織表。又因?yàn)榍懊嫖覀兲岬降?,InnoDB使用了B+樹索引模型,所以數(shù)據(jù)都是存儲(chǔ)在B+樹中的。

每一個(gè)索引在InnoDB里面對(duì)應(yīng)一棵B+樹。

假設(shè),我們有一個(gè)主鍵列為ID的表,表中有字段k,并且在k上有索引。

這個(gè)表的建表語句是:

mysql> create table T(
id int primary key, 
k int not null, 
name varchar(16),
index (k))engine=InnoDB;

表中R1~R5的(ID,k)值分別為(100,1)、(200,2)、(300,3)、(500,5)和(600,6),兩棵樹的示例示意圖如下。

圖4 InnoDB的索引組織結(jié)構(gòu)

從圖中不難看出,根據(jù)葉子節(jié)點(diǎn)的內(nèi)容,索引類型分為主鍵索引和非主鍵索引。

主鍵索引的葉子節(jié)點(diǎn)存的是整行數(shù)據(jù)。在InnoDB里,主鍵索引也被稱為聚簇索引(clustered index)。

非主鍵索引的葉子節(jié)點(diǎn)內(nèi)容是主鍵的值。在InnoDB里,非主鍵索引也被稱為二級(jí)索引(secondary index)。

根據(jù)上面的索引結(jié)構(gòu)說明,我們來討論一個(gè)問題:基于主鍵索引和普通索引的查詢有什么區(qū)別?

  • 如果語句是select * from T where ID=500,即主鍵查詢方式,則只需要搜索ID這棵B+樹;

  • 如果語句是select * from T where k=5,即普通索引查詢方式,則需要先搜索k索引樹,得到ID的值為500,再到ID索引樹搜索一次。這個(gè)過程稱為回表。

也就是說,基于非主鍵索引的查詢需要多掃描一棵索引樹。因此,我們?cè)趹?yīng)用中應(yīng)該盡量使用主鍵查詢。

索引維護(hù)

B+樹為了維護(hù)索引有序性,在插入新值的時(shí)候需要做必要的維護(hù)。以上面這個(gè)圖為例,如果插入新的行ID值為700,則只需要在R5的記錄后面插入一個(gè)新記錄。如果新插入的ID值為400,就相對(duì)麻煩了,需要邏輯上挪動(dòng)后面的數(shù)據(jù),空出位置。

而更糟的情況是,如果R5所在的數(shù)據(jù)頁已經(jīng)滿了,根據(jù)B+樹的算法,這時(shí)候需要申請(qǐng)一個(gè)新的數(shù)據(jù)頁,然后挪動(dòng)部分?jǐn)?shù)據(jù)過去。這個(gè)過程稱為頁分裂。在這種情況下,性能自然會(huì)受影響。

除了性能外,頁分裂操作還影響數(shù)據(jù)頁的利用率。原本放在一個(gè)頁的數(shù)據(jù),現(xiàn)在分到兩個(gè)頁中,整體空間利用率降低大約50%。

當(dāng)然有分裂就有合并。當(dāng)相鄰兩個(gè)頁由于刪除了數(shù)據(jù),利用率很低之后,會(huì)將數(shù)據(jù)頁做合并。合并的過程,可以認(rèn)為是分裂過程的逆過程。

基于上面的索引維護(hù)過程說明,我們來討論一個(gè)案例:

你可能在一些建表規(guī)范里面見到過類似的描述,要求建表語句里一定要有自增主鍵。當(dāng)然事無絕對(duì),我們來分析一下哪些場景下應(yīng)該使用自增主鍵,而哪些場景下不應(yīng)該。

自增主鍵是指自增列上定義的主鍵,在建表語句中一般是這么定義的: NOT NULL PRIMARY KEY AUTO_INCREMENT。

插入新記錄的時(shí)候可以不指定ID的值,系統(tǒng)會(huì)獲取當(dāng)前ID最大值加1作為下一條記錄的ID值。

也就是說,自增主鍵的插入數(shù)據(jù)模式,正符合了我們前面提到的遞增插入的場景。每次插入一條新記錄,都是追加操作,都不涉及到挪動(dòng)其他記錄,也不會(huì)觸發(fā)葉子節(jié)點(diǎn)的分裂。

而有業(yè)務(wù)邏輯的字段做主鍵,則往往不容易保證有序插入,這樣寫數(shù)據(jù)成本相對(duì)較高。

除了考慮性能外,我們還可以從存儲(chǔ)空間的角度來看。假設(shè)你的表中確實(shí)有一個(gè)唯一字段,比如字符串類型的身份證號(hào),那應(yīng)該用身份證號(hào)做主鍵,還是用自增字段做主鍵呢?

由于每個(gè)非主鍵索引的葉子節(jié)點(diǎn)上都是主鍵的值。如果用身份證號(hào)做主鍵,那么每個(gè)二級(jí)索引的葉子節(jié)點(diǎn)占用約20個(gè)字節(jié),而如果用整型做主鍵,則只要4個(gè)字節(jié),如果是長整型(bigint)則是8個(gè)字節(jié)。

顯然,主鍵長度越小,普通索引的葉子節(jié)點(diǎn)就越小,普通索引占用的空間也就越小。

所以,從性能和存儲(chǔ)空間方面考量,自增主鍵往往是更合理的選擇。

有沒有什么場景適合用業(yè)務(wù)字段直接做主鍵的呢?還是有的。比如,有些業(yè)務(wù)的場景需求是這樣的:

  1. 只有一個(gè)索引;

  2. 該索引必須是唯一索引。

你一定看出來了,這就是典型的KV場景。

由于沒有其他索引,所以也就不用考慮其他索引的葉子節(jié)點(diǎn)大小的問題。

這時(shí)候我們就要優(yōu)先考慮上一段提到的“盡量使用主鍵查詢”原則,直接將這個(gè)索引設(shè)置為主鍵,可以避免每次查詢需要搜索兩棵樹。

小結(jié)

今天,我跟你分析了數(shù)據(jù)庫引擎可用的數(shù)據(jù)結(jié)構(gòu),介紹了InnoDB采用的B+樹結(jié)構(gòu),以及為什么InnoDB要這么選擇。B+樹能夠很好地配合磁盤的讀寫特性,減少單次查詢的磁盤訪問次數(shù)。

由于InnoDB是索引組織表,一般情況下我會(huì)建議你創(chuàng)建一個(gè)自增主鍵,這樣非主鍵索引占用的空間最小。但事無絕對(duì),我也跟你討論了使用業(yè)務(wù)邏輯字段做主鍵的應(yīng)用場景。

最后,我給你留下一個(gè)問題吧。對(duì)于上面例子中的InnoDB表T,如果你要重建索引 k,你的兩個(gè)SQL語句可以這么寫:

alter table T drop index k;
alter table T add index(k);

如果你要重建主鍵索引,也可以這么寫:

alter table T drop primary key;
alter table T add primary key(id);

我的問題是,對(duì)于上面這兩個(gè)重建索引的作法,說出你的理解。如果有不合適的,為什么,更好的方法是什么?

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多