騰訊 WXG 後臺開發工程師對 MySQL 索引知識點總結

知其然知其因此然!本文介紹索引的數據結構、查找算法、常見的索引概念和索引失效場景。java

什麼是索引?

在關係數據庫中,索引是一種單獨的、物理的對數據庫表中一列或多列的值進行排序的一種存儲結構,它是某個表中一列或若干列值的集合和相應的指向表中物理標識這些值的數據頁的邏輯指針清單。索引的做用至關於圖書的目錄,能夠根據目錄中的頁碼快速找到所需的內容。(百度百科)mysql

索引的目的是提升查找效率,對數據表的值集合進行了排序,並按照必定數據結構進行了存儲。程序員

本文將從一個案例開始,從索引的數據結構、分類、關鍵概念及如何使用索引提升查找效率等方面對索引知識進行總結。算法

從一個案例開始

現象

業務中有個既存的歷史 SQL 語句在運行時會致使 DB 服務器過載,進而致使相關服務阻塞沒法及時完成。CPU 監控曲線以下:sql

從 DB 的 CPU 使用率曲線能夠看到業務運行一直處於「亞健康」狀態(1),隨着業務的增加隨時均可能出現問題。這種問題(2)在 11 月 11 日凌晨出現,當時 DB CPU 一直處於 100%高負荷狀態,且存在大量的慢查詢語句。最終以殺死進程下降 DB 負載、減小業務進程(3)的方式恢復業務。數據庫

在 11 月 11 日下午,對該業務的 SQL 語句進行了優化,優化的效果以下。業務運行時的 CPU 使用率峯值有很大的下降(對比圖 2 的 1,2,3 可見);慢查詢語句幾乎在監控曲線上也沒法明顯觀察到(對比圖 3 的 1,2,3 可見)。設計模式

分析

表結構數組

CREATE TABLE T_MchStat (FStatDate int unsigned NOT NULL DEFAULT 19700101 COMMENT '統計日期',
FMerchantId bigint unsigned NOT NULL DEFAULT 0 COMMENT '商戶ID',
FVersion int unsigned NOT NULL DEFAULT 0 COMMENT '數據版本號',
FBatch bigint unsigned NOT NULL DEFAULT 0 COMMENT '統計批次',
FTradeAmount bigint NOT NULL DEFAULT 0 COMMENT '交易金額'
PRIMARY KEY (FStatDate,FMerchantId,FVersion),
INDEX i_FStatDate_FVersion (FStatDate,FVersion))
DEFAULT CHARSET = utf8 ENGINE = InnoDB;緩存

從建表語句能夠知道該表有兩個索引:服務器

  1. 主鍵索引,是一個組合索引,由字段 FStateDate、FMerchantId 和 FVersion 組成;
  2. 普通索引,是一個組合索引,由字段 FStateDate 和 FVersion 組成;

優化前的 SQL 語句(作了部分裁剪)A:

SELECT SQL_CALC_FOUND_ROWS FStatDate,

FMerchantId,
FVersion,
FBatch,
FTradeAmount,
FTradeCount

FROM T_MchStat_1020
WHERE FStatDate = 20201020

AND FVersion = 0
AND FMerchantId > 0

ORDER BY FMerchantId ASC LIMIT 0, 8000

對該 SQL 進行 explain 獲得以下結果,Extra 字段的值爲 using where,說明並無使用到索引。

優化後的 SQL 語句(作了部分裁剪)B:

SELECT SQL_CALC_FOUND_ROWS a1.FStatDate,

a1.FMerchantId,
a1.FVersion,
FBatch,
FTradeAmount,
FTradeCount

FROM T_MchStat_1020 a1, (

SELECT FStatDate, FMerchantId, FVersion
FROM T_Mch******Stat_1020
WHERE FStatDate = 20201020
    AND FVersion = 0
    AND FMerchantId > 0
    ORDER BY FMerchantId ASC LIMIT 0, 8000 ) a2

where a1.FStatDate = a2.FStatDate

and a1.FVersion = a2.FVersion
and a1.FMerchantId = a2.FMerchantId;

優化關鍵步驟爲:

  • 新增一個子查詢,select 字段只有主鍵字段;

該 SQL 的 explain 結果以下,子查詢語句使用了索引,而最終在線上運行結果也證實了優化效果顯著。

疑問

優化後的 SQL 語句 B 比原來的 SQL 語句 A 複雜的多(子查詢,臨時表關聯等),怎麼效率會提高,違反直覺?有三個疑問:

  1. SQL 語句 A 的查詢條件字段都在主鍵中,主鍵索引用到了沒?
  2. SQL 語句 B 的子查詢爲何可以用到索引?
  3. 先後兩條語句執行流程的差別是什麼?

索引的數據結構

在 MySQL 中,索引是在存儲引擎層實現的,而不一樣的存儲引擎根據其業務場景特色會有不一樣的實現方式。這裏會先介紹咱們常見的有序數組、Hash 和搜索樹,最後看下 Innodb 的引擎支持的 B+樹。

有序數組

數組是在任何一本數據結構和算法的書籍都會介紹到的一種重要的數據結構。有序數組如其字面意思,以 Key 的遞增順序保存數據在數組中。很是適合等值查詢和範圍查詢。

ID:1

ID:2

......

ID:N

在 ID 值沒有重複的狀況下,上述數組按照 ID 的遞增順序進行保存。這個時候若是須要查詢特定 ID 值的 name,用二分法就能夠快速獲得,時間複雜度是 O(logn)。

// 二分查找遞歸實現方式
int binary_search(const int arr[], int start, int end, int key)
{

if (start > end)
    return -1;

int mid = start + (end - start) / 2;
if (arr[mid] > key)
    return binary_search(arr, start, mid - 1, key);
else if (arr[mid] < key)
    return binary_search(arr, mid + 1, end, key);
else
    return mid;

}

有序數組的優勢很明顯,一樣其缺點也很明顯。其只適合靜態數據,如遇到有數據新增插入,則就會須要數據移動(新申請空間、拷貝數據和釋放空間等動做),這將很是消耗資源。

Hash

哈希表是一種以鍵-值(K-V)存儲數據的結構,咱們只須要輸入鍵 K,就能夠找到對應的值 V。哈希的思路是用特定的哈希函數將 K 換算到數組中的位置,而後將值 V 放到數組的這個位置。若是遇到不一樣的 K 計算出相同的位置,則在這個位置拉出一個鏈表依次存放。哈希表適用於等值查詢的場景,對應範圍查詢則無能爲力。

二叉搜索樹

二叉搜索樹,也稱爲二叉查找樹、有序二叉樹或排序二叉樹,是指一顆空樹或者具備如下性質的二叉樹:

  1. 若任意節點的左子樹不空,則左子樹上全部節點的值均小於它的根節點的值;
  2. 若任意節點的右子樹不空,則右子樹上全部節點的值均大於或等於它的根節點的值;
  3. 任意節點的左、右子樹也分別爲二叉查找樹;

二叉搜索樹相比於其它數據結構的優點在於查找、插入的時間複雜度較低,爲 O(logn)。爲了維持 O(logn)的查詢複雜度,須要保持這棵樹是平衡二叉樹。

二叉搜索樹的查找算法:

  1. 若 b 是空樹,則搜索失敗,不然:
  2. 若 x 等於 b 的根節點的值,則查找成功;不然:
  3. 若 x 小於 b 的根節點的值,則搜索左子樹;不然:
  4. 查找右子樹。

相對於有序數組和 Hash,二叉搜索樹在查找和插入兩端的表現都很是不錯。後續基於此不斷的優化,發展出 N 叉樹等。

B+樹

Innodb 存儲引擎支持 B+樹索引、全文索引和哈希索引。其中 Innodb 存儲引擎支持的哈希索引是自適應的,Innodb 存儲引擎會根據表的使用狀況自動爲表生成哈希索引,不能人爲干預。B+樹索引是關係型數據庫中最多見的一種索引,也將是本文的主角。

數據結構

在前文簡單介紹了有序數組和二叉搜索樹,對二分查找法和二叉樹有了基本瞭解。B+樹的定義相對複雜,在理解索引工做機制上無須深刻、只需理解數據組織形式和查找算法便可。咱們能夠簡單的認爲 B+樹是一種 N 叉樹和有序數組的結合體。

例如:

B+樹的 3 個優勢:

  1. 層級更低,IO 次數更少
  2. 每次都須要查詢到葉子節點,查詢性能穩定
  3. 葉子節點造成有序鏈表,範圍查詢方便

操做算法

  • 查找

由根節點自頂向下遍歷樹,根據分離值在要查找的一邊的指針;在節點內使用二分查找來肯定位置。

  • 插入

Leaf Page(葉子)滿

Index Page(索引)滿

操做

  • 刪除

葉子節點小於填充因子

中間節點小於填充因子

操做

注:插入和刪除兩個表格內容來自《MySQL 技術內幕-InnoDB 存儲引擎》

填充因子(innodb_fill_factor):索引構建期間填充的每一個 B-tree 頁面上的空間百分比,其他空間保留給將來索引增加。從插入和刪除操做中能夠看到填充因子的值會影響到數據頁的 split 和 merge 的頻率。將值設置小些,能夠減小 split 和 merge 的頻率,可是索引相對會佔用更多的磁盤空間;反之,則會增長 split 和 merge 的頻率,可是能夠減小佔用磁盤空間。Innodb 對於彙集索引默認會預留 1/16 的空間保證後續的插入和升級索引。

Innodb B+樹索引

前文介紹了索引的基本數據結構,如今開始咱們從 Innodb 的角度瞭解如何使用 B+樹構建索引,索引如何工做和如何使用索引提高查找效率。

彙集索引和非彙集索引

數據庫中的 B+樹索引能夠分爲彙集索引和非彙集索引。彙集索引和非彙集索引的不一樣點在於葉子節點是不是完整行數據。

Innodb 存儲引擎表是索引組織表,即表中的數據按照主鍵順序存放。彙集索引就是按照每張表的主鍵構造一棵 B+樹,葉子節點存放的是表的完整行記錄。非彙集索引的葉子節點不包含行記錄的所有數據。Innodb 存儲引擎的非彙集索引的葉子節點的內容爲主鍵索引值。

若數據表沒有主鍵彙集索引是怎麼創建的?在沒有主鍵時 Innodb 會給數據表的每條記錄生成一個 6 個字節長度的 RowId 字段,會以此創建彙集索引。

Select 語句查找記錄的過程

下面例子將展現索引數據的組織形式及 Select 語句查詢數據的過程。

  • 建表語句:

create table T (

ID int primary key,
k int NOT NULL DEFAULT 0,
s varchar(16) NOT NULL DEFAULT '',
index k(k)

) engine=InnoDB DEFAULT CHARSET=utf8;

insert into T values(100, 1, 'aa'),(200, 2, 'bb'),(300, 3, 'cc'),(500, 5, 'ee'),(600,6,'ff'),(700,7,'gg');

  • 索引結構示意

左邊是以主鍵 ID 創建起的彙集索引,其葉子節點存儲了完整的表記錄信息;右邊是以普通字段 K 創建的普通索引,其葉子節點的值是主鍵 ID。

  • Select 語句執行過程

select * from T where k between 3 and 5;

執行流程以下:

  1. 在 K 索引樹上找到 k=3 的記錄,取得 ID=300;
  2. 再到 ID 索引樹上查找 ID=300 對應的 R3;
  3. 在 k 索引樹取下一個值 k=5,取得 ID=500;
  4. 再回到 ID 索引樹查到 ID=500 對應的 R4;
  5. 在 k 索引樹取下一個值 k=6,不知足條件,循環結束。

上述查找記錄的過程當中引入了一個重要的概念: 回表 ,即回到主鍵索引樹搜索的過程。避免回表操做是提高 SQL 查詢效率的常規思路及重要方法。那麼如何避免回表?

注:該例子來自《MySQL 實戰 45 講》

覆蓋索引

MySQL 5.7,建表語句:

CREATE TABLE employees (
emp_no int(11) NOT NULL,
birth_date date NOT NULL,
first_name varchar(14) NOT NULL,
last_name varchar(16) NOT NULL,
gender enum('M','F') NOT NULL,
hire_date date NOT NULL,
PRIMARY KEY (emp_no),
KEY i_first_name (first_name),
KEY i_hire_date (hire_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

  • SQL 語句 A

explain select * from employees where hire_date > '1990-01-14';

explain 結果:

  • SQL 語句 B

explain select emp_no from employees where hire_date > '1990-01-14';

explain 結果:

  • 分析

從先後兩次 explain 的結果能夠看到 SQL 語句 A 的 extra 爲 using where,SQL 語句 B 的 extra 爲 using where;using index。這說明 A 沒有使用索引,而 B 使用了索引。

索引 K 中包含了查詢語句所須要的字段 ID 的值,無需再次回到主鍵索引樹查找,也就是「覆蓋」了咱們的查詢需求,咱們稱之爲覆蓋索引。覆蓋索引能夠減小樹的搜索次數,顯著提高查詢性能。

最左匹配

  • SQL 語句 A

explain select * from employees where hire_date > '1990-01-14' and first_name like '%Hi%';

  • SQL 語句 B

explain select * from employees where hire_date > '1990-01-14' and first_name like 'Hi%';

  • 分析

在上述測試的 SQL 語句 A 使用了極端方式: first_name like '%Hi%',先後都增長模糊匹配使得 SQL 語句沒法使用到索引;當去掉最左邊的‘%’後,SQL 語句 B 就使用了索引。最左匹配能夠是字符串索引的最左 N 個字符,也能夠是聯合索引的最左 M 的字段。合理規劃、使用最左匹配能夠減小索引,從而節約磁盤空間。

索引下推

何爲索引下推?咱們先從下面這組對比測試開始,將在 MySQL5.5 版本和 MySQL5.7 版本中執行同一條 SQL 語句:

select * from employees where hire_date > '1990-01-14' and first_name like 'Hi%';

  • 在 MySQL 5.5 執行 explain,extra 字段的值顯示沒有使用索引

執行查詢花費時間爲 0.12s

  • 在 MySQL 5.7 執行 explain,extra 字段的值顯示使用了索引下推

執行查詢花費時間爲 0.02s

  • 索引下推

explain 結果中的 extra 字段值包含 using index condition,則說明使用了索引下推。索引下推功能是從 5.6 版本開始支持的。在 5.6 版本以前,i_first_name 索引是沒有使用上的,須要每次去主鍵索引表取完整的記錄值進行比較。從 5.6 版本開始,因爲索引 i_first_name 的存在,能夠直接取索引的 first_name 值進行過濾,這樣不符合"first_name like 'Hi%'"條件的記錄就再也不須要回表操做。

MRR 優化

MySQL 5.6 版本開始支持 Multi-Range Read(MRR)優化,MRR 優化的目的是爲減小磁盤的隨機訪問,而且將隨機訪問轉化爲較爲順序的數據訪問,對於 IO-bound 類型的 SQL 查詢語句可帶來性能極大提高。咱們先看下對比測試,如下測試語句在同一個 MySQL 實例下執行,執行前均進行 mysql 服務重啓,以保證緩存此沒被預熱。

  • 關閉 MRR

SET @@optimizer_switch='mrr=off';
select * from employees where hire_date > '1990-01-14' and first_name like 'Hi%';

執行耗時未 0.90s

  • 開啓 MRR

SET @@optimizer_switch='mrr=on,mrr_cost_based=off';
select * from employees where hire_date > '1990-01-14' and first_name like 'Hi%';

  • 分析

從測試結果能夠發如今 mrr 從關閉到開啓,耗時從 0.90s 減小到 0.03s,查詢速率達到 30 倍的提高。

常見的索引失效場景

在 MySQL 表中創建了索引,SQL 查詢語句就會必定使用到索引麼?不必定,存在着索引失效的場景。咱們給 employees 表增一個組合索引,後續例子均基於此表進行分析、測試。

alter table employees add index i_b_f_l(birth_date, first_name, last_name)
alter table employees add index i_h(hire_date);

失效場景

  • 範圍查詢(>,<,<>)

explain select * from employees where hire_date > '1989-06-02';

  • 查詢條件類型不一致

alter table employees add index i_first_name (first_name);
explain select * from employees where first_name = 1;

  • 查詢條件使用了函數

explain select * from employees where CHAR_LENGTH(hire_date) = 10;

  • 模糊查詢

explain select * from employees where hire_date like '%1995';

  • 不使用組合索引的首個字段當條件

explain select * from employees where last_name = 'Kalloufi' and first_name = 'Saniya';

爲何會失效?

  • 順序讀比離散讀性能要好

    範圍查詢必定會致使索引失效麼?

    並不會!稍微更改下查詢條件看下 explain 的對比結果,能夠看到新語句用到索引下推,說明索引並未失效。爲何?

    在不使用覆蓋索引的狀況下,優化器只有在數據量小的時候纔會選擇使用非彙集索引。受制於傳統的機械磁盤特性,經過彙集索引順序讀數據行的性能會比經過非彙集索引離散讀數據行要好。因此,優化器在即便有非彙集索引、可是訪問數據量可能達到送記錄數的 20%時會選擇彙集索引。固然也能夠用 Force index 強制使用索引。

explain select * from employees where hire_date > '1999-06-02';

  • 沒法使用 B+索引快速查找

    B+樹索引支持快速查詢的基本要素是由於其索引鍵值是有序存儲的,從左到右由小到大,這樣就能夠在每一個層級的節點中快速查並進入下一層級,最終在葉子節點找到對應的值。

    使用函數會使得 MySQL 沒法使用索引進行快速查詢,由於對索引字段作函數操做會破壞索引值的有序性,因此優化器選擇不使用索引。而查詢條件類型不一致其實也是一樣的狀況,由於其使用了隱式類型轉換*。

模糊匹配和不使用組合索引的首字段做爲查詢條件均是沒法快速定位索引位置從而致使沒法使用索引。模糊匹配當查詢條件是 lwhere A ike 'a%',a 是 A 的最左前綴時是可能用上索引的(最左匹配),是否用上最終仍是依賴優化器對查詢數據量的評估。

回到初始的案例

讓咱們回到文章初的案例,嘗試回答下當時提出的 3 個問題。

-- A語句
SELECT FStatDate, FMerchantId, FVersion, FBatch, FTradeAmount, FTradeCount FROM T_MchStat_1020 WHERE FStatDate = 20201020 AND FVersion = 0 AND FMerchantId > 0 ORDER BY FMerchantId ASC LIMIT 0, 8000;

-- B語句
SELECT SQL_CALC_FOUND_ROWS a1.FStatDate,

a1.FMerchantId,
a1.FVersion,
FBatch,
FTradeAmount,
FTradeCount

FROM T_MchStat_1020 a1, (

SELECT FStatDate, FMerchantId, FVersion
FROM T_Mch******Stat_1020
WHERE FStatDate = 20201020
    AND FVersion = 0
    AND FMerchantId > 0
    ORDER BY FMerchantId ASC LIMIT 0, 8000 ) a2

where a1.FStatDate = a2.FStatDate

and a1.FVersion = a2.FVersion
and a1.FMerchantId = a2.FMerchantId;

SQL 語句 A 的查詢條件字段都在主鍵中,主鍵索引用到了沒?

主鍵索引實際上是有被使用的:索引的範圍查詢,只是其須要逐條讀取和解析全部記錄才致使慢查詢。

SQL 語句 B 的子查詢爲何可以用到索引?

  1. 前文中咱們介紹了彙集索引,其索引鍵值就是主鍵。
  2. 兩條 SQL 語句的不一樣之處在於 B 語句的子查詢語句的 Select 字段都包含在主鍵字段中,而 A 語句還有其它字段(例如 FBatch 和 FTradeAmount 等)。這種狀況下只憑主鍵索引的鍵值就能知足 B 語句的字段要求;A 語句則須要逐條取整行記錄進行解析。

先後兩條語句執行流程的差別是什麼?

  • SQL 語句 A 的執行過程:
  1. 逐條掃描索引表並比較查詢條件
  2. 遇到符合查詢條件的則讀取整行數據返回
  3. 回到 a 步驟,直至完成全部索引記錄的比較
  4. 對返回的全部符合條件的記錄(完整的記錄)進行排序
  5. 選取前 8000 條數據返回
  • SQL 語句 B 的執行過程:
  1. 逐條掃描索引表並比較查詢條件
  2. 遇到符合查詢條件的則從索引鍵中取相關字段值返回
  3. 回到 a 步驟,直至完成全部索引記錄的比較
  4. 對返回的全部符合條件的記錄(每條記錄只有 3 個主鍵)進行排序
  5. 選取前 8000 條數據返回造成臨時表
  6. 關聯臨時表與主表,使用主鍵相等比較查詢 8000 條數據
  • 對比兩個 SQL 語句的執行過程,能夠發現差別點集中在步驟 2 和步驟 4。在步驟 2 中 SQL 語句 A 須要隨機讀取整行數據並解析很是耗資源;步驟 4 涉及 MySQL 的排序算法,這裏也會對執行效率有影響,排序效果上看 SQL 語句 B 比 SQL 語句 A 好。

名詞解釋

  • 主鍵索引

顧名思義該類索引由表的主鍵組成,從左到右由小到大排序。一個 Innodb 存儲表只有一張主鍵索引表(彙集索引)。

  • 普通索引

最爲日常的一種索引,沒有特別限制。

  • 惟一索引

該索引的字段不能有相同值,但容許有空值。

  • 組合索引

由多列字段組合而成的索引,每每是爲了提高查詢效率而設置。

總結

在文章開始時介紹了常見的幾種索引數據結構,適合靜態數據的有序數組、適合 KV 結構的哈希索引及兼顧查詢及插入性能的搜索二叉樹;而後介紹了 Innodb 的常見索引實現方式 B+樹及 Select 語句使用 B+樹索引查找記錄的執行過程,在這個部分咱們瞭解了幾個關鍵的概念,回表、覆蓋索引、最左匹配、索引下推和 MMR;以後還總結了索引的失效場景及背後的緣由。最後,咱們回到最初的案例,分析出優化先後 SQL 語句在使用索引的差別,進而致使執行效率的差別。

本文介紹了索引的一些粗淺知識,但願可以對讀者有些許幫助。做爲階段性學習的一個總結,文章對 MySQL 索引的相關知識基本上是淺藏輒止,往後還需多多使用和深刻學習。

何以解憂?惟有學習。

推薦閱讀

=====

MySQL從入門到進階教程,主講老師:馬士兵、連鵬舉

字節跳動總結的設計模式 PDF 火了,完整版開放分享

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

若是能聽懂這個網約車實戰,哪怕接私活你均可以月入40K

爲何阿里巴巴的程序員成長速度這麼快,看完他們的內部資料我懂了

程序員達到50W年薪所須要具有的知識體系。

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

看完三件事❤️

========

若是你以爲這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:點贊,轉發,有大家的 『點贊和評論』,纔是我創造的動力。關注公衆號 『 Java鬥帝 』,不按期分享原創知識。同時能夠期待後續文章ing🚀

相關文章
相關標籤/搜索