六、mysql中的索引

目錄

6.1. 索引的常見模型

哈希表

有序數組

二叉搜索樹

6.2. InnoDB 的索引模型?

6.3. 主鍵索引和非主鍵索引的存儲區別?

6.4. 基於主鍵索引和普通索引的查詢有什麼區別?

6.5. 索引的維護?

6.6. 如何重建索引?

6.7. 如何重建主鍵索引?

6.8. 覆蓋索引?

6.9. 聯合索引?

6.10. 最左前綴原則?

6.11. 索引下推

6.12. 普通索引和唯一索引,應該怎麼選擇?

查詢過程:

更新過程:

merge 的執行流程是這樣的:

6.13. mysql爲什麼會選錯索引?

優化器、掃描行數

採樣統計的方法?

索引統計的行數不準確?

索引選擇異常和處理?

6.14. 怎麼給字符串字段加索引?

6.15. InnoDB刷髒頁的控制策略?

設計策略控制刷髒頁的速度,會參考哪些因素呢?


6.1. 索引的常見模型

哈希表

是一種以鍵 - 值(key-value)存儲數據的結構,我們只要輸入待查找的值即 key,就可以找到其對應的值即 Value。哈希的思路很簡單,把值放在數組裏,用一個哈希函數把 key 換算成一個確定的位置,然後把 value 放在數組的這個位置。不可避免地,多個 key 值經過哈希函數的換算,會出現同一個值的情況。處理這種情況的一種方法是,拉出一個鏈表。( 哈希表這種結構適用於只有等值查詢的場景,比如 Memcached 及其他一些 NoSQL 引擎)(插入快,範圍查詢慢)

有序數組

在等值查詢和範圍查詢場景中的性能就都非常優秀。 但是,在需要更新數據的時候就麻煩了,你往中間插入一個記錄就必須得挪動後面所有的記錄,成本太高。所以,有序數組索引只適用於靜態存儲引擎,比如你要保存的是 2017 年某個城市的所有人口信息,這類不會再修改的數據。

二叉搜索樹

 二叉搜索樹的特點是:每個節點的左兒子小於父節點,父節點又小於右兒子。

樹可以有二叉,也可以有多叉。多叉樹就是每個節點有多個兒子,兒子之間的大小保證從左到右遞增。二叉樹是搜索效率最高的,但是實際上大多數的數據庫存儲卻並不使用二叉樹。其原因是,索引不止存在內存中,還要寫到磁盤上。

6.2. InnoDB 的索引模型?

在 InnoDB 中,表都是根據主鍵順序以索引的形式存放的,這種存儲方式的表稱爲索引組織表。又因爲前面我們提到的,InnoDB 使用了 B+ 樹索引模型,所以數據都是存儲在 B+ 樹中的。每一個索引在 InnoDB 裏面對應一棵 B+ 樹。

 

6.3. 主鍵索引和非主鍵索引的存儲區別?

主鍵索引的葉子節點存的是整行數據。在 InnoDB 裏,主鍵索引也被稱爲聚簇索引(clustered index);

非主鍵索引的葉子節點內容是主鍵的值。在 InnoDB 裏,非主鍵索引也被稱爲二級索引(secondary index)。

6.4. 基於主鍵索引和普通索引的查詢有什麼區別?

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

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

也就是說,基於非主鍵索引的查詢需要多掃描一棵索引樹。因此,我們在應用中應該儘量使用主鍵查詢

 

6.5. 索引的維護?

    B+ 樹爲了維護索引有序性,在插入新值的時候需要做必要的維護。以上面這個圖爲例,如果插入新的行 ID 值爲 700,則只需要在 R5 的記錄後面插入一個新記錄。如果新插入的 ID 值爲 400,就相對麻煩了,需要邏輯上挪動後面的數據,空出位置。

    而更糟的情況是,如果 R5 所在的數據頁已經滿了,根據 B+ 樹的算法,這時候需要申請一個新的數據頁,然後挪動部分數據過去。這個過程稱爲頁分裂。在這種情況下,性能自然會受影響。

除了性能外,頁分裂操作還影響數據頁的利用率。原本放在一個頁的數據,現在分到兩個頁中,整體空間利用率降低大約 50%。當然有分裂就有合併。當相鄰兩個頁由於刪除了數據,利用率很低之後,會將數據頁做合併。合併的過程,可以認爲是分裂過程的逆過程。

 

6.6. 如何重建索引?

 

alter table T drop index k;

alter table T add index(k);

 

6.7. 如何重建主鍵索引?

alter table T drop primary key;

alter table T add primary key(id);

重建索引 k 的做法是合理的,可以達到省空間的目的。但是,重建主鍵的過程不合理。不論是刪除主鍵還是創建主鍵,都會將整個表重建。所以連着執行這兩個語句的話,第一個語句就白做了。這兩個語句,你可以用這個語句代替 : alter table T engine=InnoDB

 

6.8. 覆蓋索引?

由於覆蓋索引可以減少樹的搜索次數,顯著提升查詢性能,所以使用覆蓋索引是一個常用的性能優化手段。

select * from T where k between 3 and 5;

上邊的查詢語句可以使用覆蓋索引進行優化?

select * from T where ID IN(select ID from T where k between 3 and 5)

 

6.9. 聯合索引?

聯合索引是指 聯合索引就是一棵B+樹,不同的是聯合索引的鍵值的數量不是1,而是>=2。

聯合索引的第二個好處是在第一個鍵相同的情況下,已經對第二個鍵進行了排序處理

對於查詢select * from table where a=xxx and b=xxx, 顯然是可以使用(a,b) 這個聯合索引的;

對於單個列a的查詢select * from table where a=xxx,也是可以使用(a,b)這個索引的。

但對於b列的查詢select * from table where b=xxx,則不可以使用(a,b) 索引,其實你不難發現原因,葉子節點上b的值爲1、2、1、4、1、2顯然不是排序的,因此對於b列的查詢使用不到(a,b) 索引。

 

6.10. 最左前綴原則?

如果爲每一種查詢都設計一個索引,索引是不是太多了,但是又不能讓業務查詢進行全表掃描把?

B+ 樹這種索引結構,可以利用索引的“最左前綴”,來定位記錄。

可以看到,索引項是按照索引定義裏面出現的字段順序排序的。當你的邏輯需求是查到所有名字是“張三”的人時,可以快速定位到 ID4,然後向後遍歷得到所有需要的結果。如果你要查的是所有名字第一個字是“張”的人,你的 SQL 語句的條件是"where name like ‘張 %’"。這時,你也能夠用上這個索引,查找到第一個符合條件的記錄是 ID3,然後向後遍歷,直到不滿足條件爲止。

可以看到,不只是索引的全部定義,只要滿足最左前綴,就可以利用索引來加速檢索。這個最左前綴可以是聯合索引的最左 N 個字段,也可以是字符串索引的最左 M 個字符。

基於上面對最左前綴索引的說明,我們來討論一個問題:在建立聯合索引的時候,如何安排索引內的字段順序。

那麼,如果既有聯合查詢,又有基於 a、b 各自的查詢呢?查詢條件裏面只有 b 的語句,是無法使用 (a,b) 這個聯合索引的,這時候你不得不維護另外一個索引,也就是說你需要同時維護 (a,b)、(b) 這兩個索引。 這時候,我們要考慮的原則就是空間了。比如上面這個市民表的情況,name 字段是比 age 字段大的 ,那我就建議你創建一個(name,age) 的聯合索引和一個 (age) 的單字段索引。

 

6.11. 索引下推

圖 1 中,在 (name,age) 索引裏面去掉了 age 的值,這個過程 InnoDB 並不會去看 age 的值,只是按順序把“name 第一個字是’張’”的記錄一條條取出來回表。因此,需要回表 4 次。

圖 2 跟圖 1的區別是,InnoDB 在 (name,age) 索引內部就判斷了 age 是否等於 10,對於不等於 10 的記錄,直接判斷並跳過。在我們的這個例子中,只需要對 ID4、ID5 這兩條記錄回表取數據判斷,就只需要回表 2 次。

6.12. 普通索引和唯一索引,應該怎麼選擇?

查詢過程:

    對於普通索引來說 ,查找到滿足條件的第一個記錄後,需要查找下一個記錄,直到碰到第一個不滿足條件的記錄;

    對於唯一索引來說,由於索引定義的唯一性,查找到第一個滿足條件的記錄後,就會停止繼續檢索。

由於mysql的innodb引擎是按數據頁爲單位來讀寫的,也就是說,當需要讀一條記錄的時候,並不是將這個記錄本身從磁盤讀出,而是以頁爲單位將其整體讀入內存。再Innodb中每個數據頁的大小默認爲16KB.

更新過程:

    當需要更新一個數據頁時,如果數據頁在內存中就直接更新,否則Innodb會先將這些更新操作緩存到change buffer中,這樣就不需要從磁盤中讀入這個數據頁了。在下次查詢需要訪問這個數據頁的時候,將數據頁讀入內存,然後執行change buffer中與這個頁有關的操作;通過這種方式就能保證這個數據邏輯的正確性。

    對於唯一索引來說,所有的更新操作都要先判斷這個操作是否違反唯一性約束,而這必須要讀入到內存才能判斷,如果已經讀入到內存則直接更新內存會更快,就不需要使用change buffer了。

    change buffer也就只能是普通索引才能使用

change buffer 用的是 buffer pool 裏的內存,因此不能無限增大。change buffer 的大小,可以通過參數 innodb_change_buffer_max_size 來動態設置。這個參數設置爲 50 的時候,表示 change buffer 的大小最多隻能佔用 buffer pool 的 50%。

merge 的執行流程是這樣的:

從磁盤讀入數據頁到內存(老版本的數據頁);

從 change buffer 裏找出這個數據頁的 change buffer 記錄 (可能有多個),依次應用,得到新版數據頁;寫 redo log;

這個 redo log 包含了數據的變更和 change buffer 的變更;

到這裏 merge 過程就結束了;

這時候,數據頁和內存中 change buffer 對應的磁盤位置都還沒有修改,屬於髒頁,之後各自刷回自己的物理數據,就是另外一個過程了。

6.13. mysql爲什麼會選錯索引?

優化器、掃描行數

mysql在真正開始執行語句之前,並不能精確地直到滿足條件的記錄有多少條,而只能根據統計信息來估算記錄數;

這個統計信息就是索引的“區分度”。顯然,一個索引上不同的值越多,這個索引的區分度就越好;而一個索引上

不同的值的個數,我們稱之爲“基數”,也就是說,這個基數越大,索引的區分度越好。可以使用 ”show index  from 表名“方法,看到一個索引的基數。

mysql是怎麼得到索引的基數?

mysql是選用採樣統計的方式,因爲把整張表取出來一行行統計,雖然可以得到精確的結果,但是代價太高,所以只能選擇

“採樣統計”。

採樣統計的方法?

    採樣統計的時候,InnoDB默認會選擇N個數據頁,停機這些頁面上的不同值,得到一個平均值,然後乘以這個索引的頁面數,就得到這個索引的基數。

在 MySQL 中,有兩種存儲索引統計的方式,可以通過設置參數 innodb_stats_persistent 的值來選擇:設置爲 on 的時候,表示統計信息會持久化存儲。這時,默認的 N 是 20,M 是 10。設置爲 off 的時候,表示統計信息只存儲在內存中。這時,默認的 N 是 8,M 是 16。由於是採樣統計,所以不管 N 是 20 還是 8,這個基數都是很容易不準的。

索引統計的行數不準確?

如果只是索引統計不準確,通過 analyze 命令可以解決很多問題,但是前面我們說了,優化器可不止是看掃描行數。

索引選擇異常和處理?

一種方法是,採用 force index 強行選擇一個索引;

第二種方法就是,我們可以考慮修改語句,引導 MySQL 使用我們期望的索引。

第三種方法是,在有些場景下,我們可以新建一個更合適的索引,來提供給優化器做選擇,或刪掉誤用的索引。

6.14. 怎麼給字符串字段加索引?

1、直接創建完整索引,這樣可能比較佔用空間;

2、創建前綴索引,節省空間,但會增加查詢掃描次數,並且不能使用覆蓋索引;

3、倒序存儲,再創建前綴索引,用於繞過字符串本身前綴的區分度不夠的問題;

4、創建 hash 字段索引,查詢性能穩定,有額外的存儲和計算消耗,跟第三種方式一樣,都不支持範圍掃描。

 

6.15. InnoDB刷髒頁的控制策略?

mysql中的redolog日誌是一個循環讀寫的日誌,寫滿了就需要把內存中的數據flush到磁盤中,以便於從新開始寫日誌,以保證數據的一致性;當內存數據頁跟磁盤數據頁內容不一致的時候,我們稱這個內存頁爲“髒頁”。內存數據寫入到磁盤後,內存和磁盤上的數據頁的內容就一致了,稱爲“乾淨頁”。

所以,刷髒頁雖然是常態,但是出現以下這兩種情況,都是會明顯影響性能的:

1、一個查詢要淘汰的髒頁個數太多,會導致查詢的響應時間明顯變長;

2、日誌寫滿,更新全部堵住,寫性能跌爲 0,這種情況對敏感業務來說,是不能接受的。所以,InnoDB 需要有控制髒頁比例的機制,來儘量避免上面的這兩種情況。

正確地告訴 InnoDB 所在主機的 IO 能力,這樣 InnoDB 才能知道需要全力刷髒頁的時候,可以刷多快。這就要用到 innodb_io_capacity 這個參數了,它會告訴 InnoDB 你的磁盤能力。這個值我建議你設置成磁盤的 IOPS。磁盤的 IOPS 可以通過 fio 這個工具來測試,下面的語句是我用來測試磁盤隨機讀寫的命令:

fio -filename=$filename -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest

設計策略控制刷髒頁的速度,會參考哪些因素呢?

這個問題可以這麼想,如果刷太慢,會出現什麼情況?首先是內存髒頁太多,其次是 redo log 寫滿。

所以,InnoDB 的刷盤速度就是要參考這兩個因素:一個是髒頁比例,一個是 redo log 寫盤速度。

參數 innodb_max_dirty_pages_pct 是髒頁比例上限,默認值是 75%。InnoDB 會根據當前的髒頁比例(假設爲 M),算出一個範圍在 0 到 100 之間的數字,計算這個數字的僞代碼類似這樣:

mysql> select VARIABLE_VALUE into @a from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_dirty';

select VARIABLE_VALUE into @b from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_total';

select @a/@b;

 

發佈了102 篇原創文章 · 獲贊 14 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章