目錄
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;