介紹一下如何真正的發揮索引的優勢
(1)獨立的列
我們通常會看到一些查詢不當的使用索引,或者是的MYSQL無法使用已有的索引。如果查詢中的列不是獨立的,則MYSQL就不會使用索引。“獨立的列”是指索引列不能是表達式的一部分,也不能是函數的參數
例如下面的的這個查詢語句
select student_id form student where student_id+1=5;
看一下我們就知道where中的表達式等價於actor_id=4,但是MYSQL無法自動解析這個方程式,所以我們就要優化一下這個語句,始終把索引列單獨放在比較符號的一側
下面是另一個常見的錯誤
select …… where to_days(current_data)-to_days(date_col)<=10
(2)前綴索引和索引的選擇性
有時候索引很長的字符列,會讓索引變得大且慢,碰到這種情況,一種方法是模擬哈希索引列。單有時候這樣子做仍然不夠,那么要怎么做呢?
通常可以把索引開始的部分字符,這樣可以大大節約索引空間,從而提高索引的效率,但這樣子也會降低索引的選擇性。索引的選擇性是指:不重復的索引值(基數)和數據包的記錄總數的比值,范圍從1/#T到1之間。索引的選擇性越高則查詢效率越高,因為選擇性高的索引可以讓MySQL在查找時過濾掉更多的行。唯一索引的選擇性是1,這是最好的索引選擇性,性能也是最好的。
一般情況下某個列前綴的選擇性也是足夠高的,足以滿足查詢性能。對於較長的varchar類型的列,必須使用前綴索引,因為MySQL不允許索引這些列的完整長度、
訣竅在於要選擇足夠長的前綴以保證較高的選擇性,同時又不能太長,前綴應該足夠長,以使得前綴索引的選擇性接近於索引的整個列。換句話說,前綴的基數應該接近於完整列的基數
接下來演示一下如何創建前綴索引
alter table student.id add key(id(7))
以上語句的意思就是取id的前幾位進行創建前綴索引
前綴索引是一種能使索引更小、更快的有效方法,但是另一方面也有其缺點:MySQL無法使用前綴索引做排序(order by或group by),也無法使用前綴索引做覆蓋掃描
(3)多列索引
很多人對多列索引的理解都不夠,一個常見的錯誤就是為沒格列創立獨立的索引,或者按照錯誤的順序創建多列索引。
第一個問題:為每個列創建獨立索引
create table t{ c1 int, c2 int, c3 int, key(c1), key(c2), key(c3) };
這種索引策略,一般是因為一句話“把where條件里面的列都建上索引”。實際上這句話是非常錯誤的。這樣一來最好的情況下也只能是一星索引,其性能比起真正最優的索引可能差幾個數量級。有時如果無法設計一個三星索引,那么不如忽略掉where子句,集中精力優化索引列的順序,或者創建一個全覆蓋索引
在多個列上建立獨立的單列索引大部分情況下並不能提高MySQL的查詢性能。MySQL5.0和耿勛版本引入了索引合並策略,一定程度上可以使用表上的多個單列索引來定位指定的行。更早的MySQL只能使用其中某一個單列索引,然而這個情況下沒有哪一個獨立的單列索引是非常有效的。
索引合並策略有時候是一種優化的結果,但實際上更多時候說明了表上的索引建的很糟糕:
(1)當出現服務器對多個索引做相交操作室(通常由多個AND條件),通常意味着需要一個包含所有相關列的多列索引,而不是多個獨立的單列索引
(2)當服務器需要對多個索引做聯合操作室(通常有多個OR條件),通常需要好非大量的CPU和內存資源在算法的緩存、排序和合並操作上。特別是當其中有些索引的選擇性不搞,需要合並掃描返回的大量數據的時候
(3)優化器不會把這些計算到“查詢成本”中,優化器只關心隨機頁面讀取。這會使得查詢的成本被“低估”,導致該執行計划還不如直接走全表掃描。這樣做不但會小號更多的CPU和內存資源,還可能會影響查詢的並發性,但如果是單獨運行這樣的查詢則往往會忽略對並發性的影響。
如果在EXPLAIN中看到索引合並,應該好好檢查一下查詢和表的結構,看是不是已經是最優的。也可以通過參數optimizer_switch來關閉索引合並功能,也可以使用IGNORE INDEX提示讓優化器忽略掉某些索引
(4)選擇合適的索引列順序
索引列順序實際上是非常重要的。正確的順序依賴於使用該索引的查詢,並且同時需要考慮如何更好的滿足排序和分組的需要(只用於B-Tree索引;哈希或者其他索引存儲數據並不是順序存儲)
在一個多列B-Tree索引中,索引列的順序意味着索引首先按照最左列進行排列,其次是第二列……。所以索引可以按照升序或者降序進行掃描,以滿足符合列順序的order by,group by和distinct等子句的查詢需求。
所以多列索引列的順序至關重要。對於如何選擇索引的列順序有一個經驗法則:將選擇性最高的索引放在索引的最高列。在某些場景這個經驗時非常有用,但是通常不如避免隨機IO和排序那么重要,考慮問題需要更全面。
當不需要考慮排序和分組時,將選擇性最高的列放在前面通常是很好的。這時候索引的作用只是用於優化where條件的查找。這種情況下,這樣設計的索引確實能夠最快的過濾出需要的行,對於在where的子句中只是用了索引部分前綴列的查詢來說選擇性也更高。然而性能不只是依賴於所有索引列的選擇性(整體基數),也和查詢條件的具體值有關,也就是和值的分布有關(需要根據那些運行頻率最高的查詢來調整索引列的順序,讓這種情況下的索引列的選擇性最高)。
以下面的查詢為例
select * from payment where staff_id=2 and customer_id=584
是應該創建一個(staff_id,customer_id)索引還是應該顛倒一下順序?可以跑一些查詢來確定這個表中值的分布情況,並確定哪個列的選擇性最高。先用下面的查詢預測一下,看看各個where條件的分支對應的數據基數有多大:
select sum(staff_id=2),sum(customer_id=584) from payment
根據前面的經驗法則,我們應該將索引列customer_id放在前面,因為對應條件值的customer_id數量更小。我們再來看看對於這個customer_id的條件值,對應的staff_id列的選擇性如何
這樣做有一個地方需要注意,查詢的結果非常依賴於選定的具體指。如果按上述方法優化,可能對其他一些條件值的查詢不公平, 服務器的整體性能可能變得更早,或者其他某些查詢的運行變得不如預期。
如果是從諸如pt-query-digest這樣的工具的報告中提起“最差”查詢,那么再按上述辦法選定的索引順序往往是非常搞笑的。如果沒有類似的具體查詢來運行,那么最好還是按經驗法則來做,因為經驗法則考慮的是全局基數和選擇性,而不是某個具體查詢:
從圖中我們就可以看出來customer的選擇性更高,所以答案是將其作為索引列的第一列
alter table payment add key(customer_id,staff_id)
(5)聚簇索引
局促索引並不是一種單獨的索引類型,而是一種數據存儲方式。具體的細節依賴於其實現方式,單InnoDB的局促索引實際上在同一個結構中保存了B-Tree索引和數據行。
當表有局促索引時,它的數據行實際上存放在索引的葉子頁中。術語聚簇表示數據行和相鄰的鍵值緊湊的存儲在一起。因為無法同時把數據行存放在兩個不同的地方,所以一個表只能有一個聚簇索引。
如果沒有定義主鍵,InnoDB會選擇一個唯一的非空索引代替。如果沒有這樣的索引,InnoDB會隱式定義一個主鍵來作為聚簇索引。InnoDB只聚集在同一個頁面中的記錄,包括相鄰鍵值的頁面可能會相距甚遠
聚簇主鍵可能對性能有幫助,但也可能導致嚴重的性能問題。所以需要仔細的考慮聚簇索引,尤其是將表的存儲引擎從InnoDB改成其他引擎的時候(反過來也一樣)。
聚集的數據有一些優點:
(1)可能把相關數據保存在一起。例如實現電子郵箱時,可以根據用戶ID來聚集數據,這樣子只需要從磁盤中讀取少數的數據也技能獲取某個用戶的全部郵件
(2)數據訪問更快。聚簇索引把索引和數據都放在同一個B-Tree中,因此從聚簇索引中獲取數據比從非聚簇索引中要快
(3)使用覆蓋索引掃描的查詢可以直接使用頁節點中的主鍵值
聚簇索引的缺點:
(1)聚簇數據最大限度的提高了I/O密集型應用的性能,但如果數據全部都放在內存中,則訪問的順序就沒那么重要了,聚簇索引也就沒什么優勢了
(2)插入速度嚴重依賴於插入順序。按照主鍵的順序插入是加載數據到InnoDB表中速度最快的方式。但如果不是按照逐漸順序加載數據,那么在加載完成后最好使用OPTIMIZE TABLE重新組織一下表
(3)更新聚簇索引列的代價很高。因為要強制InnoDB將每個被更新的行移動到新的位置
(4)基於聚簇索引的表在插入新航,或者主鍵被更新導致移動行的時候,可能面臨“頁分裂”的問題。當行的主鍵值要求姜哲一行插入到某個已滿的頁中時,存儲引擎會將該頁分裂成兩個頁來容納該行,這就是一次頁分裂操作,這也意味着這樣導致表占用更多的磁盤空間
(5)聚簇索引可能導致權標掃描變慢,尤其是行比較稀疏時,或者由於頁分裂導致數據存儲不連續的時候
(6)二級索引可能比想想的要更大。因為二級索引的葉子結點包含了引用行的主鍵列
(7)二級索引訪問需要兩次索引查找,而不是一次
最后一點讓人有些疑惑,為什么二級索引需要二次查找?答案在於二級索引中保存的“行指針”的實質。要記住,二級索引葉子節點保存的不是指向行的物理位置的指針而是行的主鍵值
這意味着通過二級索引查找行,需要存儲引擎找到二級索引的葉子節點獲得對應的主鍵值,然后根據這個值去聚簇索引中查找到對應的行。這里做了重復的工作:兩次B-Tree查找而不是一次,
(6)覆蓋索引
通常大家都會根據查詢的where條件來創建合適的索引,不過這也只是索引優化的一個方面。設計優秀的索引應該考慮到整個查詢,而不單單是where條件部分。索引確實是一種查找數據的高效方式,但是MySQL也可以使用索引來直接獲取列的數據,這樣就不再需要讀取數據行。如果索引一個索引包含索引需要查詢的字段的值,我們就稱其為覆蓋索引
覆蓋索引的好處如下:
(1)索引條目通常遠小於數據行的大小,所以如果只需要讀取索引,那么MySQL就會極大的減少數據訪問量。這對緩存的負載非常重要,因為這種情況下響應時間大部分花費在數據拷貝下。覆蓋索引對於I/O密集型的應用也有幫助,因為索引比數據更小,更容易全部放進去內存
(2)因為索引是按照列值順序存儲的,對於I/O密集型的范圍查詢會比隨機從磁盤讀取每一行數據的I/O要少的多。對於某些存儲引擎,例如MyISAM的Percona XtraDB,甚至可以通過POTIMIZE命令是的索引完全順序排列,這樣就可以讓簡單的范圍查詢能使用完全排序的索引訪問
(3)一些存儲引擎如MyISAM在內存中只緩存索引。數據則依賴於操作系統來緩存,因此要訪問數據需要一次系統調用。這可能會導致嚴重的性能問題,尤其是那些系統調用占了數據訪問中的最大開銷的場景
(4)由於InnoDB的聚簇索引,覆蓋索引對於InnoDB表特別有用。InnoDB的二級索引在葉子節點保存了行的主鍵值,所以如果二級主鍵能夠覆蓋查詢,則可以避免對主鍵索引的二次查詢
不是所有的索引都可以成為覆蓋索引。覆蓋索引必須要存儲索引列,而哈希索引、空間索引和全文索引等都不存儲索引列的值,所以MySQL只能使用B-Tree所以來做覆蓋索引,另外不同的存儲引擎實現覆蓋索引的方式也不同,而且不是所有的引擎都支持覆蓋索引
(7)使用索引掃描來做排序
MySQL有兩種方式可以生成有序的結果:通過排序操作;或者按索引順序掃描;如果EXPLAIN出來的type列的值為“index”,則說明MySQL使用了索引掃描來做排序
掃描索引本身是很快的,因為只需要從一條索引記錄移動到緊接着的下一條記錄。但如果索引不能覆蓋查詢所需的全部列,那就不得不每掃描一條索引記錄就都回表查詢一次對應的行。這基本上都是隨機I/O,因此按索引順序讀取數據的速度通常要比順序的全表掃描慢,尤其是在I/O密集型的工作負載時、
MySQL可以使用同一個索引既滿足排序,又用於查找行。因此,如果可能,設計索引時應該盡可能的同時滿足這兩種任務,這樣是最好的。
只有當索引的列順序和order by子句的順序完全一致,並且所有列的排序方向都一樣時,MySQL才能夠使用索引來對結果做排序。如果查詢需要關聯多張表,則只有當order by子句引用的字段全部為第一個表時,才能使用索引做排序。order by子句和查找性查詢的限制是一樣的:需要滿足索引的最左前綴的要求;否則,MySQL都需要執行的順序操作,而無法使用索引排序。
有一種情況下order by子句可以不滿足索引的最左前綴的要求,就是前導列為常量的時候。如果where 子句或者join子句中這些列指定了常量,就可以彌補索引的不足。
下面是我一些不能使用索引做排序的查詢
(8)壓縮前綴
MyISAM使用前綴壓縮來減少索引的大小,從而讓更多的索引可以放入內存中,這在某些情況下能極大的提高性能(默認只壓縮字符串,但是也可以通過參數設置壓縮數字)。
MyISAM壓縮每個索引塊的方法是,先完全保存索引塊中的第一個值,然后將其他值和第一個值進行比較得到相同前綴的字節數和剩余的不同后綴部分,把這一份存儲起來即可。比如,索引塊的第一個值是“bea”,第二個是“beautyful”,那么第二個值壓縮后就是“4,tyful”。MyISAM對於行指針也有類似的壓縮手段
壓縮塊使用更少的空間,代價是某些操作可能更慢。因為每個值的壓縮前綴都依賴前面的值,所以MyISAM查找是無法在索引塊使用二分查找只能從頭到尾進行掃描。正序的掃描速度還不錯,但是如果是倒序掃描(比如order by desc)就不是很好了。所有在快中查找一行的操作平均都需要掃描半個索引塊
測試表明,對於CPU密集型引用,因為掃描需要隨機掃描,壓縮索引使得MyISAM在索引查找上要慢好幾倍。壓縮索引的倒序掃描就更慢了。壓縮索引需要在CPU內存資源與硬盤之間做權衡。壓縮索引可能只需要1/10的磁盤空間,如果是I/O密集型應用,對某些查詢帶來的好處會比成本多很多。
(9)冗余和重復索引
重復索引是指在相同列上按照相同的順序創建的相同類型的索引。應該避免這樣的創建重復索引,發現以后也應該立即移除。比如以下代碼
create table test{ id int not null primary key, a int not null, b int not null, unique(id) index(id) }engine=InnoDB;
一個經驗不足的用戶可能是想創建一個主鍵,先加上唯一限制,然后再加上索引以供查詢使用。然而唯一限制和主鍵限制都是通過索引使用,因此上面的寫法實際上在相同的列上創建了三個重復的索引。當然了,如果這三個索引是不同類型的,那么就不算是重復索引,比如key(id)雞兒filltext key(id)就不算是重復索引
冗余索引和重復索引有一些不同,比如:如果創建了索引(A,B),再創建(A)那就是冗余索引,因為A就是前一個索引的前綴索引。索引(A,B)完全就可以當做A來使用。但是如果創建了索引(B,A)那就不是冗余索引了,索引B也不是。因為B不是索引(A,B)的最左前綴索引。另外,其他不同類型的索引,例如哈希,全文索引也不會是B-Tree的冗余索引。
冗余索引通常發生在為表添加新索引的時候。例如,有人可能會增加一個新的索引(A,B)而不是拓展已有的索引(A),還有一種情況是將一個索引擴展為(A,ID),其中的ID是主鍵,對於InnoDB來說主鍵列已經包含在二級索引當中了, 所以這也是冗余的
大多數情況下不需要冗余索引,應該盡量擴展已有的索引而不是創建新的索引。但也有時候處於性能方面的考慮需要冗余索引,因為擴展已有的索引會導致其變得太大 ,從而影響其他使用該索引的查詢的性能。例如,在一個整數列索引上添加一個很長的varchar列,那性能可能會急劇下降。特別是有索引把這個索引當中覆蓋索引時,或者這是MyISAM表並且有很多范圍查詢的時候
解決冗余索引和重復索引的方法非常簡單,刪除這些索引就可以。但是首先要做的事找出這樣的索引。可以通過歇一歇復雜的訪問information_schema表的查詢來找,不過還有兩個更簡單的方法就是使用Shlomi Noach的common_schema中的一些視圖來定位(common_schema是一系列可以安裝在服務器上的常用的存儲和視圖)。另外一個方法就是使用Percona Toolkit中的pt_duplicate-key-checker,該工具通過分析表結構來找出冗余和重復索引。
(10)未使用的索引
除了冗余索引和重復索引,可能還會有一些服務器永遠不用的索引。這樣的索引完全是累贅,建議直接刪除。有兩個工具可以幫助定位未使用的索引。最簡單的方法是在Percona Server或者Maria DB中先打開userstates服務器變量,然后讓服務器正常運行一段時間,再通過查詢INFORMATION_SCHEMA_STATISTICS就能查到每個索引的使用頻率。另外還可以使用Percona Toolkit中的pt-index-usage,該工具可以讀取查詢日志,並對日志中都每條查詢進行EXPLAIN操作,然后打印關於索引和查詢的報告
(11)索引和鎖
索引可以讓查詢鎖定更少的行。如果你的查詢從不訪問那些不需要的行,那么就會鎖定更高的行,從兩個方面來看這對性能都有什么好處。
首先:雖然InnoDB的行鎖效率很高,內存使用也很少,但是鎖定航的時候仍然會帶來額外的開銷,其次碎釘超過需要的行會增加鎖征用並減少並用性。
InnoDB只有在訪問行的時候才會對其枷鎖,而索引能夠減少InnoDB訪問的行數,從而減少鎖的數量。但這只有當InnoDB在存儲引擎層能夠過濾掉索引不需要的行時才有效。如果索引無法過濾掉無效的行,那么在InnoDB檢索到數據並返回給服務器層以后,MySQL服務器才能應用where子句。這是已經無法避免鎖定航了:InnoDB已經鎖住了這些行。到適當的時候才釋放。
比如以下的查詢語句
返回結果如下
從結果上看實際上我們僅需要對2-4之間的行進行加鎖,但是實際上獲取了1-4之間的行的排它鎖。InnoDB會鎖定第一行,這是因為MySQL為該查詢選擇的執行計划是索引的范圍掃描:
換句話說,底層存儲引擎的操作是:從索引的開頭開始獲取滿足條件actor_Id<5的記錄。服務器並沒有告訴InnoDB可以過濾掉第一行的where條件。注意EXPLAIN的EXTRA出現了using where,這表示MySQL服務器將存儲引擎返回行以后再引用了where過濾條件
保持第一個鏈接打開,然后再打開一個鏈接並執行一下查詢
這個查詢將會直接掛起,知道第一個事務釋放第一行的鎖。
以上栗子表明了即使使用索引,InnoDB也會鎖住一些不需要的數據。如果沒有使用索引查找和鎖定行的話問題將會更加糟糕,MySQL會做全表掃描並鎖定索引的行,不管需不需要。