索引(index),在MySQL中也被叫做鍵(key),是儲存引擎用於快速找到記錄的一種資料結構。索引優化是對查詢效能優化最有效的手段。
 
5.1 索引基礎
 
索引的型別
 
索引是在儲存引擎層而不是伺服器層實現的。所以,並沒有統一的索引標準;
 
B-Tree 索引
 
不同的儲存引擎以不同的方式使用B-Tree索引,效能也各有不同;例如,MyISAM使用字首壓縮技術使得索引更小,而InnoDB則按照原資料格式進行儲存。再如MyISAM索引通過資料的物理位置引用被索引的行,而InooDB則根據主鍵索引被索引的行。
 
B-Tree索引適用於全鍵值、鍵值範圍或鍵字首查詢(即 =、>=、 <=、 >、 <、 !=、 和 like ' keyword%')。因為索引樹中的節點是有序的,所以除了按值查詢之外,索引還可以用於查詢中的order by和group by操作。
 
索引有效的型別
 
  • 全值匹配:是指和索引中的所有列進行匹配。
 
  • 匹配最左字首:即只使用索引的第一列。
 
  • 匹配列字首:即只使用索引第一列的開頭部分。
 
  • 匹配範圍值:只使用了索引的第一列。
 
  • 精確匹配某個列並範圍匹配另外一列:即索引第一列全匹配,第二列範圍匹配。
 
  • 只訪問索引的查詢:又被稱為“覆蓋索引”,B-tree通常支援“只訪問索引的查詢”,即查詢只需要訪問索引,而無須訪問資料行。如使用者名稱與密碼的匹配,手機號與驗證碼的匹配。
 
索引無效的型別
 
  • 如果不是按照索引的最左列開始查詢,則無法使用索引。
 
  • 不能跳過索引中的列。
 
  • 如果查詢中有某個列的範圍查詢,則其右邊所有列都無法使用索引。
 
綜上所述,能否有效使用索引的關鍵在於索引列的順序。在優化效能的時候,可能需要使用相同的列但順序不同的索引來滿足不同型別的查詢需求。
 
5.2 索引的優點
 
優勢
 
  • 索引大大減少了伺服器需要掃描的資料量;
 
  • 索引可以幫助伺服器避免排序和臨時表;
 
  • 索引可以將隨機I/O變為順序I/O;
 
評價索引是否合適的“三星系統”(three-star system)
 
  • 索引相關記錄放在一起則獲得一星;
 
  • 索引中的資料順序和查詢中的排序順序一致則獲得二星;
 
  • 索引中的列包含了查詢中需要的全部列則獲取三星;
 
5.3 高效能的索引策略
 
獨立的列
 
如果查詢中的列不是獨立的,則MySQL就不會使用索引,所謂獨立的列,是指索引列不能是表示式的一部分,也不能使函式的引數。
 
字首索引和索引選擇性
 
索引的選擇性是指,不重複的索引值(也稱為基數,cardinality)和資料表的記錄的總數(#T)的比值,範圍從1/#T到1之間。索引的選擇性越高則查詢效率越高,因為選擇性高的所以可以讓MySQL在查詢時過濾掉更多的行。唯一索引的選擇性是1,這是最好的索引選擇性,效能也是最好的。
 
對於較長的VARCHAR型別的列如果必須新增索引,則必須使用字首索引,因為MySQL不允許索引這些列的完整長度。取捨在於要選擇足夠長的字首以保證較高的選擇性,同時又不能太長,以便節約空間(即在索引選擇性與索引空間之間做平衡)。
 
字首索引是一種能使索引更小、更快的有效方法,但另一方面也有其缺點:MySQL無法使用字首索引做order by 和 group by,也無法使用字首索引做覆蓋掃描。
 
字首索引的長度的判斷方法
 
SELECT
    count(DISTINCT LEFT(field_name, 3)) / count(*) AS sel3,
    count(DISTINCT LEFT(field_name, 4)) / count(*) AS sel4,
    count(DISTINCT LEFT(field_name, 5)) / count(*) AS sel5,
    count(DISTINCT LEFT(field_name, 6)) / count(*) AS sel6,
    count(DISTINCT LEFT(field_name, 7)) / count(*) AS sel7,
FROM
    table_name
 
新增字首索引
 
ALTER TABLE table_name ADD KEY (field_name, num);
 
多列索引
 
為每個列建立獨立的索引或者按照錯誤的順序建立多列索引,是常見的錯誤索引策略。應該集中精力優化索引列的順序,或者建立一個全覆蓋的索引。
 
在多個列上建立獨立的單列索引大部分情況下並不能提高MySQL的查詢效能。MySQL的“索引合併”(index merge)策略一定程式上可以使用表上的多個單列索引來定位指定的行。索引合併策略能夠同時使用多個單列索引進行掃描,並將結果進行合併。具體可檢視explain中的extra列。
 
雖然索引合併策略有時候是一種優化的結果,但從另一角度也說明索引建得很糟糕(即如果在explain中看到有索引合併,則應該檢查一下查詢和表的結構)。
 
  • 當出現伺服器對多個索引做相交操作時(通常有多個and條件),通常意味著需要一個包含所有香港列的多列索引,而不是多個獨立的單列索引。
 
  • 當伺服器需要對多個索引做聯合操作時(通常有多個or條件),通常需要消耗大量CPU和記憶體資源在演算法的快取、排序和合並操作上。特別是當其中有些索引的選擇性不高,需要合併掃描返回的大量資料的時候。
 
  • 更重要的是,優化器不會把這些計算到“查詢成本”(cost)中,優化器只關心隨機頁面讀取。這會使得查詢的成本被“低估”,還可能會影響查詢的併發性,但如果是單獨執行這樣的查詢則往往會忽略對併發性的影響。
 
選擇合適的索引列順序
 
正確的索引列順序依賴於該索引的查詢,並且同時需要考慮如何更好的滿足排序和分組的需要(order by、group by、distinct等)。
 
一般將選擇性最高的列放在索引最前列。但這也不是一個放之四海皆準的法則,因為不同場景選擇不同。
 
SELECT
    count(DISTINCT field_name_a) / count(0),
    count(DISTINCT field_name_b) / count(0),
    count(0)
FROM
    table_name
 
當不需要考慮排序和分組時,將選擇性最高的列放在前面通常是很好的。這時候索引的作用只是用於優化where條件的查詢。在這種情況下,這樣設計的索引確實能夠最快地過濾出需要的行,對於在where子句中只使用了索引部分字首列的查詢來說選擇性也更高。然而,效能不只是依賴於所有索引列的選擇性(整體基數),也和查詢條件的具體值有關,也就是和值的分佈有關。可能需要根據那些執行頻率最高的查詢來調整索引列的順序,讓這種情況下的索引的選擇性最高。
 
聚簇索引表(索引組織表)
 
聚簇索引並不是一種單獨的索引型別,而是一種資料儲存方式。InnoDB的聚簇索引實際上在同一個結構中儲存了B-Tree索引和資料行。
 
 
聚簇資料的優點
 
  • 可以把相關資料儲存在一起。
 
  • 資料訪問更快。聚簇索引將索引和資料儲存在同一個B-Tree中,因此從聚簇索引中獲取資料通常比在非聚簇索引中查詢要快。
 
  • 使用覆蓋索引掃描的查詢可以直接使用葉節點的主鍵值。
 
聚簇資料的缺點
 
  • 聚簇資料最大限度地提高了IO密集型應用的效能。但如果資料全部都放在記憶體中,則訪問的順序就沒那麼重要了,聚簇索引也就沒了優勢。
 
  • 插入速度嚴重依賴插入順序,按照主鍵的順序是載入資料到InnoDB表中速度最快的方式。但如果不是按照主鍵順序載入資料,那麼在載入完成後最好使用optimize table命令重新組織一下表。
 
  • 更新聚簇索引列的代價很高,因為會強制InnoDB將每個被更新的行移動到新的位置。
 
  • 基於聚簇索引的表在插入新行,或者主鍵被更新導致需要移動行的時候,可能面臨“頁分裂”(page split)的問題。當行的主鍵值要求必須將這一行插入到某個已滿的頁中時,儲存引擎會將該頁分裂成兩個頁面來容納該行,這就是一次頁分裂操作。頁分裂會導致表佔用更多的磁碟空間。
 
  • 聚簇索引可能導致全表掃描變慢,尤其是行比較稀疏,或者由於頁分裂導致資料儲存不連續的時候。
 
  • 二級索引(非聚簇索引)可能比想象的要更大,因為在二級索引的葉子節點包含了引用行的主鍵列。
 
  • 二次索引訪問需要兩次索引查詢,而不是一次。這是因為二級索引中儲存的“行指標”的實質。二級索引葉子節點儲存的不是指向行的物理位置的指標,而是行的主鍵值。通過二級索引查詢行,儲存引擎需要找到二級索引的葉子節點獲得對應的主鍵值,然後根據這個值取聚簇索引中查詢到對應的行。
 
覆蓋索引
 
如果一個索引包含(或者說覆蓋)所有需要查詢的欄位的值,我們就稱之為“覆蓋索引”。
 
優點
 
  • 索引條目通常遠小於資料行大小,所以如果只需要讀取索引,那MySQL就會極大地減少資料訪問量。這對快取的負載非常重要,因為這種情況下響應時間大部分花費在資料拷貝上。覆蓋索引對於IO密集型的應用也有幫助,因為索引比資料更小,更容易全部放入記憶體中。
 
  • 因為索引是按照列值順序儲存的(至少在單個數據頁內是如此)所以對於IO密集型的範圍查詢會比隨機從磁碟讀取每一行資料的IO要少得多。
 
  • 由於InnoDB的聚簇索引,覆蓋索引對InnoDB表特別有用。InnoDB的二級索引在葉子節點中儲存了行的主鍵值,所以如果二級索引能夠覆蓋查詢,則可以比main對主鍵索引的二次查詢。
 
使用索引掃描來做排序
 
MySQL有兩種方式可以生成有序的結果:通過排序操作(檔案排序) 和按索引順序掃描。掃描所有本身速度很快,因為只需要從一條索引記錄移動到緊接著的下一條記錄。但如果索引不能覆蓋查詢所需的全部列,那就不得不每掃描一條索引記錄就都回表查詢一次對應的行。這基本上都是隨機IO。因此按索引順序讀取資料的速度通常要比順序地全表掃描慢,尤其在IO密集型的工作負載時。
 
只有當索引的列順序和order by子句的順序完全一致,並且所有列的排序方向都一樣時,MySQL才能夠使用索引來對結果做排序。如果查詢需要關聯多張表,則只有當order by子句引用的欄位全部為第一張表時,才能使用索引做排序。order by子句和查詢行查詢的限制是一樣的:需要滿足索引的最左字首的要求;否則,MySQL都需要執行排序操作,而無法利用索引排序。
 
有一個情況下order by子句可以不滿足索引的最左字首的要求,就是前導列為常熟的時候。如果where子句或者join子句中隊這些列指定了常量(而不是範圍),就可以“彌補”索引的不足。
 
示例
 
CREATE TABLE table_name (
    id INT auto_increment,
    a INT NOT NULL,
    b INT NOT NULL,
    c INT NOT NULL,
    PRIMARY KEY (id),
    KEY `idx_ab` (a, b) ,
    KEY `idx_c` (field_c)
);
 
 
排序 是否使用了索引掃描來做排序
ORDER BY a yes
ORDER BY a, b yes
ORDER BY a, b desc no
WHERE a = 1 ORDER BY b yes
WHERE a > 1 ORDER BY a, b  yes
WHERE a =1 ORDER BY b, c no
WHERE a > 1 ORDER BY b no
 
使用索引做排序的一個最重要的用法是當查詢同時有 order by 和 limit 子句的時候。
 
冗餘和重複索引
 
重複索引是指在相同的列上按照相同的順序建立的相同型別的索引。應該避免這樣情況的出現。如一張表裡出現primary key(id), unique(id), index(id)。
 
MySQL允許在相同列上建立多個索引。但MySQL需要單獨維護重複的索引,並且優化器在優化查詢時也需要逐個進行考慮,這將影響效能。
 
冗餘索引和重複索引有一些不同。如建立了索引(A,B),再建立索引(A)就是冗餘索引,因為這只是前一個索引的字首索引。因此索引(A,B)也可以當作索引(A)來使用。但是如果在建立索引(B,A)或索引(B),則不是冗餘索引。另外其他不同型別的索引(例如雜湊索引或者全文索引) 也不會是B-Tree索引的冗餘索引。
 
大多數情況下都不需要冗餘索引,應該儘量擴充套件已有的索引而不是建立新索引。但也有時候會處於效能方面的老驢需要冗餘索引,因為擴充套件已有的索引會導致其變得太大,從而影響其他使用該索引的查詢效能。例如,如果在整數列上有一個索引,現在需要額外增加一個很長的varchar列來擴充套件改索引,那效能可能會急劇下降。特別是有查詢把這個索引當作覆蓋索引。
 
二級索引的葉子節點包含了主鍵值,所以在列(A)上的索引就相當於在(A,ID)上的索引,如果有像where A = 5 order by ID這樣的查詢,這樣的索引會很有作用。單如果將索引擴充套件為(A,B),則實際上就變成了(A,B,ID),那麼上面查詢的order by子句就無法使用該索引做排序,而只能用檔案排序。
 
未使用的索引
 
這樣的索引完全是累贅,建議考慮刪除。
 
5.5 維護索引和表
 
維護表的三個主要目的:找到並修復損壞的表、維護準確的索引統計資訊、減少碎片。
 
找到並修復損壞的表
 
CHECK TABLE table_name;
REPAIR TABLE table_name;
ALTER TABLE table_name ENGINE = INNODB;
 
更新索引統計資訊
 
MySQL的查詢優化器會通過兩個API來了解儲存引擎的索引值的分佈資訊,以決定如何使用索引。第一個API是records_in_range(),通過向儲存引擎傳入兩個邊界值獲取在這個範圍大概有多少條記錄。第二個API是info(),該介面返回各種型別的資料,包括索引的基礎(每個鍵值有多少天記錄)。
 
如果儲存引擎向優化器提供的掃描行數資訊是不準確的資料,或者執行計劃本身太複雜以致3無法準確地獲取各個階段匹配的行數,那麼優化器會使用索引停機資訊來估算掃描行數。如果表沒有統計資訊,或者統計資訊不準確,又花錢就很有可能做出錯誤的決定。可以通過執行 analyze table 來重新生成統計資訊解決這個問題。
 
ANALYZE TABLE table_name;
 
減少索引和資料的碎片
 
B-Tree索引可能會碎片化,這會降低查詢效率。碎片化的索引可能會以很差或者無序的方式儲存在磁碟上。根據設計B-Tree需要隨機磁碟訪問才能定位到葉子頁。如果葉子頁在物理分佈上市順序且緊密的,那麼查詢的效能就會更好。
 
表的資料儲存也可能碎片化,分為3種類型:
     行碎片(Row fragmentation)
     這種碎片指的是資料行被儲存為多個地方的多個片段中。即使查詢只從索引中訪問一行記錄,行碎片也會導致效能下降。
     行間碎片(Intra-row fragmentation)
     行間碎片是指邏輯上順序的頁或者行 在磁碟上不是順序儲存的。行間碎片對注入全表掃描和聚簇索引掃描之類的操作有很大的影響,因為這些操作原本能夠從磁碟上順序儲存和資料中獲益。
     剩餘空間碎片(Free space fragment)
     剩餘空間碎片是指資料頁中有大量的空餘空間。這會導致伺服器讀取大量不需要的資料,送來造成浪費。
 
OPTIMIZE TABLE table_name;
 
5.6 總結
 
選擇索引和編寫利用這些索引的查詢的3原則:
 
  • 單行訪問是很慢的。特別是在機械硬碟儲存中(SSD固態硬碟的隨意IO要快很多,不過這一點仍然適用)。如果伺服器從儲存中讀取一個數據塊只是為了獲取其中一行,那麼就浪費了很多工作。最好讀取的塊中能包含儘可能多所需要的行。使用索引可以建立位置引用以提升效率。
 
  • 按順序訪問範圍資料是很快的,這有兩個原因。第一,順序IO不需要多次磁碟尋道,所以比隨機IO要快很多。第二,如果伺服器能夠按需要順序讀取資料,那麼就不再需要額外的排序操作,並且GROUP BY 查詢也無須再做排序和將行按組進行聚合計算了。
 
  • 索引覆蓋查詢是很快的。如果一個索引包含了查詢需要的所有列,那麼儲存引擎就不需要再回表查詢行。這避免了大量的單行訪問。
 
編寫查詢語句時應儘可能選擇合適的所有以避免單行查詢、儘可能地使用資料原聲順序從而避免額外的排序操作,儘可能使用索引覆蓋查詢。