菜鳥必知的 MySQL 知識(三)—— 索引優(yōu)化

后端工程師在開發(fā)過程中經(jīng)常需要和數(shù)據(jù)庫打交道,而如何建立高效的索引應(yīng)該是數(shù)據(jù)庫技能點的關(guān)鍵了。本篇介紹了索引的原理和索引優(yōu)化的策略,能夠幫助大家在建數(shù)據(jù)表索引的時候不再迷茫。

1 - 索引

索引(在MySQL中也叫做“鍵(key)”)是存儲引擎用于快速找到記錄的一種數(shù)據(jù)結(jié)構(gòu)。索引對于良好的性能非常關(guān)鍵。尤其是當(dāng)表中的數(shù)據(jù)量越來越大時,索引對性能的影響愈發(fā)重要。在數(shù)據(jù)量較小且負載較低時,不恰當(dāng)?shù)乃饕龑π阅艿挠绊懣赡苓€不明顯,但當(dāng)數(shù)據(jù)量逐漸增大時,性能則會急劇下降。

索引優(yōu)化應(yīng)該是對查詢性能優(yōu)化最有效的手段了。索引能夠輕易地將查詢性能提高幾個數(shù)量級,“最優(yōu)”的索引有時比一個“好的”索引性能要好兩個數(shù)量級。

2 - B-Tree 索引

2.1 基本介紹

索引有很多種類型,對于MySQL來說,使用不同的存儲引擎就意味著使用了不同類型的索引,因為MySQl中的索引是在存儲引擎層而不是服務(wù)器層實現(xiàn)的。大多數(shù) MySQL 引擎都支持 B-Tree 索引,如果沒有指明索引類型,一般默認即為 B-Tree 類型。也正是因為其應(yīng)用范圍最廣,本文就將B-Tree 索引作為例子,介紹索引的優(yōu)化。
 
 B-Tree 的特點是所有的值都是按順序存儲的,并且每一個葉子頁到根的距離相同。下圖展示了 B-Tree 索引的抽象表示,反映了 InnoDB 索引是如何工作的。

建立在B-Tree結(jié)構(gòu)上的索引(技術(shù)上來說B+Tree)

根節(jié)點存放了指向子節(jié)點的指針,存儲引擎根據(jù)這些指針向下層節(jié)點頁查找。如圖所示,通過key值的比較進行選擇路徑,經(jīng)過一層一層的節(jié)點頁,最終找到最底下的葉子頁,葉子頁存放著指向數(shù)據(jù)的指針。需要特別指出的是,改圖所用的結(jié)構(gòu)其實是B-Tree的變種B+Tree,關(guān)于B-TreeB+Tree的區(qū)別,大家可以參考從B樹、B+樹、B*樹談到R 樹這篇博客。
 
 B-Tree 索引能夠加快訪問數(shù)據(jù)的速度,因為存儲引擎不再需要進行全表掃描來獲取需要的數(shù)據(jù),取而代之的是從索引的根節(jié)點開始進行搜索。由于B-Tree 對索引列是順序組織存儲的,所以很適合查找范圍數(shù)據(jù)


2.2 查詢特點
CREATE TABLE People (
    last_name   varchar(50) not null,
    first_name  varchar(50) not null,
    birth       datetime    not null,
    key(last_name, first_name, birthday)
);

上面一段SQL主要是建立了一張名為People的表,將last_name, first_name, birthday作為一個聯(lián)合索引,下圖顯示了該索引是如何組織數(shù)據(jù)的存儲的。

People表中索引樹條目

B-Tree 索引適用于全鍵值、鍵值范圍或鍵前綴查找。讓我們用下面一段例子來解釋:

// 1 - 全鍵值
SELECT * FROM People WHERE last_name = Allen 
AND first_name = Cuba AND birth = '1990-01-01';

// 2- 匹配最左前綴
SELECT * FROM People WHERE last_name = Allen;

// 3 - 匹配列前綴
SELECT * FROM People WHERE last_name LIKE 'A%';

// 4 - 未使用索引
SELECT * FROM People WHERE first_name = Cuba;

// 5 - 使用了部分索引
SELECT * FROM People WHERE last_name = Allen
AND birth = '1990-01-01';

// 6 - 使用了部分索引
SELECT * FROM People WHERE last_name = Allen 
AND first_name LIKE 'C%' AND birth = '1990-01-01';
2.3 B-Tree 索引的限制
  • 如果不是按照索引的最左列開始查找,則無法使用索引。式(4)沒有從last_name開始查找,所以該條SQL會從全局遍歷查找first_name = Cuba的項,這就比從索引查找慢了許多。

  • 不能跳過索引中的列。如式(5),中間跳過了first_name項的匹配,所以這段SQL先是通過last_name這條索引將數(shù)據(jù)全部篩選出,然后遍歷取出birth條件項。

  • 如果查詢中有某個列的范圍查詢,則其右邊所有列都無法使用索引優(yōu)化查找。如式(6),這個查詢只能使用last_namefirst_name列的索引,因為這里LIKE是一個范圍條件,查詢出前兩列的結(jié)果后,再遍歷篩選出birth條件項。

3 - 高性能索引的策略

如何評價一個索引
索引將相關(guān)的記錄放到一起則獲得一星;如果索引中的數(shù)據(jù)順序和查找中的排列順序一致則獲得二星;如果索引中的列包含了查詢需要的全部列則獲得“三星”

3.1 獨立的列

“獨立的列”是指索引列不能是表達式的一部分,也不能是函數(shù)的參數(shù)。下面是一些使用不當(dāng)?shù)陌咐?/p>

// MySQL無法解析,不會使用索引
SELECT * FROM student WHERE id + 1 = 5;
// 同上
SELECT * FROM student WHERE updateTime - createTime <=  10;
3.2 前綴索引和索引選擇性

有時候需要索引很長的字符列,這會讓索引變得大且慢。通常可以索引開始的那部分字符,這樣可以大大節(jié)約索引空間,從而提高索引效率。但這樣也會降低索引的選擇性。
 
 索引的的選擇性是指,不重復(fù)的索引值(基數(shù))和數(shù)據(jù)表的記錄總數(shù)(#T)的比值,范圍從 1/#T 到 1 之間,索引的選擇性越高則查詢效率越高,因為選擇性高的索引可以讓MySQL在查找時過濾掉更多的行。唯一索引的選擇性是1,這是最好的索引選擇性,性能也是最好的。

訣竅在于要選擇足夠長的前綴以保證較高的選擇性,同時又不能太長(以便節(jié)約空間)。前綴應(yīng)該足夠長,以使得前綴索引的選擇性接近于索引整個列。

// 創(chuàng)建前綴索引實例
ALTER TABLE tbl_city ADD KEY (cityName(7));
3.3 多列索引

在多個列上分別建立獨立的單列索引在大多情況下并不能提高MySQL的查詢性能。MySQL5.0*之后引入了一種叫“索引合并”(index merge)的策略,一定程度上可以使用表上的多個單列索引來定位指定的行。

例如表film_actor在字段film_id和actor_id上各有一個單列索引,當(dāng)執(zhí)行下面一條SQL:

SELECT film_id, actor_id FROM film_actor WHERE actor_id = 1 OR film_id = 1;

老的MySQL版本中,會對這個查詢使用全表掃描,除非改寫成如下的兩個查詢 UNION 的方式:

SELECT film_id, actor_id FROM film_actor WHERE actor_id = 1 
UNION ALL 
SELECT film_id, actor_id FROM film_actor WHERE  film_id = 1 OR actor_id <> 1;

MySQL5.0 后的版本中,在查詢能夠同時使用這兩個單列索引進行掃描,并將結(jié)果進行合并。這種算法有三個變種:OR條件的聯(lián)合(union),AND條件的相交(intersection),組合前兩種情況的聯(lián)合及相交

不過值得注意的是,索引合并策略有時候是一種優(yōu)化的結(jié)果,但實際上更多時候說明了表上的索引建的很糟糕。

  • 當(dāng)出現(xiàn)服務(wù)器對多個索引做相交操作(通常有多個AND條件),通常意味著需要一個包含所有相關(guān)列的多列索引,而不是多個獨立的單列索引。

  • 當(dāng)服務(wù)器需要對多個索引做聯(lián)合操作是(通常有多個OR條件),通常需要耗費大量的CPU和內(nèi)存資源在算法的緩存、排序和合并操作上。特別是當(dāng)其中有些索引的選擇性不高,需要合并掃描返回大量的數(shù)據(jù)的時候。

  • 更重要的是,優(yōu)化器不會把這些計算到“查詢成本”中,優(yōu)化器只關(guān)心隨機頁面讀取。這會使得查詢的成本被“低估”,導(dǎo)致該執(zhí)行計劃還不如直接走全表掃描。

3.4 選擇合適的索引列順序

在一個多列B-Tree 索引中,索引列的順序意味著索引首先按照最左列進行排序,其次是第二列,等等。所以,索引可以按照升序或者降序進行掃描,以滿足精確符合順序的ORDER BY、GROUP BY和DISTINCT等子句的查詢需求。

在不考慮排序和分組的情況下,將選擇性最高的列放在前面通常是很好的。這時候索引的作用只是用于優(yōu)化 WHERE 條件的查找。然而,性能不只是依賴于所有索引列的選擇性,也和查詢條件的具體值有關(guān),也就是和值得分布有關(guān)。

3.5 聚簇索引

當(dāng)表有聚簇索引時,它的數(shù)據(jù)行實際上存放在索引的葉子頁。術(shù)語“聚簇”表示數(shù)據(jù)行和相鄰的鍵值緊湊地存儲在一起。因為無法同時把數(shù)據(jù)行存放在兩個不同的地方,所以一個表只能有一個聚簇索引。

3.6 覆蓋索引

如果一個索引包含所有需要查詢的字段的值,我們就稱之為“覆蓋索引”。覆蓋索引是非常有用的工具,能夠極大地提高性能,其好處有:

  • 索引條目通常遠小于數(shù)據(jù)行大小,所以如果只需要讀取索引,那MySQL就會極大地減少數(shù)據(jù)訪問量。
  • 因為索引是按照列值順序存儲的,所以對于I/O密集型的范圍查詢會比隨機從磁盤讀取每一行數(shù)據(jù)的I/O要少得少。
  • 對于MyISAM存儲引擎,內(nèi)存中只緩存索引,數(shù)據(jù)則依賴于操作系統(tǒng)來緩存,因為要訪問數(shù)據(jù)需要一次系統(tǒng)調(diào)用
  • 對于InnoDB的聚簇索引,覆蓋索引對InnoDB表特別有用。InnoDB的二級索引在葉子節(jié)點中保存了行的主鍵值,所以如果二級主鍵能夠覆蓋查詢,則可以避免對主鍵索引的二次查詢。
3.7 使用索引掃描來做排序

MySQL 有兩種方式可以生成有序的結(jié)果:排序操作和按索引順序掃描。如果EXPLAIN出來的type列的值為“index”,則說明MySQL 使用了索引掃描來做排序。MySQL 可以使用同一個索引既滿足排序,又用于查找行。因此,如果可能,設(shè)計索引時應(yīng)該盡可能地同時滿足這兩種任務(wù),這樣是最好的。

3.8 冗余和重復(fù)索引

MySQL 允許在相同列上創(chuàng)建多個索引,無論是有意的還是無意的。MySQL 需要單獨維護重復(fù)的索引,并且優(yōu)化器在優(yōu)化查詢的時候也需要逐個地進行考慮,這會影響性能。


大家好,我是彬彬醬,目前在騰訊從事Web后端開發(fā)。
菜鳥必知的 MySQL 知識專題整理了關(guān)于 MySQL 的基礎(chǔ)知識,適合大家進行入門級學(xué)習(xí),這個專題現(xiàn)包含下列文章:
菜鳥必知的 MySQL 知識(一)—— 基礎(chǔ)知識
菜鳥必知的 MySQL 知識(二)—— 數(shù)據(jù)類型優(yōu)化
菜鳥必知的 MySQL 知識(三)—— 索引優(yōu)化


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容