一、InnoDB實現原理
雖然InnoDB也使用B+Tree作為索引結構,但具體實現方式卻與MyISAM截然不同。因為InnoDB支持聚簇索引(主鍵索引),聚簇索引就是表,所以InnoDB不用像MyISAM那樣需要獨立的行存儲。也就是說,InnoDB的數據文件本身就是索引文件。
聚簇索引的每一個葉子節點都包含了主鍵值、事務ID、用於事務和MVCC的回滾指針以及所有的剩余列。假設我們以col1為主鍵,則下圖是一個InnoDB表的聚簇索引(主鍵索引)(Primary key)示意。
與MyISAM不同的是,InnoDB的二級索引和聚簇索引很不相同。InnoDB的二級索引的葉子節點存儲的不是行號(行指針),而是主鍵列。這種策略的缺點是二級索引需要兩次索引查找,第一次在二級索引中查找主鍵,第二次在聚簇索引中通過主鍵查找需要的數據行。
畫外音:可以通過我們前面提到過的索引覆蓋來避免回表查詢,這樣就只需要一次回表查詢,對於InnoDB而言,就是只需要一次索引查找就可以查詢到需要的數據記錄,因為需要的數據記錄已經被索引到二級索引中,直接就可以找到。
因為InnoDB的索引的方式通過主鍵聚集數據,嚴重依賴主鍵。索引如果沒有定義主鍵,那么InnoDB會選擇一個唯一的非空索引代替。如果沒有這樣的索引,InnoDB會隱式定義一個主鍵來作為聚簇索引。
二、優缺點
- 優點
- 可以把相關數據存儲在一起,減少數據查詢時的磁盤I/O
- 數據訪問更快,因為聚簇索引就是表,索引和數據保存在一個B+Tree中
- 使用索引覆蓋的查詢時可以直接使用頁節點中的主鍵值
- 缺點
- 插入速度嚴重依賴插入順序
- 更新聚簇索引列的代價很高,因為會強制InnoDB把更新的列移動到新的位置
- 基於聚簇索引的表在插入新行,或者主鍵被更新導致需要移動行的時候,可能會導致“頁分裂”。當行的主鍵值要求必須將這一行插入到已滿的頁中時,存儲引擎會將該頁分裂為兩個頁面來容納該行,這就是一次頁分裂操作,頁分裂會導致表占用更多的存儲空間。
基於聚簇索引以上的這些特點,在InnoDB中,我們應該盡量使用和應用無關的主鍵,例如自增主鍵,這樣可以保證數據行是按照順序寫入的。而不是使用GUID、UUID生成隨機的主鍵。畫外音:關於頁,我們在上一篇文章中也提到過。頁是計算機管理存儲器的邏輯塊,硬件及操作系統往往將主存和磁盤存儲區分割為連續的 大小相等的塊,每個存儲塊稱為一頁。存和磁盤以頁為單位交換數據。數據庫系統的設計者巧妙利用了磁盤預讀原理,將一個節點的大小設 為等於一個頁,這樣每個節點只需要一次磁盤I/O就可以完全載入
三、 注意&建議
- 主鍵推薦使用整型,避免索引分裂;
- 查詢使用索引覆蓋能夠提升很大的性能,因為避免了回表查詢
- 選擇合適的順序建立索引,有的場景並非區分度越高的字段放在前邊越好,聯合索引使用居多
- 合理使用in操作將范圍查詢轉換成多個等值查詢,但是如果有order by 不同的列來說是不會走索引的
- 大批量數據查詢任務分解為分批查詢
- 將復雜查詢轉換為簡單查詢
- 合理使用inner join,比如分頁的時候
四、一些問題的分析
-
索引分裂個人理解:在 MySQL插入記錄的同時會更新配置的相應索引文件,根據以上的了解,在插入索引時,可能會存在索引的頁的分裂,因此會導致磁盤數據的移動。當插入的主鍵是隨機字符串時,每次插入不會是在B+樹的最后插入,每次插入位置都是隨機的,每次都可能導致數據頁的移動,而且字符串的存儲空間占用也很大,這樣重建索引不僅僅效率低而且 MySQL的負載也會很高,同時還會導致大量的磁盤碎片,磁盤碎片多了也會對查詢造成一定的性能開銷,因為存儲位置不連續導致更多的磁盤I/O,這就是為什么推薦定義主鍵為遞增整型的一個原因
-
自增主鍵的弊端 對於高並發的場景,在InnoDB中按照主鍵的順序插入可能會造成明顯的爭用,主鍵的上界會成為“熱點”,因為所有的插入都發生在此處,索引並發的插入可能會造成間隙鎖競爭,何為間隙鎖競爭,下個會詳細介紹;另外一個原因可能是Auto_increment的鎖機制,在 MySQL處理自增主鍵時,當innodb_autoinc_lock_mode為0或1時,在不知道插入有多少行時,比如insert t1 xx select xx from t2,對於這個statement的執行會進行鎖表,只有這個statement執行完以后才會釋放鎖,然后別的插入才能夠繼續執行,但是在innodb_autoinc_lock_mode=2時,這種情況不會存在表鎖,但是只能保證所有並發執行的statement插入的記錄是唯一並且自增的,但是每個statement做的多行插入之間是不連接的
-
優化器不使用索引選擇全表掃描 比如一張order表中有聯合索引(order_id, goods_id),在此例子上來說明這個問題是從兩個方面來說:
- 查詢字段在索引中
select order_id from order where order_id > 1000; --如果查看其執行計划的話,發現是用use index condition,走的是索引覆蓋。
- 查詢字段不在索引中
select * from order where order_id > 1000;
此條語句查詢的是該表所有字段,有一部分字段並未在此聯合索引中,因此走聯合索引查詢會走兩步,首先通過聯合索引確定符合條件的主鍵id,然后利用這些主鍵id再去聚簇索引中去查詢,然后得到所有記錄,利用主鍵id在聚簇索引中查詢記錄的過程是無序的,在磁盤上就變成了離散讀取的操作,假如當讀取的記錄很多時(一般是整個表的20%左右),這個時候優化器會選擇直接使用聚簇索引,也就是掃全表,因為順序讀取要快於離散讀取,這也就是為何一般不用區分度不大的字段單獨做索引,注意是單獨因為利用此字段查出來的數據會很多,有很大概率走全表掃描。
-
范圍查詢之后的條件不走索引 根據 MySQL的查詢原理的話,當處理到where的范圍查詢條件后,會將查詢到的行全部返回到服務器端(查詢執行引擎),接下來的條件操作在服務器端進行處理,這也就是為什么范圍條件不走索引的原因了,因為之后的條件過濾已經不在存儲引擎完成了。但是在 MySQL 5.6以后假如了一個新的功能index condition pushdown(ICP),這個功能允許范圍查詢條件之后的條件繼續走索引,但是需要有幾個前提條件:
- 查詢條件的第一個條件需要時有邊界的,比如select * from xx where c1=x and c2>x and c3<x,這樣c3是可以走到索引的;
- 支持InnoDB和MyISAM存儲引擎;
- where條件的字段需要在索引中;
- 分表ICP功能5.7開始支持;
- 使用索引覆蓋時,ICP不起作用。
-
分頁offset值很大性能問題
在 MySQL中,分頁當offset值很大的時候,性能會非常的差,比如limit 100000, 20,需要查詢100020條數據,然后取20條,拋棄前100000條,在這個過程中產生了大量的隨機I/O,這是性能很差的原因,為了解決這個問題,切入點便是減少無用數據的查詢,減少隨機I/O- 利用inner join
select * from t1 inner join (select id from t1 where xxx order by xx limit 1000000,5) as t2 using(id); --子查詢先走索引覆蓋查得id,然后根據得到的id直接取5條得數據。
- 利用范圍查詢條件來限制取出的數據
select * from t1 where id > 1000000 order by id limit 0, 5; --即利用條件id > 1000000在掃描索引是跳過1000000條記錄,然后取5條即可,這種處理方式的offset值便成為0了,但此種方式通常分頁不能用,但是可以用來分批取數據。