MySQL之索引(四)
壓縮索引
MyISAM使用字首壓縮來減少索引的大小,從而讓更多的索引可以放入記憶體中,這在某些情況下能極大地提高效能。預設只壓縮字串,但通過引數設定也可以對整數做壓縮。
MyISAM壓縮每個索引塊的方法是,先完全儲存索引塊中的第一個值,然後將其他值和第一個值進行比較得到相同字首的位元組數和剩餘的不同字尾部分,把這部分儲存起來即可。例如,索引塊中的第一個值是“perform”,第二個值是“performance”,那麼第二個值的字首壓縮後儲存的是類似“7,ance”這樣的形式。MyISAM對行指標也採用類似的字首壓縮方式。
壓縮塊使用更少的空間,代價是某些操作可能更慢。因為每個值的壓縮字首都依賴前面的值,所以MyISAM查詢時無法在索引塊使用二分查詢而只能從頭開始掃描。正序的掃描速度還不錯,但是如果是倒序掃描——例如ORDER BY DESC——就不是很好了。所有在塊中查詢某一行的操作平均都需要掃描半個索引塊。
測試表明,對於CPU密集型應用,因為掃描需要隨機查詢,壓縮索引使得MyISAM在索引查詢上要慢好幾倍。壓縮索引的倒序掃描就更慢了。壓縮索引需要在CPU記憶體資源與磁碟之間做權衡。壓縮索引可能只需要十分之一大小的磁碟空間,如果是I/O密集型應用,對某些查詢帶來的好處會比成本多很多。
可以在CREATE TABLE語句中指定PACK_KEYS引數來控制索引壓縮的方式。
冗餘和重複索引
MySQL允許在相同列上建立多個索引,無論是有意還是無意的。MySQL需要單獨維護重複的索引,並且優化器在優化查詢的是時候也需要逐個地進行考慮,這會影響效能。重複索引是指的在相同的列上按照相同的順序建立的相同型別的索引,應該避免這樣建立重複索引,發現以後也應該立即刪除。
有時會在不經意間建立了重複索引,例如下面的程式碼:
CREATE TABLE test( ID INT NOT NULL PRIMARY KEY, A INT NOT NULL, B INT NOT NULL, UNIQUE(ID), INDEX(ID), ) ENGINE=InnoDB;
一個經驗不足的使用者可能是想建立一個主鍵,先加上唯一限制,然後再加上索引以供查詢使用。事實上,MySQL的唯一限制和主鍵限制都是通過索引實現的,因此,上面的寫法實際上在相同的列上建立了三個重複的索引。通常並沒有理由這樣做,除非是在同一列上建立不同型別的索引來滿足不同的查詢需求。
冗餘索引和重複索引有一些不同。如果建立了索引(A,B),再建立索引(A)就是冗餘索引,因為這只是前一個索引的字首索引。因此索引(A,B)也可以當做索引(A)來使用(這種冗餘只是對B-Tree索引來說的)。但是如果再建立索引(B,A),則不是冗餘索引,索引(B)也不是,因為B不是索引(A,B)的最左字首。另外,其他不同型別的索引(例如雜湊索引或者全文索引)也不會是B-Tree索引的冗餘索引,而無論覆蓋的索引列是什麼。
冗餘索引通常發生在為表新增新索引的時候。例如,有人可能會增加一個新的索引(A,B)而不是擴充套件已有的索引(A)。還有一種情況是將一個索引擴充套件為(A,ID),其中ID是主鍵,對於InnoDB來說主鍵列已經包含在二級索引中了,所以這也是冗餘的。
大多數情況下都不需要冗餘索引,應該儘量擴充套件已有的索引而不是建立新索引。但也有時候出於效能方面的考慮需要冗餘索引,因為擴充套件已有的索引會導致其變得太大,從而影響其他使用該索引的查詢的效能。
例如,如果在整數列上有一個索引,現在需要額外增加一個很長的VARCHAR列來擴充套件該索引,那效能可能會急劇下降。特別是有查詢把這個索引當做覆蓋索引,或者這是MyISAM表並且有很多範圍查詢(由於MyISAM的字首壓縮)的時候。
有一個userinfo表,這個表有1000 000行,對每個state_id值大概有20 000條記錄。在state_id列有一個索引對下面的查詢有用,假設查詢名為Q1:
SELECT count(*) FROM userinfo WHERE state_Id=5;
一個簡單的測試表明該查詢的執行速度大概是每秒115次(QPS)。還有一個相關查詢需要檢索幾個列的值,而不是隻統計行數,假設名為Q2:
SELECT state_id,city,address FROM userinfo WHERE state_id=5;
對於這個查詢,測試結果QPS小於10。提升該查詢效能的最簡單辦法就是擴充套件索引為(state_id,city,address),讓索引能覆蓋查詢:
ALTER TABLE userinfo DROP KEY state_id, ADD KEY state_id_2(state_id,city,address);
索引擴充套件後,Q2執行得更快了,但是Q1卻變慢了。如果我們想讓兩個查詢都變得更快,就需要兩個索引,儘管這樣一來原來的單列索引是冗餘的了。表1-3顯示這兩個查詢在不同索引策略下的詳細結果,分別使用MyISAM和InnoDB儲存引擎。注意到只有state_id_2索引時,InnoDB引擎上的查詢Q1的效能下降並不明顯,這是因為InnoDB沒有使用索引壓縮。
只有state_id | 只state_id_2 | 同時有state_id和state_d_2 | |
MyISAM, Q1 | 114.96 | 25.40 | 112.19 |
MylSAM, Q2 | 9.97 | 16.34 | 16.37 |
InnoDB, Q1 | 108.55 | 100.33 | 107.97 |
InnoDB, Q2 | 12.12 | 28.04 | 28.06 |
有兩個索引的缺點是索引成本更高。表1-4顯示了想表中插入100萬行資料所需要的時間。
只有state_id | 同時有state_id和state_ id_2 | |
InnoDB,對兩個索引都有足夠的內容 | 80秒 | 136秒 |
MylSAM,只有一個索引有足夠的內容 | 72秒 | 470秒 |
可以看到,表中的索引越多插入速度越慢。一般來說,增加新索引將會導致INSERT、UPDATE、DELETE等操作的速度變慢,特別是當新增索引後導致達到了記憶體瓶頸的時候。
解決冗餘索引和重複索引的方法很簡單,刪除這些索引就可以,但首先要做的是找出這樣的索引。可以通過寫一些複雜的訪問INFORMATION_SCHEMA表的查詢來找,不過還有兩個更簡單的方法。可使用Shlomi Noach的common_schema中的一些檢視來定位,common_schema是一系列可以安裝到伺服器上的常用的儲存和檢視。這筆自己編寫查詢要快而且簡單。另外也可以使用Percona Toolkit中的pt-duplicate-key-checker,該工具通過分析表結構來找出冗餘和重複的索引。對於大型伺服器來說,使用外部的工具可能更合適些;如果伺服器上有大量的資料或者大量的表,查詢INFORMATION_SCHEMA表可能會導致效能問題。
在決定哪些索引可以被刪除的時候要非常小心。回憶一下,在前面的InnoDB的示例表中,因為二級索引的葉子節點包含了主鍵值,所以在列(A)上的索引就相當於在(A,ID)上的索引。如果有像WHERE A = 5 ORDER BY ID這樣的查詢,這個索引會很有作用。但如果將索引擴充套件為(A,B),則實際上就變成了(A,B,ID),那麼上面查詢的ORDER BY子句就無法使用該索引做排序,而只能用檔案排序了。所以,建議使用Percona工具箱中的pt-upgrade工具來仔細檢查計劃中的索引變更。
未使用的索引
除了冗餘索引和重複索引,可能還會有一些伺服器永遠不使用的索引,這樣的索引完全是累贅,建議考慮刪除,有兩個工具可以幫助定位未使用的索引:
- 在Percona Server或者Mariadb中先開啟userstates伺服器變數(預設是關閉的),然後讓伺服器執行一段時間,再通過查詢INFORMATION_SCHEMA.INDEX_STATISTICS就能查到每個索引的使用頻率。
- 使用Percona Toolkit中的pt-index-usage工具,該工具可以讀取查詢日誌,並對日誌中的每個查詢進行EXPLAIN操作,然後打印出關於索引和查詢的報告。這個工具不僅可以找出哪些索引是未使用的,還可以瞭解查詢的執行計劃——例如在某些情況下有些類似的查詢的執行方式不一樣,這可以幫助定位到那些偶爾伺服器質量差的查詢,該工具也可以將結果寫入到MySQL的表中,方便查詢結果。
索引和鎖
索引可以讓查詢鎖定更少的行,如果查詢從不訪問那些不需要的行,那麼就會鎖定更少的行,從兩個方面來看這對效能都有好處。首先,雖然InnoDB的行鎖效率很高,記憶體使用也很少,但是鎖定行的時候仍然會帶來額外開銷;其次,鎖定超過需要的行會增加鎖爭用並減少併發性。
InnoDB只有在訪問行的時候才會對其加鎖,而索引能夠減少InnoDB訪問的行數,從而減少鎖的數量。但這隻有當InnoDB在儲存引擎層能夠過濾掉所有不需要的行時才有效。如果索引無法過濾掉無效的行,那麼在InnoDB檢索到資料並返回給伺服器層以後,MySQL伺服器才能應用 WHERE子句。這時已經無法避免鎖定行了:InnoDB已經鎖住這些行,到適當的時候才釋放。在MySQL5.1及更新的版本中,InnoDB可以在伺服器端過濾掉行後就釋放鎖。
通過下面的例子再次使用資料庫Sakila很好的解釋這些情況:
mysql> SET AUTOCOMMIT = 0; Query OK, 0 rows affected (0.00 sec) mysql> SELECT actor_id FROM actor WHERE actor_id < 5 AND actor_id <> 1 FOR UPDATE; +----------+ | actor_id | +----------+ | 2 | | 3 | | 4 | +----------+ 3 rows in set (0.01 sec)
這條查詢只返回2~4行資料,實際上獲取1~4行排他鎖。InnoDB鎖住第1行,因為MySQL為該查詢選擇的執行計劃是索引範圍掃描:
mysql> EXPLAIN SELECT actor_id FROM actor WHERE actor_id < 5 AND actor_id <> 1 FOR UPDATE\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: actor partitions: NULL type: range possible_keys: PRIMARY key: PRIMARY key_len: 2 ref: NULL rows: 4 filtered: 100.00 Extra: Using where; Using index 1 row in set, 1 warning (0.00 sec)
換句話說,底層儲存引擎的操作是“從索引的開頭獲取滿足條件 actor_id < 5 的記錄”,伺服器並沒有告訴InnoDB可以過濾第1行的WHERE 條件。注意到EXPLAIN的Extra出現“Using Where”表示MySQL伺服器將儲存引擎返回行以後再應用WHERE 過濾條件。
第二個查詢就能證明第一行確實已經被鎖定(重新開啟一個MySQL的控制檯),儘管第一個查詢的結果並沒有這個第一行。保持第一個連線的開啟,然後開啟第二個連線並執行如下查詢:
mysql> SET AUTOCOMMIT = 0; Query OK, 0 rows affected (0.00 sec) mysql> BEGIN; Query OK, 0 rows affected (0.00 sec) mysql> SELECT actor_id FROM actor WHERE actor_id = 1 FOR UPDATE;
這個查詢將會掛起,直到第一個事物釋放第一行的鎖。這個行為對於基於語句的複製的正常執行來說是必要的。就像這個例子顯示的,即使使用了索引,InnoDB可能也會鎖住一些不需要的資料。如果不能使用索引查詢和鎖定行的話問題可能會很糟糕,MySQL會做全表掃描並鎖定所有的行,而不管是不是需要。關於InnoDB,索引和鎖有一些很少有人知道的細節:InnoDB在二級索引上使用共享鎖(讀鎖),但訪問主鍵索引需要排他鎖(寫),這消除了使用覆蓋索引的可能性,並且使得SELECT FOR UPDATE 比LOCK IN SHARE MODE 或非鎖定查詢要慢得多。