提到數據庫索引,我想你並不陌生,在平常工做中會常常接觸到。好比某一個SQL查詢比較慢,分析完緣由以後,你可能就會說「給某個字段加個索引吧」之類的解決方案。但到底什麼是索引,索引又是如何工做的呢?今天就讓咱們一塊兒來聊聊這個話題吧。mysql
數據庫索引的內容比較多,我分紅了上下兩篇文章。索引是數據庫系統裏面最重要的概念之一,因此我但願你可以耐心看完。在後面的實戰文章中,我也會常常引用這兩篇文章中提到的知識點,加深你對數據庫索引的理解。算法
一句話簡單來講,索引的出現其實就是爲了提升數據查詢的效率,就像書的目錄同樣。一本500頁的書,若是你想快速找到其中的某一個知識點,在不借助目錄的狀況下,那我估計你可得找一下子。一樣,對於數據庫的表而言,索引其實就是它的「目錄」。sql
索引的出現是爲了提升查詢效率,可是實現索引的方式卻有不少種,因此這裏也就引入了索引模型的概念。能夠用於提升讀寫效率的數據結構不少,這裏我先給你介紹三種常見、也比較簡單的數據結構,它們分別是哈希表、有序數組和搜索樹。數據庫
下面我主要從使用的角度,爲你簡單分析一下這三種模型的區別。數組
哈希表是一種以鍵-值(key-value)存儲數據的結構,咱們只要輸入待查找的值即key,就能夠找到其對應的值即Value。哈希的思路很簡單,把值放在數組裏,用一個哈希函數把key換算成一個肯定的位置,而後把value放在數組的這個位置。數據結構
不可避免地,多個key值通過哈希函數的換算,會出現同一個值的狀況。處理這種狀況的一種方法是,拉出一個鏈表。框架
假設,你如今維護着一個身份證信息和姓名的表,須要根據身份證號查找對應的名字,這時對應的哈希索引的示意圖以下所示:函數
圖中,User2和User4根據身份證號算出來的值都是N,但不要緊,後面還跟了一個鏈表。假設,這時候你要查ID_card_n2對應的名字是什麼,處理步驟就是:首先,將ID_card_n2經過哈希函數算出N;而後,按順序遍歷,找到User2。工具
須要注意的是,圖中四個ID_card_n的值並非遞增的,這樣作的好處是增長新的User時速度會很快,只須要日後追加。但缺點是,由於不是有序的,因此哈希索引作區間查詢的速度是很慢的。性能
你能夠設想下,若是你如今要找身份證號在[ID_card_X, ID_card_Y]這個區間的全部用戶,就必須所有掃描一遍了。
因此,哈希表這種結構適用於只有等值查詢的場景,好比Memcached及其餘一些NoSQL引擎。
而有序數組在等值查詢和範圍查詢場景中的性能就都很是優秀。仍是上面這個根據身份證號查名字的例子,若是咱們使用有序數組來實現的話,示意圖以下所示:
這裏咱們假設身份證號沒有重複,這個數組就是按照身份證號遞增的順序保存的。這時候若是你要查ID_card_n2對應的名字,用二分法就能夠快速獲得,這個時間複雜度是O(log(N))。
同時很顯然,這個索引結構支持範圍查詢。你要查身份證號在[ID_card_X, ID_card_Y]區間的User,能夠先用二分法找到ID_card_X(若是不存在ID_card_X,就找到大於ID_card_X的第一個User),而後向右遍歷,直到查到第一個大於ID_card_Y的身份證號,退出循環。
若是僅僅看查詢效率,有序數組就是最好的數據結構了。可是,在須要更新數據的時候就麻煩了,你往中間插入一個記錄就必須得挪動後面全部的記錄,成本過高。
因此,有序數組索引只適用於靜態存儲引擎,好比你要保存的是2017年某個城市的全部人口信息,這類不會再修改的數據。
二叉搜索樹也是課本里的經典數據結構了。仍是上面根據身份證號查名字的例子,若是咱們用二叉搜索樹來實現的話,示意圖以下所示:
二叉搜索樹的特色是:每一個節點的左兒子小於父節點,父節點又小於右兒子。這樣若是你要查ID_card_n2的話,按照圖中的搜索順序就是按照UserA -> UserC -> UserF -> User2這個路徑獲得。這個時間複雜度是O(log(N))。
固然爲了維持O(log(N))的查詢複雜度,你就須要保持這棵樹是平衡二叉樹。爲了作這個保證,更新的時間複雜度也是O(log(N))。
樹能夠有二叉,也能夠有多叉。多叉樹就是每一個節點有多個兒子,兒子之間的大小保證從左到右遞增。二叉樹是搜索效率最高的,可是實際上大多數的數據庫存儲卻並不使用二叉樹。其緣由是,索引不止存在內存中,還要寫到磁盤上。
你能夠想象一下一棵100萬節點的平衡二叉樹,樹高20。一次查詢可能須要訪問20個數據塊。在機械硬盤時代,從磁盤隨機讀一個數據塊須要10 ms左右的尋址時間。也就是說,對於一個100萬行的表,若是使用二叉樹來存儲,單獨訪問一個行可能須要20個10 ms的時間,這個查詢可真夠慢的。
爲了讓一個查詢儘可能少地讀磁盤,就必須讓查詢過程訪問儘可能少的數據塊。那麼,咱們就不該該使用二叉樹,而是要使用「N叉」樹。這裏,「N叉」樹中的「N」取決於數據塊的大小。
以InnoDB的一個整數字段索引爲例,這個N差很少是1200。這棵樹高是4的時候,就能夠存1200的3次方個值,這已經17億了。考慮到樹根的數據塊老是在內存中的,一個10億行的表上一個整數字段的索引,查找一個值最多隻須要訪問3次磁盤。其實,樹的第二層也有很大機率在內存中,那麼訪問磁盤的平均次數就更少了。
N叉樹因爲在讀寫上的性能優勢,以及適配磁盤的訪問模式,已經被普遍應用在數據庫引擎中了。
無論是哈希仍是有序數組,或者N叉樹,它們都是不斷迭代、不斷優化的產物或者解決方案。數據庫技術發展到今天,跳錶、LSM樹等數據結構也被用於引擎設計中,這裏我就再也不一一展開了。
你內心要有個概念,數據庫底層存儲的核心就是基於這些數據模型的。每碰到一個新數據庫,咱們須要先關注它的數據模型,這樣才能從理論上分析出這個數據庫的適用場景。
截止到這裏,我用了半篇文章的篇幅和你介紹了不一樣的數據結構,以及它們的適用場景,你可能會以爲有些枯燥。可是,我建議你仍是要多花一些時間來理解這部份內容,畢竟這是數據庫處理數據的核心概念之一,在分析問題的時候會常常用到。當你理解了索引的模型後,就會發如今分析問題的時候會有一個更清晰的視角,體會到引擎設計的精妙之處。
如今,咱們一塊兒進入相對偏實戰的內容吧。
在MySQL中,索引是在存儲引擎層實現的,因此並無統一的索引標準,即不一樣存儲引擎的索引的工做方式並不同。而即便多個存儲引擎支持同一種類型的索引,其底層的實現也可能不一樣。因爲InnoDB存儲引擎在MySQL數據庫中使用最爲普遍,因此下面我就以InnoDB爲例,和你分析一下其中的索引模型。
在InnoDB中,表都是根據主鍵順序以索引的形式存放的,這種存儲方式的表稱爲索引組織表。又由於前面咱們提到的,InnoDB使用了B+樹索引模型,因此數據都是存儲在B+樹中的。
每個索引在InnoDB裏面對應一棵B+樹。
假設,咱們有一個主鍵列爲ID的表,表中有字段k,而且在k上有索引。
這個表的建表語句是:
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),兩棵樹的示例示意圖以下。
從圖中不難看出,根據葉子節點的內容,索引類型分爲主鍵索引和非主鍵索引。
主鍵索引的葉子節點存的是整行數據。在InnoDB裏,主鍵索引也被稱爲聚簇索引(clustered index)。
非主鍵索引的葉子節點內容是主鍵的值。在InnoDB裏,非主鍵索引也被稱爲二級索引(secondary index)。
根據上面的索引結構說明,咱們來討論一個問題:基於主鍵索引和普通索引的查詢有什麼區別?
也就是說,基於非主鍵索引的查詢須要多掃描一棵索引樹。所以,咱們在應用中應該儘可能使用主鍵查詢。
B+樹爲了維護索引有序性,在插入新值的時候須要作必要的維護。以上面這個圖爲例,若是插入新的行ID值爲700,則只須要在R5的記錄後面插入一個新記錄。若是新插入的ID值爲400,就相對麻煩了,須要邏輯上挪動後面的數據,空出位置。
而更糟的狀況是,若是R5所在的數據頁已經滿了,根據B+樹的算法,這時候須要申請一個新的數據頁,而後挪動部分數據過去。這個過程稱爲頁分裂。在這種狀況下,性能天然會受影響。
除了性能外,頁分裂操做還影響數據頁的利用率。本來放在一個頁的數據,如今分到兩個頁中,總體空間利用率下降大約50%。
固然有分裂就有合併。當相鄰兩個頁因爲刪除了數據,利用率很低以後,會將數據頁作合併。合併的過程,能夠認爲是分裂過程的逆過程。
基於上面的索引維護過程說明,咱們來討論一個案例:
你可能在一些建表規範裏面見到過相似的描述,要求建表語句裏必定要有自增主鍵。固然事無絕對,咱們來分析一下哪些場景下應該使用自增主鍵,而哪些場景下不該該。
自增主鍵是指自增列上定義的主鍵,在建表語句中通常是這麼定義的: NOT NULL PRIMARY KEY AUTO_INCREMENT。
插入新記錄的時候能夠不指定ID的值,系統會獲取當前ID最大值加1做爲下一條記錄的ID值。
也就是說,自增主鍵的插入數據模式,正符合了咱們前面提到的遞增插入的場景。每次插入一條新記錄,都是追加操做,都不涉及到挪動其餘記錄,也不會觸發葉子節點的分裂。
而有業務邏輯的字段作主鍵,則每每不容易保證有序插入,這樣寫數據成本相對較高。
除了考慮性能外,咱們還能夠從存儲空間的角度來看。假設你的表中確實有一個惟一字段,好比字符串類型的身份證號,那應該用身份證號作主鍵,仍是用自增字段作主鍵呢?
因爲每一個非主鍵索引的葉子節點上都是主鍵的值。若是用身份證號作主鍵,那麼每一個二級索引的葉子節點佔用約20個字節,而若是用整型作主鍵,則只要4個字節,若是是長整型(bigint)則是8個字節。
顯然,主鍵長度越小,普通索引的葉子節點就越小,普通索引佔用的空間也就越小。
因此,從性能和存儲空間方面考量,自增主鍵每每是更合理的選擇。
有沒有什麼場景適合用業務字段直接作主鍵的呢?仍是有的。好比,有些業務的場景需求是這樣的:
只有一個索引;
該索引必須是惟一索引。
你必定看出來了,這就是典型的KV場景。
因爲沒有其餘索引,因此也就不用考慮其餘索引的葉子節點大小的問題。
這時候咱們就要優先考慮上一段提到的「儘可能使用主鍵查詢」原則,直接將這個索引設置爲主鍵,能夠避免每次查詢須要搜索兩棵樹。
今天,我跟你分析了數據庫引擎可用的數據結構,介紹了InnoDB採用的B+樹結構,以及爲何InnoDB要這麼選擇。B+樹可以很好地配合磁盤的讀寫特性,減小單次查詢的磁盤訪問次數。
因爲InnoDB是索引組織表,通常狀況下我會建議你建立一個自增主鍵,這樣非主鍵索引佔用的空間最小。但事無絕對,我也跟你討論了使用業務邏輯字段作主鍵的應用場景。
最後,我給你留下一個問題吧。對於上面例子中的InnoDB表T,若是你要重建索引 k,你的兩個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);
個人問題是,對於上面這兩個重建索引的做法,說出你的理解。若是有不合適的,爲何,更好的方法是什麼?
你能夠把你的思考和觀點寫在留言區裏,我會在下一篇文章的末尾給出個人參考答案。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。
我在上一篇文章末尾給你留下的問題是:如何避免長事務對業務的影響?
這個問題,咱們能夠從應用開發端和數據庫端來看。
首先,從應用開發端來看:
確認是否使用了set autocommit=0。這個確認工做能夠在測試環境中開展,把MySQL的general_log開起來,而後隨便跑一個業務邏輯,經過general_log的日誌來確認。通常框架若是會設置這個值,也就會提供參數來控制行爲,你的目標就是把它改爲1。
確認是否有沒必要要的只讀事務。有些框架會習慣無論什麼語句先用begin/commit框起來。我見過有些是業務並無這個須要,可是也把好幾個select語句放到了事務中。這種只讀事務能夠去掉。
業務鏈接數據庫的時候,根據業務自己的預估,經過SET MAX_EXECUTION_TIME命令,來控制每一個語句執行的最長時間,避免單個語句意外執行太長時間。(爲何會意外?在後續的文章中會提到這類案例)
其次,從數據庫端來看:
監控 information_schema.Innodb_trx表,設置長事務閾值,超過就報警/或者kill;
Percona的pt-kill這個工具不錯,推薦使用;
在業務功能測試階段要求輸出全部的general_log,分析日誌行爲提早發現問題;
若是使用的是MySQL 5.6或者更新版本,把innodb_undo_tablespaces設置成2(或更大的值)。若是真的出現大事務致使回滾段過大,這樣設置後清理起來更方便。