1. 程式人生 > >InnoDB索引實現原理以及注意點和建議

InnoDB索引實現原理以及注意點和建議

一、InnoDB實現原理

雖然InnoDB也使用B+Tree作為索引結構,但具體實現方式卻與MyISAM截然不同。因為InnoDB支援聚簇索引(主鍵索引),聚簇索引就是表,所以InnoDB不用像MyISAM那樣需要獨立的行儲存。也就是說,InnoDB的資料檔案本身就是索引檔案。

聚簇索引的每一個葉子節點都包含了主鍵值、事務ID、用於事務和MVCC的回滾指標以及所有的剩餘列。假設我們以col1為主鍵,則下圖是一個InnoDB表的聚簇索引(主鍵索引)(Primary key)示意。

與MyISAM不同的是,InnoDB的二級索引和聚簇索引很不相同。InnoDB的二級索引的葉子節點儲存的不是行號(行指標),而是主鍵列。這種策略的缺點是二級索引需要兩次索引查詢,第一次在二級索引中查詢主鍵,第二次在聚簇索引中通過主鍵查詢需要的資料行。

畫外音:可以通過我們前面提到過的索引覆蓋來避免回表查詢,這樣就只需要一次回表查詢,對於InnoDB而言,就是隻需要一次索引查詢就可以查詢到需要的資料記錄,因為需要的資料記錄已經被索引到二級索引中,直接就可以找到。

因為InnoDB的索引的方式通過主鍵聚集資料,嚴重依賴主鍵。索引如果沒有定義主鍵,那麼InnoDB會選擇一個唯一的非空索引代替。如果沒有這樣的索引,InnoDB會隱式定義一個主鍵來作為聚簇索引。

二、優缺點

  1. 優點
    1. 可以把相關資料儲存在一起,減少資料查詢時的磁碟I/O
    2. 資料訪問更快,因為聚簇索引就是表,索引和資料儲存在一個B+Tree中
    3. 使用索引覆蓋的查詢時可以直接使用頁節點中的主鍵值
  2. 缺點
    1. 插入速度嚴重依賴插入順序
    2. 更新聚簇索引列的代價很高,因為會強制InnoDB把更新的列移動到新的位置
    3. 基於聚簇索引的表在插入新行,或者主鍵被更新導致需要移動行的時候,可能會導致“頁分裂”。當行的主鍵值要求必須將這一行插入到已滿的頁中時,儲存引擎會將該頁分裂為兩個頁面來容納該行,這就是一次頁分裂操作,頁分裂會導致表佔用更多的儲存空間。
    畫外音:關於頁,我們在上一篇文章中也提到過。頁是計算機管理儲存器的邏輯塊,硬體及作業系統往往將主存和磁碟儲存區分割為連續的
    大小相等的塊,每個儲存塊稱為一頁。存和磁碟以頁為單位交換資料。資料庫系統的設計者巧妙利用了磁碟預讀原理,將一個節點的大小設
    為等於一個頁,這樣每個節點只需要一次磁碟I/O就可以完全載入
    
    基於聚簇索引以上的這些特點,在InnoDB中,我們應該儘量使用和應用無關的主鍵,例如自增主鍵,這樣可以保證資料行是按照順序寫入的。而不是使用GUID、UUID生成隨機的主鍵。

三、 注意&建議

  1. 主鍵推薦使用整型,避免索引分裂;
  2. 查詢使用索引覆蓋能夠提升很大的效能,因為避免了回表查詢
  3. 選擇合適的順序建立索引,有的場景並非區分度越高的欄位放在前邊越好,聯合索引使用居多
  4. 合理使用in操作將範圍查詢轉換成多個等值查詢,但是如果有order by 不同的列來說是不會走索引的
  5. 大批量資料查詢任務分解為分批查詢
  6. 將複雜查詢轉換為簡單查詢
  7. 合理使用inner join,比如分頁的時候

四、一些問題的分析

  1. 索引分裂個人理解:在 MySQL插入記錄的同時會更新配置的相應索引檔案,根據以上的瞭解,在插入索引時,可能會存在索引的頁的分裂,因此會導致磁碟資料的移動。當插入的主鍵是隨機字串時,每次插入不會是在B+樹的最後插入,每次插入位置都是隨機的,每次都可能導致資料頁的移動,而且字串的儲存空間佔用也很大,這樣重建索引不僅僅效率低而且 MySQL的負載也會很高,同時還會導致大量的磁碟碎片,磁碟碎片多了也會對查詢造成一定的效能開銷,因為儲存位置不連續導致更多的磁碟I/O,這就是為什麼推薦定義主鍵為遞增整型的一個原因

  2. 自增主鍵的弊端 對於高併發的場景,在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做的多行插入之間是不連線的

  3. 優化器不使用索引選擇全表掃描 比如一張order表中有聯合索引(order_id, goods_id),在此例子上來說明這個問題是從兩個方面來說:

    1. 查詢欄位在索引中
    select order_id from order where order_id > 1000;
    --如果檢視其執行計劃的話,發現是用use index condition,走的是索引覆蓋。
    
    1. 查詢欄位不在索引中
    select * from order where order_id > 1000;
    

    此條語句查詢的是該表所有欄位,有一部分欄位並未在此聯合索引中,因此走聯合索引查詢會走兩步,首先通過聯合索引確定符合條件的主鍵id,然後利用這些主鍵id再去聚簇索引中去查詢,然後得到所有記錄,利用主鍵id在聚簇索引中查詢記錄的過程是無序的,在磁碟上就變成了離散讀取的操作,假如當讀取的記錄很多時(一般是整個表的20%左右),這個時候優化器會選擇直接使用聚簇索引,也就是掃全表,因為順序讀取要快於離散讀取,這也就是為何一般不用區分度不大的欄位單獨做索引,注意是單獨因為利用此欄位查出來的資料會很多,有很大概率走全表掃描。

  4. 範圍查詢之後的條件不走索引 根據 MySQL的查詢原理的話,當處理到where的範圍查詢條件後,會將查詢到的行全部返回到伺服器端(查詢執行引擎),接下來的條件操作在伺服器端進行處理,這也就是為什麼範圍條件不走索引的原因了,因為之後的條件過濾已經不在儲存引擎完成了。但是在 MySQL 5.6以後假如了一個新的功能index condition pushdown(ICP),這個功能允許範圍查詢條件之後的條件繼續走索引,但是需要有幾個前提條件:

    1. 查詢條件的第一個條件需要時有邊界的,比如select * from xx where c1=x and c2>x and c3<x,這樣c3是可以走到索引的;
    2. 支援InnoDB和MyISAM儲存引擎;
    3. where條件的欄位需要在索引中;
    4. 分表ICP功能5.7開始支援;
    5. 使用索引覆蓋時,ICP不起作用。
  5. 分頁offset值很大效能問題
    在 MySQL中,分頁當offset值很大的時候,效能會非常的差,比如limit 100000, 20,需要查詢100020條資料,然後取20條,拋棄前100000條,在這個過程中產生了大量的隨機I/O,這是效能很差的原因,為了解決這個問題,切入點便是減少無用資料的查詢,減少隨機I/O

    1. 利用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條得資料。
    
    1. 利用範圍查詢條件來限制取出的資料
    select * from t1 where id > 1000000 order by id limit 0, 5;
    --即利用條件id > 1000000在掃描索引是跳過1000000條記錄,然後取5條即可,這種處理方式的offset值便成為0了,但此種方式通常分頁不能用,但是可以用來分批取資料。