1. 程式人生 > 實用技巧 >萬字詳解 阿里面試真題:請你說說索引的原理

萬字詳解 阿里面試真題:請你說說索引的原理

前言

相信每個IT界大佬,簡歷上少不了Mysql索引這個關鍵字,但如果被問起來,你能說出多少乾貨呢?先看下面幾個問題測試一下吧:

  • 索引是怎麼提高查詢效率的?可以為了提高查詢效率增加索引麼?
  • mysql索引系統採用的資料結構是什麼?
  • 為什麼要使用B+樹?
  • 聚集索引相對於非聚集索引的區別?
  • 什麼是回表?
  • 什麼是索引覆蓋?
  • 什麼是最左匹配原則?
  • 索引失效場景有哪些,如何避免?

這些問題說不明白?不要慌!請帶著問題向下看。

1 索引原理探究

什麼是資料庫索引?先來個官方一些的定義吧。

在關係資料庫中,索引是一種單獨的、物理的數對資料庫表中一列或多列的值進行排序的一種儲存結構,它是某個表中一列或若干列值的集合和相應的指向表中物理標識這些值的資料頁的邏輯指標清單。

這段話有點繞,其實把索引理解為圖書目錄,就非常好理解了。

如果我們想在圖書中查詢特定內容,在沒有目錄的情況下只能逐頁翻找。與此類似,當執行下面這樣一條SQL語句時,假如沒有索引,資料庫如何查詢到相對應的記錄呢?

SELECT*FROMstudentWHEREname='葉良辰'

搜尋引擎只能掃描整個表的每一行,並依次對比判斷name的值是否等於“葉良辰”。我們知道,單純的記憶體運算是很快的,但從磁碟中取資料到記憶體中是相對慢的,當表中有大量資料時,記憶體與磁碟互動次數大大增加,這就導致了查詢效率低下。

1.1 B樹與B+樹

相對於cpu和記憶體操作,磁碟IO開銷很大,非常容易成為系統的效能瓶頸,因此計算機作業系統做了一些優化:

當一次IO時,將相鄰的資料也都讀取到記憶體緩衝區內,而不是僅僅讀取當前磁碟地址的資料。因為區域性預讀性原理告訴我們,當計算機訪問一個地址的資料的時候,與其相鄰的資料也會很快被訪問到。每一次IO讀取的資料我們稱之為一頁(page)。具體一頁有多大資料跟作業系統有關,一般為4k或8k,也就是我們讀取一頁內的資料時候,實際上才發生了一次IO,這個理論對於索引的資料結構設計非常有幫助。

為什麼索引能提升資料庫查詢效率呢?根本原因就在於索引減少了查詢過程中的IO次數。那麼它是如何做到的呢?使用B+樹。下面先簡單瞭解一下B樹和B+樹。

B樹,即平衡多路查詢樹(B-Tree),是為磁碟等外儲存裝置設計的一種平衡查詢樹。

B樹簡略示意圖:

觀察上圖可見B樹的兩個特點:

  1. 樹內的每個節點都儲存資料
  2. 葉子節點之間無指標連線

B+樹簡略示意圖:

再看B+樹相對於B樹的兩個特點:

  1. 資料只出現在葉子節點
  2. 所有葉子節點增加了一個鏈指標

葉子結點是離散數學中的概念。一棵樹當中沒有子結點(即度為0)的結點稱為葉子結點,簡稱“葉子”。葉子是指出度為0的結點,又稱為終端結點。

但是,為什麼是B+樹而不是B樹呢?原因有兩點:

  1. B樹每個節點中不僅包含資料的key值,還有data值。而每一個頁的儲存空間是有限的,如果data資料較大時將會導致每個節點能儲存的key的數量很小,要儲存同樣多的key,就需要增加樹的高度。樹的高度每增加一層,查詢時的磁碟I/O次數就增加一次,進而影響查詢效率。而在B+Tree中,所有資料記錄節點都是按照鍵值大小順序存放在同一層的葉子節點上,而非葉子節點上只儲存key值資訊,這樣可以大大加大每個節點儲存的key值數量,降低B+樹的高度。
  2. B+樹的葉子節點上有指標進行相連,因此在做資料遍歷的時候,只需要對葉子節點進行遍歷即可,這個特性使得B+樹非常適合做範圍查詢。

1.2 聚簇索引與非聚簇索引

首先,為了方便理解,我們先了解一下聚集索引(clustered index)和非聚集索引(secondary index,也稱輔助索引或普通索引)。這兩種索引是按儲存方式進行區分的。

聚集索引(clustered)也稱聚簇索引,這種索引中,資料庫錶行中資料的物理順序與鍵值的邏輯(索引)順序相同。一個表的物理順序只有一種情況,因此對應的聚集索引只能有一個。如果某索引不是聚集索引,則表中的行物理順序與索引順序不匹配,與非聚集索引相比,聚集索引有著更快的檢索速度。

如果不好理解,請看下面這個表:

表中id和實體地址是保持一致順序的,id較大的行,其實體地址也比較靠後。因為聚集索引的特性,它的建立有一定的特殊要求:

  1. 在Innodb中,聚簇索引預設就是主鍵索引。
  2. 如果表中沒有定義主鍵,那麼該表的第一個唯一非空索引被作為聚集索引。
  3. 如果沒有主鍵也沒有合適的唯一索引,那麼innodb內部會生成一個隱藏的主鍵作為聚集索引,這個隱藏的主鍵是一個6個位元組的列,改列的值會隨著資料的插入自增。

大家還記得,自增主鍵和uuid作為主鍵的區別麼?由於主鍵使用了聚集索引,如果主鍵是自增id,那麼對應的資料一定也是相鄰地存放在磁碟上的,寫入效能比較高。如果是uuid的形式,頻繁的插入會使innodb頻繁地移動磁碟塊,寫入效能就比較低了。

1.3 索引原理圖示

下面用一個通過主鍵索引查詢資料的案例演示一下索引的原理。假如有student表如下,id上建立了聚集索引,name上建立非聚集索引:

idnamescore2葉良辰784龍傲天8810趙日天5611徐勝虎77

1.3.1 聚簇索引

當我們執行下面的語句時,

SELECTnameFROMstudentWHEREid=2

查詢過程如下圖所示:

用語言描述一下,是這樣的:

  1. 先找到根節點所在磁碟塊,讀入記憶體。(第1次磁碟I/O操作)
  2. 在記憶體中判斷id=3所在區間(0,8),找到該區間對應的指標1(第1次記憶體查詢)
  3. 根據指標1記錄的磁碟地址,找到磁碟塊2並讀入記憶體(第2次磁碟I/O操作)
  4. 在記憶體中判斷id=3所在區間(0,4),找到該區間對應的指標2(第2次記憶體查詢)
  5. 根據指標2記錄的磁碟地址,找到磁碟塊4並讀入記憶體(第3次磁碟I/O操作)
  6. 在記憶體中查詢到id=2對應的資料行記錄(第3次記憶體查詢)

我們知道,磁碟I/O相對於記憶體運算(尤其記憶體中的主鍵是有序排列的,利用二分查詢等演算法效率非常高)耗時高得多,因此在資料庫查詢中,減少磁碟訪問時資料庫的效能優化的主要手段。

而分析上面過程,發現整個查詢只需要3次磁碟I/O操作(其實InnoDB引擎是將根節點常駐記憶體的,第1次磁碟I/O操作並不存在)和3次記憶體查詢操作。相對於不使用索引的遍歷式查詢,大大減少了對磁碟的訪問,因此查詢效率大幅提高。但是,因為索引樹要與表中資料保持一致,因此當表發生資料增刪改時,索引樹也要相應修改,導致寫資料比沒有索引時開銷大一些。

1.3.2 非聚簇索引

好,聚集索引看完後,再看非聚集索引。

如上圖,多加一個索引,就會多生成一顆非聚簇索引樹。因此,索引不能隨意增加。在做寫庫操作的時候,需要同時維護這幾顆樹的變化,導致效率降低!

另外,仔細觀察的人一定會發現,不同於聚集索引,非聚集索引葉子節點上不再是真實資料,而是儲存了索引欄位自身值和主鍵索引。因此,當我們執行以下SQL語句時:

SELECTid,nameFROMstudentWHEREname='葉良辰';

整個查詢過程與聚集索引的過程一樣,只需要掃描一次索引樹(n次磁碟I/O和記憶體查詢),即可拿到想要的資料。

但是,如果查詢name索引樹沒有的資料時,情況就不一樣了:

SELECTscoreFROMstudentWHEREname='葉良辰';

注意看上圖中的紅色箭頭,因為掃描完name索引後,Mysql只能獲取到對應的id和name,然後用id的值再去聚集索引中去查詢score的值。這個過程相對於聚集索引查詢的效率下降,可以理解了吧。

這就是通常所說的回表或者二次查詢:使用聚集索引查詢可以直接定位到記錄,而普通索引通常需要掃描兩遍索引樹,即先通過普通索引定位到主鍵值,在通過聚集索引定位到行記錄,這就是所謂的回表查詢,它的效能比掃描一遍索引樹低。

既然普通索引會導致回表二次查詢,那麼有什麼辦法可以應對呢?建立聯合索引!

1.3.3 聯合索引

所謂聯合索引,也稱多列所謂,就是建立在多個欄位上的索引,這個概念是跟單列索引相對的。聯合索引依然是B+樹,但聯合索引的健值數量不是一個,而是多個。構建一顆B+樹只能根據一個值來構建,因此資料庫依據聯合索引最左的欄位來構建B+樹。

例如在a和b欄位上建立聯合索引,索引結構將如下圖所示:

一目瞭然,當我們再執行SELECT score FROM student WHERE name='葉良辰';時,可以直接通過掃描非聚集索引直接獲取score的值,而不再需要到聚集索引上二次掃描了。

最左字首匹配

聯合索引中有一個重要的課題,就是最左字首匹配。

最左字首匹配原則:在MySQL建立聯合索引時會遵守最左字首匹配原則,即最左優先,在檢索資料時從聯合索引的最左邊開始匹配。

這是為什麼呢?我們再仔細觀察索引結構,可以看到索引key在排序上,首先按a排序,a相等的節點中,再按b排序。因此,如果查詢條件是a或a和b聯查時,是可以應用到索引的。如果查詢條件是單獨使用b,因為無法確定a的值,因此無法使用索引。

假如在table表的a,b,c三個列上建立聯合索引,簡要分類分析下聯合索引的最左字首匹配。

首先看等值查詢:

1、全值匹配查詢時(where子句搜尋條件順序調換不影響索引使用,因為查詢優化器會自動優化查詢順序 ),可以用到聯合索引

SELECT*FROMtableWHEREa=1ANDb=3ANDc=2
SELECT*FROMtableWHEREb=3ANDc=4ANDa=2

2、匹配左邊的列時,可以用到聯合索引

SELECT*FROMtableWHEREa=1
SELECT*FROMtableWHEREa=1ANDb=3

3、未從最左列開始時,無法用到聯合索引

SELECT*FROMtableWHEREb=1ANDb=3

4、查詢列不連續時,無法使用聯合索引(會用到a列索引,但c排序依賴於b,所以會先通過a列的索引篩選出a=1的記錄,再在這些記錄中遍歷篩選c=3的值,是一種不完全使用索引的情況)

SELECT*FROMtableWHEREa=1ANDc=3

再看範圍查詢:

1、範圍查詢最左列,可以使用聯合索引

SELECT*FROMtableWHEREa>1ANDa<5;

2、精確匹配最左列並範圍匹配其右一列(a值確定時,b是有序的,因此可以使用聯合索引)

SELECT*FROMtableWHEREa=1ANDb>3;

3、精確匹配最左列並範圍匹配非右一列(a值確定時,c排序依賴b,因此無法使用聯合索引,但會使用a列索引篩選出a>2的記錄行,再在這些行中條件 c >3逐條過濾)

SELECT*FROMtableWHEREa>2ANDc>5;

索引的原理探究到此結束,這部分內容堪稱最難啃的骨頭。不過,能堅持讀下來的朋友,你的收穫也一定良多。接下來的內容就輕鬆愉悅多了。

2 索引的正確使用姿勢

索引的優點如下:

  • 通過建立唯一索引可以保證資料庫表中每一行資料的唯一性。
  • 可以大大加快資料的查詢速度,這是使用索引最主要的原因。
  • 在實現資料的參考完整性方面可以加速表與表之間的連線。
  • 在使用分組和排序子句進行資料查詢時也可以顯著減少查詢中分組和排序的時間。

既然索引這麼好,那麼我們是不是盡情使用索引呢?非也,索引優點明顯,但相對應,也有缺點:

  • 建立和維護索引組要耗費時間,並且隨著資料量的增加所耗費的時間也會增加。
  • 索引需要佔磁碟空間,除了資料表佔資料空間以外,每一個索引還要佔一定的物理空間。
  • 當對錶中的資料進行增加、刪除和修改的時候,索引也要動態維護,這樣就降低了資料的維護速度。

因此,使用索引時要兼顧索引的優缺點,尋找一個最有利的平衡點。

2.1 索引的型別區分

以InnoDB引擎為例,Mysql索引可以做如下區分。

首先,索引可以分為聚集索引和非聚集索引,它們的區別和含義在前文有大幅介紹,此處不再贅述。

其次,從邏輯上,索引可以區分為:

  • 普通索引:普通索引是 MySQL 中最基本的索引型別,它沒有任何限制,唯一任務就是加快系統對資料的訪問速度。普通索引允許在定義索引的列中插入重複值和空值。
  • 唯一索引:唯一索引與普通索引類似,不同的是建立唯一性索引的目的不是為了提高訪問速度,而是為了避免資料出現重複。唯一索引列的值必須唯一,允許有空值。如果是組合索引,則列值的組合必須唯一。建立唯一索引通常使用UNIQUE關鍵字。例如在student表中的id欄位上建立名為index_id的索引CREATE UNIQUE INDEX index_id ON tb_student(id);
  • 主鍵索引:主鍵索引就是專門為主鍵欄位建立的索引,也屬於索引的一種。主鍵索引是一種特殊的唯一索引,不允許值重複或者值為空。建立主鍵索引通常使用PRIMARY KEY關鍵字。不能使用CREATE INDEX語句建立主鍵索引。
  • 空間索引:空間索引是對空間資料型別的欄位建立的索引,空間索引主要用於地理空間資料型別 ,很少用到。
  • 全文索引:全文索引主要用來查詢文字中的關鍵字,只能在CHAR、VARCHAR 或 TEXT型別的列上建立。在MySQL中只有MyISAM儲存引擎支援全文索引。全文索引允許在索引列中插入重複值和空值。

索引在實際使用上分為單列索引和多列索引。

單列索引:單列索引就是索引只包含原表的一個列。在表中的單個欄位上建立索引,單列索引只根據該欄位進行索引。

例如在student表中的address欄位上建立名為index_addr的單列索引,address欄位的資料型別為VARCHAR(20),索引的資料型別為CHAR(4)。SQL 語句如下:

CREATEINDEXindex_addrONstudent(address(4));

這樣,查詢時可以只查詢 address 欄位的前 4 個字元,而不需要全部查詢。

**多列索引也稱為複合索引或組合索引。**相對於單列索引來說,組合索引是將原表的多個列共同組成一個索引。

多列索引是在表的多個欄位上建立一個索引。該索引指向建立時對應的多個欄位,可以通過這幾個欄位進行查詢。但是,只有查詢條件中使用了這些欄位中第一個欄位時,索引才會被使用。

下面在 student 表中的 name 和 address 欄位上建立名為 index_na 的索引,SQL 語句如下:

CREATEINDEXindex_naONtb_student(name,address);

該索引建立好了以後,查詢條件中必須有 name 欄位才能使用索引。

一個表可以有多個單列索引,但這些索引不是組合索引。一個組合索引實質上為表的查詢提供了多個索引,以此來加快查詢速度。比如,在一個表中建立了一個組合索引(c1,c2,c3),在實際查詢中,系統用來實際加速的索引有三個:單個索引(c1)、雙列索引(c1,c2)和多列索引(c1,c2,c3)。

2.2 索引的檢視

檢視索引的語法格式如下:

SHOWINDEXFROM<表名>

查詢結果說明如下:

2.3 索引的建立

建立索引有3種方式:

1、CREATE INDEX直接建立:

可以使用專門用於建立索引的 CREATE INDEX 語句在一個已有的表上建立索引,但該語句不能建立主鍵。

CREATE<索引名>ON<表名>(<列名>[<長度>][ASC|DESC])

語法說明如下:

  • <索引名>:指定索引名。一個表可以建立多個索引,但每個索引在該表中的名稱是唯一的。
  • <表名>:指定要建立索引的表名。
  • <列名>:指定要建立索引的列名。通常可以考慮將查詢語句中在 JOIN 子句和 WHERE 子句裡經常出現的列作為索引列。
  • <長度>:可選項。指定使用列前的 length 個字元來建立索引。使用列的一部分建立索引有利於減小索引檔案的大小,節省索引列所佔的空間。在某些情況下,只能對列的字首進行索引。索引列的長度有一個最大上限 255 個位元組(MyISAM 和 InnoDB 表的最大上限為 1000 個位元組),如果索引列的長度超過了這個上限,就只能用列的字首進行索引。另外,BLOB 或 TEXT 型別的列也必須使用字首索引。
  • ASC|DESC:可選項。ASC指定索引按照升序來排列,DESC指定索引按照降序來排列,預設為ASC。

例如,在student表name欄位上建立索引:

  • 普通索引:CREATE INDEX index_name ON student (name)
  • 唯一索引:CREATE UNIQUE index_name ON student (name)

建立普通索引使用的關鍵字,例如在student表name欄位上建立一個普通索引index_name

  • 建表建立:CREATE TABLE student(id INT NOT NULL,name CHAR(45) DEFAULT NULL,INDEX(name));
  • ALTER TABLE:ALTER student ADD INDEX index_name (name)

2、CREATE TABLE時建立

索引也可以在建立表(CREATE TABLE)的同時建立。在 CREATE TABLE 語句中新增以下語句。例如建立student表時在name欄位新增索引:

  • 主鍵索引:CREATE TABLE student(name CHAR(45) PRIMARY KEY);
  • 唯一索引:CREATE TABLE student(id INT NOT NULL,name CHAR(45) DEFAULT NULL,UNIQUE INDEX(name));
  • 普通索引:CREATE TABLE student(id INT NOT NULL,name CHAR(45) DEFAULT NULL,INDEX(name));

3、ALTER TABLE時建立

ALTER TABLE 語句也可以在一個已有的表上建立索引。例如在student表name欄位上建立一個普通索引index_name:

  • 主鍵索引:ALTER TABLE student ADD PRIMARY KEY (name);
  • 唯一索引:ALTER TABLE student ADD UNIQUE INDEX index_name(name);
  • 普通索引:ALTER TABLE student ADD INDEX index_name(name);

2.4 索引失效場景

建立了索引並不意味著高枕無憂,在很多場景下,索引會失效。下面列舉了一些導致索引失效的情形,是我們寫SQL語句時應儘量避免的。

1、條件欄位原因

  • 單欄位有索引,WHERE條件使用多欄位(含帶索引的欄位),例如SELECT * FROM student WHERE name ='張三' AND addr = '北京市'語句,如果name有索引而addr沒索引,那麼SQL語句不會使用索引。
  • 多欄位索引,違反最佳左字首原則。例如,student表如果建立了(name,addr,age)這樣的索引,WHERE後的第一個查詢條件一定要是name,索引才會生效。

2、<>、NOT、in、not exists

當查詢條件為等值或範圍查詢時,索引可以根據查詢條件去找對應的條目。否則,索引定位困難(結合我們查字典的例子去理解),執行計劃此時可能更傾向於全表掃描,這類的查詢條件有:<>、NOT、in、not exists

3、查詢條件中使用OR

如果條件中有or,即使其中有條件帶索引也不會使用(因此SQL語句中要儘量避免使用OR)。要想使用OR,又想讓索引生效,只能將OR條件中的每個列都加上索引。

4、查詢條件使用LIKE萬用字元

SQL語句中,使用後置萬用字元會走索引,例如查詢姓張的學生(SELECT * FROM student WHERE name LIKE '張%'),而前置萬用字元(SELECT * FROM student WHERE name LIKE '%東')會導致索引失效而進行全表掃描。

5、索引列上做操作(計算,函式,(自動或者手動)型別裝換

有以下幾種例子:

  • 在索引列上使用函式:例如select * from student where upper(name)='ZHANGFEI';會導致索引失效,而select * from student where name=upper('ZHANGFEI');是會使用索引的。
  • 在索引列上計算:例如select * from student where age-1=17;

6、在索引列上使用mysql的內建函式,索引失效

例如,SELECT * FROM student WHERE create_time

7、索引列資料型別不匹配

例如,如果age欄位有索引且型別為字串(一般不會這麼定義,此處只是舉例)但條件值為非字串,索引失效,例如SELECT * FROM student WHERE age=18會導致索引失效。

8、索引列使用IS NOT NULL或者IS NULL可能會導致無法使用索引

B-tree索引IS NULL不會使用索引,IS NOT NULL會使用,點陣圖索引IS NULL、IS NOT NULL都會使用索引。

最後,對索引的使用做一個總結吧:

  1. 索引有利於查詢,但不能隨意加索引,因為索引不僅會佔空間,而且需要在寫庫時進行維護。
  2. 如果多個欄位常常需要一起查詢,那麼在這幾個欄位上建立聯合索引是個好辦法,同時注意最左匹配原則。
  3. 不要在重複度很高的欄位上加索引,例如性別。
  4. 避免查詢語句導致索引失效

推薦閱讀

為什麼阿里巴巴的程式設計師成長速度這麼快,看完他們的內部資料我懂了

從事開發一年的程式設計師能拿到多少錢?

位元組跳動總結的設計模式 PDF 火了,完整版開放下載

刷Github時發現了一本阿里大神的演算法筆記!標星70.5K

程式設計師50W年薪的知識體系與成長路線。

關於【暴力遞迴演算法】你所不知道的思路

開闢鴻蒙,誰做系統,聊聊華為微核心

看完三件事❤️

如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:

點贊,轉發,有你們的 『點贊和評論』,才是我創造的動力。

關注公眾號 『 Java鬥帝 』,不定期分享原創知識。

同時可以期待後續文章ing