從原理上理解MySQL的優化建議

概述

自從學習 MySQL 以來,我們一直聽到或者看到很多優化建議,比如說不要用 select * 查詢,用什麼字段就查什麼字段;建議用自增主鍵來作爲表的主鍵,等等。這些建議聽得很多感覺都成了 MySQL 開發的常識了,但是對於這些優化建議,我們有沒有想過爲什麼要這麼做呢?這篇博文我們從 MySQL 的原理出發,來解釋下爲什麼有這些優化建議?

本文實驗環境 MySQL 5.7.25

預備知識

B+ 樹索引

MySQL 的默認存儲引擎 InnoDB 使用 B+ 樹來存儲數據的,所以在分析優化建議之前,我們有必要了解 B+ 樹索引的基本原理。

上圖是一個 B + 樹索引示意圖(B+ 樹的定義可以看這裏),每個節點表示一個磁盤塊,也可以理解爲數據庫中的頁。我們來分析下 B+ 樹索引的查找過程,如果我要查詢主鍵爲 35 的數據,索引會怎麼走呢?首先會判斷 35 小於根節點 37 ,繼續查詢左子樹,判斷 35 大於 22 和 33 那麼進入其右子樹,找到了葉子節點 33 ,繼續遍歷找到了 35 ,最後取出其 data 即可。在索引的情況下,查詢 35 只用了3次 IO 操作,這是非常高效的。在真實的場景下,3層的b+樹可以表示上百萬的數據,如果上百萬的數據查找只需要三次IO,性能提高將是巨大的,如果沒有索引,每個數據項都要發生一次IO,那麼總共需要百萬次的IO,顯然成本非常非常高。上圖中也是體現了只要維持樹的高度足夠低,IO 操作就會足夠少,IO次數少,查詢性能就會高。

Explain 執行計劃

上圖就是一個 explian 執行計劃,先看看上面各個字段的含義是什麼?

  • id: Query Optimizer 所選定的執行計劃中的查詢編號。
  • select_type: 所使用的查詢類型,主要有幾種查詢類型:

  • table: 顯示執行這一步所訪問的數據庫中的表的名稱。
  • partitions: 查詢分區表匹配的分區,非分區表顯示 NULL 。
  • type: 查詢表所使用的方式,類型如下:

  • 他們的性能由好到差依次是:system > const > eq_ref > ref > full_text > ref_or_null > unique_subquery > index_subquery > range > index_merge > index > all 。
  • possible_keys: 查詢可能用到的索引。
  • key_len: 用到的索引長度。
  • ref: 展示將那些列或者常量與命中的索引比較。
  • rows: 執行這次查詢掃描的行數。
  • filtered: 過濾行數百分比,最大值是100,當顯示100時候,表示沒有過濾行, rows顯示了檢查的估計行數,乘以過濾百分比將顯示與下表連接的行數。例如,如果行數爲1000,過濾條件爲50.00(50%),則與下表聯接的行數爲1000×50%= 500。
  • extra: 執行查詢額外的條件。

掌握了前面這些前置知識,我們來用這些知識分析下經常建議我們的那些MySQL優化建議。

爲什麼建議使用自增主鍵

當我們每次建立表的時候都在考慮是用自增主鍵呢?還是用 UUID 呢?但是從性能考慮我們還是建議使用自增 Id,爲什麼呢?主要是由於 MySQL B+ 樹索引性質決定的,數據的新增是要更新索引的,也就是要更新 B+ 樹。換句話說,使用自增Id 和 非自增 Id 哪種更新 B+ 樹更快,成本更低,誰就是更優的選擇。我們來模擬下自增 Id 插入和非自增 Id 插入情況。

自增Id 插入情況: 我們在一個已經有10條數據的 B + 數上插入2條數據,分別是10和11,我們看看樹是如何變化的。

我們這裏可以發現兩個特點:

1、自增的數據插入影響的範圍永遠只有最右的子樹,要麼直接在子樹插入節點,要麼就是子樹分裂,影響其父節點。

2、除了最右子樹,其他子樹的節點都是滿的。

上面兩個特點有什麼影響呢?我們根據前面 B+ 樹索引示意圖可以知道,每個點都是一個磁盤塊,操作每個節點相當於進行一次 IO,由於每次插入影響的節點只有最右子樹,那麼磁盤 IO 的範圍就可控;最重要一點是除最右子樹,其他子樹的節點都是滿的,這種情況,葉子節點數據的物理連續性會更好, 根據局部性原理,查詢性能也會更高。

非自增 Id 插入情況:

非自增 Id 插入特點對比自增 Id 插入我們很容易就能知道:

1、插入影響節點不可控,無法預知。

2、每個子樹都存在葉子節點不滿的情況。

按照之前的分析思路,我們也就知道了非自增 Id 插入有什麼性能劣勢了。由於插入數據影響節點不可控,導致節點分裂的情況就會更頻繁,節點分裂也是 IO 操作,性能自然受到影響。子樹的葉子節點不滿,會導致葉子節點物理連續性不好。最後如果我們是UUID的話,Id 過長,會佔用節點空間,每個頁能存儲的節點變少,頁分裂變多,性能也會受到影響。這也是爲什麼建議使用自增主鍵的原因。

爲什麼不要使用 select * 查詢

我們經常聽到查詢表,只要查詢自己想要的字段,不需要的字段就不要查詢,嚴禁使用 select *,我們能想到很直觀的理由就是,數據庫要幫你翻譯成每個字段名去查詢,接着查詢多餘的字段會佔用內存,帶寬等資源。這確實是一個理由,而且這個理由很重要,但是我這裏想說的是另外一個原因,覆蓋索引。我之前的一篇索引文章也介紹了覆蓋索引,感興趣的同學可以點擊這裏。覆蓋索引的意思是指查詢使用聯合索引覆蓋了要查詢的字段,這樣數據庫不用去進行回表,從而減少IO,提高性能。

這裏我用MySQL官方給的示列數據進行一個實驗,數據地址下載可以點擊這裏

我選擇 employees 表數據進行演示,默認數據是沒有聯合索引的,我們加上一個聯合索引:

---employee表結構-----------
+------------+---------------+------+-----+---------+-------+
| Field      | Type          | Null | Key | Default | Extra |
+------------+---------------+------+-----+---------+-------+
| emp_no     | int(11)       | NO   | PRI | NULL    |       |
| birth_date | date          | NO   |     | NULL    |       |
| first_name | varchar(14)   | NO   | MUL | NULL    |       |
| last_name  | varchar(16)   | NO   |     | NULL    |       |
| gender     | enum('M','F') | NO   |     | NULL    |       |
| hire_date  | date          | NO   |     | NULL    |       |
+------------+---------------+------+-----+---------+-------+

ALTER TABLE employees.employees add Index `idx_first_name_last_name` (first_name,last_name);

查看該表索引情況:show index from employees.employees;

表已經成功創建了first_name 和 last_name的符合索引,我們開啓 profiles 監控 SQL 執行的情況。

SET SESSION profiling = 1;

然後分別執行以下SQL

SELECT first_name,last_name  FROM employees.employees WHERE first_name='Eric';
SELECT *  FROM employees.employees WHERE first_name='Eric';

查看Profiles;

這裏我們看到使用 select * 比 select 字段慢了4倍左右,爲什麼會這樣呢?我們看看執行計劃。

我們看到兩者的執行計劃幾乎一樣,只有 Extra 有區別,使用字段的 Extra 顯示 using index,這就告訴你只使用索引就找到了想要的數據,因爲你的索引就是用 first_name 和 last_name 建立的, 而 select * 它還需要查詢 genderhire_date字段(主鍵字段不用額外查,輔助索引指向的就是主鍵),所以它不能不進行回表查詢其他字段,性能差異也是這裏。

總結

本文從原理上分析了我們日常的兩點建議,爲什麼建議使用自增主鍵?爲什麼不建議使用 select * 查詢?其實主要最終的原因還是和索引相關,既然我們用索引來提高我們的效率就要充分利用它,下面是知識點總結:

1、B+ 樹查詢的效率高低是受其樹高影響,樹的高度越低,查詢IO次數越少,性能相對也就越高。

2、執行計劃的類型由好到差依次是:system > const > eq_ref > ref > full_text > ref_or_null > unique_subquery > index_subquery > range > index_merge > index > all 。

3、自增主鍵的好處就是連續,插入維護的成本相對較低,同時子樹的葉子節點大部分是滿節點,物理連續性好,查詢性能更優。

4、UUID 主鍵長度過長,導致單個子節點存儲的主鍵變少,更平凡的出發頁分裂,影響性能,這也是爲什麼建議索引不要太長的原因。

5、覆蓋索引是很好的優化技巧,可以讓查詢直接通過索引返回數據,而不用回表,減少IO,提升性能。

參考

1、https://dev.mysql.com/doc/refman/5.7/en/explain-output.html

2、https://dev.mysql.com/doc/refman/5.7/en/explain-output.html#explain-join-types

3、http://blog.codinglabs.org/articles/theory-of-mysql-index.html

來源:開源中國

作者:木木匠

原文:https://my.oschina.net/luozhou/blog/4289527

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章