SQL執行內幕:從執行原理看調優的本質
阿新 • • 發佈:2020-06-29
相信大家看過無數的MySQL調優經驗貼了,會告訴你各種調優手段,如:
* 避免 select *;
* join欄位走索引;
* 慎用in和not in,用exists取代in;
* 避免在where子句中對欄位進行函式操作;
* 儘量避免更新聚集索引;
* group by如果不需要排序,手動加上 order by null;
* join選擇小表作為驅動表;
* order by欄位儘量走索引...
其中有些手段也許跟隨者MySQL版本的升級過時了。我們真的需要背這些調優手段嗎?我覺得是沒有必要的,在掌握`MySQL儲存架構`和`SQL執行原理`的情況下,我們就很自然的明白,為什麼要提議這麼優化了,甚至能夠發現別人提的不太合理的優化手段。
在 [洞悉MySQL底層架構:遊走在緩衝與磁碟之間](https://www.itzhai.com/database/insight-into-the-underlying-architecture-of-mysql-buffer-and-disk.html) 這篇文章中,我們已經介紹了MySQL的儲存架構,詳細對你在MySQL`儲存`、`索引`、`緩衝`、`IO`相關的調優經驗中有了一定的其實。
本文,我們重點講解常用的SQL的執行原理,從執行原理,以及MySQL內部對SQL的優化機制,來分析SQL要如何調優,理解為什麼要這樣...那樣...那樣...調優。
![](https://img2020.cnblogs.com/blog/494394/202006/494394-20200628234500190-1979572113.png)
如果沒有特別說明,本文以MySQL5.7版本作為講解和演示。
閱讀完本文,您將瞭解到:
* **COUNT:** MyISAM和InnoDB儲存引擎處理count的區別是什麼?
* **COUNT:** count為何效能差?
* **COUNT:** count有哪些書寫方式,怎麼count統計會快點?
* **ORDER BY:** order by語句有哪些排序模式,以及每種排序模式的優缺點?
* **ORDER BY:** order by語句會用到哪些排序演算法,在什麼場景下會選擇哪種排序演算法
* **ORDER BY:** 如何檢視和分析sql的order by優化手段(執行計劃 + OPTIMIZER_TRACE日誌)
* **ORDER BY:** 如何優化order by語句的執行效率?(思想:減小行查詢大小,儘量走索引,能夠走覆蓋索引最佳,可適當增加sort buffer記憶體大小)
* **JOIN:** join走索引的情況下是如何執行的?
* **JOIN:** join不走索引的情況下是如何執行的?
* **JOIN:** MySQL對Index Nested-Loop Join做了什麼優化?(MMR,BKA)
* **JOIN:** BNL演算法對快取會產生什麼影響?有什麼優化策略?
* **JOIN:** 有哪些常用的join語句?
* **JOIN:** 針對join語句,有哪些優化手段?
* **UNION:** union語句執行原理是怎樣的?
* **UNION:** union是如何去重的?
* **GROUP BY:** group by完全走索引的情況下執行計劃如何?
* **GROUP BY:** 什麼情況下group by會用到臨時表?什麼情況下會用到臨時表+排序?
* **GROUP BY:** 對group by有什麼優化建議?
* **DISTINCT:** distinct關鍵詞執行原理是什麼?
* **子查詢:** 有哪些常見的子查詢使用方式?
* **子查詢:** 常見的子查詢優化有哪些?
* **子查詢:** 真的要儘量使用關聯查詢取代子查詢嗎?
* **子查詢:**in 的效率真的這麼慢嗎?
* **子查詢:** MySQL 5.6之後對子查詢做了哪些優化?(SEMIJOIN,Materializatioin,Exists優化策略)
* **子查詢:** Semijoin有哪些優化策略,其中Materializatioin策略有什麼執行方式,為何要有這兩種執行方式?
* **子查詢:** 除了in轉Exists這種優化優化,MariaDB中的exists轉in優化措施有什麼作用?
![](https://img2020.cnblogs.com/blog/494394/202006/494394-20200628234517359-555486504.png)
# 1、count
**儲存引擎的區別**
* MyISAM引擎每張表中存放了一個meta資訊,裡面包含了row_count屬性,記憶體和檔案中各有一份,記憶體的count變數值通過讀取檔案中的count值來進行初始化。[^1]但是如果帶有where條件,還是必須得進行表掃描
* InnoDB引擎執行count()的時候,需要把資料一行行從引擎裡面取出來進行統計。
下面我們介紹InnoDB中的count()。
## count中的一致性檢視
**InnoDB中為何不像MyISAM那樣維護一個row_count變數呢?**
前面 [洞悉MySQL底層架構:遊走在緩衝與磁碟之間](https://www.itzhai.com/database/insight-into-the-underlying-architecture-of-mysql-buffer-and-disk.html) 一文我們瞭解到,InnoDB為了實現事務,是需要MVCC支援的。MVCC的關鍵是一致性檢視。一個事務開啟瞬間,所有活躍的事務(未提交)構成了一個檢視陣列,InnoDB就是通過這個檢視陣列來判斷行資料是否需要undo到指定的版本。
如下圖,假設執行count的時候,一致性檢視得到當前事務能夠取到的最大事務ID DATA_TRX_ID=1002,那麼行記錄中事務ID超過1002都都要通過undo log進行版本回退,最終才能得出最終哪些行記錄是當前事務需要統計的:
![](https://img2020.cnblogs.com/blog/494394/202006/494394-20200628234603245-1057639770.png)
row1是其他事務新插入的記錄,當前事務不應該算進去。所以最終得出,當前事務應該統計row2,row3。
> **執行count會影響其他頁面buffer pool的命中率嗎?**
>
> 我們知道buffer pool中的LRU演算法是是經過改進的,預設情況下,舊子列表(old區)佔3/8,count載入的頁面一直往舊子列表中插入,在舊子列表中淘汰,不會晉升到新子列表中。所以不會影響其他頁面buffer pool的命中率。
## count(主鍵)
count(主鍵)執行流程如下:
* 執行器請求儲存引擎獲取資料;
* 為了保證掃描資料量更少,**儲存引擎找到最小的那顆索引樹獲取所有記錄**,返回記錄的id給到server。返回記錄之前會進行MVCC及其可見性的判斷,只返回當前事務可見的資料;
* server獲取到記錄之後,判斷id如果不為空,則累加到結果記錄中。
![](https://img2020.cnblogs.com/blog/494394/202006/494394-20200628234639633-933947836.png)
## count(1)
count(1)與count(主鍵)執行流程基本一致,區別在於,針對查詢出的每一條記錄,不會取記錄中的值,而是**直接返回一個"1"**用於統計累加。統計了所有的行。
## count(欄位)
與count(主鍵)類似,會篩選非空的欄位進行統計。如果**欄位沒有新增索引,那麼會掃描聚集索引樹,導致掃描的資料頁會比較多,效率相對慢點**。
## count(*)
count(*)不會取記錄的值,與count(1)類似。
執行效率對比:count(欄位) < count(主鍵) < count(1)
# 2、order by
以下是我們本節作為演示例子的表,假設我們有如下表:
![](https://img2020.cnblogs.com/blog/494394/202006/494394-20200628234718297-1287344126.png)
索引如下:
![](https://img2020.cnblogs.com/blog/494394/202006/494394-20200628234751795-485442321.png)
對應的idx_d索引結構如下(這裡我們做了一些誇張的手法,讓一個頁資料變小,為了展現在索引樹中的查詢流程):
![](https://img2020.cnblogs.com/blog/494394/202006/494394-20200628234821619-964468367.png)
## 2.1、如何跟蹤執行優化
為了方便分析sql的執行流程,我們可以在當前session中開啟 optimizer_trace:
> SET optimizer_trace='enabled=on';
然後執行sql,執行完之後,就可以通過以下堆疊資訊檢視執行詳情了:
> SELECT * FROM information_schema.OPTIMIZER_TRACE\G;
以下是
```sql
select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 100,2;
```
的執行結果,其中符合a=3的有8457條記錄,**針對order by重點關注以下屬性**:
```json
"filesort_priority_queue_optimization": { // 是否啟用優先順序佇列
"limit": 102, // 排序後需要取的行數,這裡為 limit 100,2,也就是100+2=102
"rows_estimate": 24576, // 估計參與排序的行數
"row_size": 123, // 行大小
"memory_available": 32768, // 可用記憶體大小,即設定的sort buffer大小
"chosen": true // 是否啟用優先順序佇列
},
...
"filesort_summary": {
"rows": 103, // 排序過程中會持有的行數
"examined_rows": 8457, // 參與排序的行數,InnoDB層返回的行數
"number_of_tmp_files": 0, // 外部排序時,使用的臨時檔案數量
"sort_buffer_size": 13496, // 記憶體排序使用的記憶體大小
"sort_mode": "sort_key, additional_fields" // 排序模式
}
```
### 2.1.1、排序模式
其中 sort_mode有如下幾種形式:
* `sort_key, rowid`:表明排序緩衝區元組包含排序鍵值和原始錶行的行id,排序後需要使用行id進行回表,這種演算法也稱為`original filesort algorithm`(回表排序演算法);
* `sort_key, additional_fields`:表明排序緩衝區元組包含排序鍵值和查詢所需要的列,排序後直接從緩衝區元組取資料,無需回表,這種演算法也稱為`modified filesort algorithm`(不回表排序);
* `sort_key, packed_additional_fields`:類似上一種形式,但是附加的列(如varchar型別)緊密地打包在一起,而不是使用固定長度的編碼。
#### 如何選擇排序模式
選擇哪種排序模式,與`max_length_for_sort_data`這個屬性有關,這個屬性預設值大小為1024位元組:
* 如果查詢列和排序列佔用的大小超過這個值,那麼會轉而使用`sort_key, rowid`模式;
* 如果不超過,那麼所有列都會放入sort buffer中,使用`sort_key, additional_fields`或者`sort_key, packed_additional_fields`模式;
* 如果查詢的記錄太多,那麼會使用`sort_key, packed_additional_fields`對可變列進行壓縮。
### 2.1.2、排序演算法
基於參與排序的資料量的不同,可以選擇不同的排序演算法:
* 如果排序取的結果很小,小於記憶體,那麼會使用`優先順序佇列`進行堆排序;
* 例如,以下只取了前面10條記錄,會通過優先順序佇列進行排序:
* ```sql
select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 10;
```
* 如果排序limit n, m,n太大了,也就是說需要取排序很後面的資料,那麼會使用sort buffer進行`快速排序`:
* 如下,表中a=1的資料又三條,但是由於需要limit到很後面的記錄,MySQL會對比優先順序佇列排序和快速排序的開銷,選擇一個比較合適的排序演算法,這裡最終放棄了優先順序佇列,轉而使用sort buffer進行快速排序:
* ```sql
select a, b, c, d from t20 force index(idx_abc) where a=1 order by d limit 300,2;
```
* 如果參與排序的資料sort buffer裝不下了,那麼我們會一批一批的給sort buffer進行記憶體快速排序,結果放入排序臨時檔案,最終使對所有排好序的臨時檔案進行`歸併排序`,得到最終的結果;
* 如下,a=3的記錄超過了sort buffer,我們要查詢的資料是排序後1000行起,sort buffer裝不下1000行資料了,最終MySQL選擇使用sort buffer進行分批快排,把最終結果進行歸併排序:
* ```sql
select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 1000,10;
```
## 2.2、order by走索引避免排序
執行如下sql:
```sql
select a, b, c, d from t20 force index(idx_d) where d like 't%' order by d limit 2;
```
我們看一下執行計劃:
![](https://img2020.cnblogs.com/blog/494394/202006/494394-20200628234902249-1292230147.png)
發現Extra列為:`Using index condition`,也就是這裡只走了索引。
執行流程如下圖所示:
通過idx_d索引進行range_scan查詢,掃描到4條記錄,然後order by繼續走索引,已經排好序,直接取前面兩條,然後去聚集索引查詢完整記錄,返回最終需要的欄位作為查詢結果。這個過程只需要藉助索引。
![](https://img2020.cnblogs.com/blog/494394/202006/494394-20200628234945603-1559987123.png)
**如何檢視和修改sort buffer大小?**
我們看一下當前的sort buffer大小:
![](https://img2020.cnblogs.com/blog/494394/202006/494394-20200628235020232-2056460751.png)
可以發現,這裡預設配置了sort buffer大小為512k。
我們可以設定這個屬性的大小:
> SET GLOBAL sort_buffer_size = 32*1024;
>
> 或者
>
> SET sort_buffer_size = 32*1024;
下面我們統一把sort buffer設定為32k
```
SET sort_buffer_size = 32*1024;
```
## 2.3、排序演算法案例
### 2.3.1、使用優先順序佇列進行堆排序
如果排序取的結果很小,並且小於sort buffer,那麼會使用優先順序佇列進行堆排序;
例如,以下只取了前面10條記錄:
```sql
select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 10;
```
a=3的總記錄數:`8520`。檢視執行計劃:
![](https://img2020.cnblogs.com/blog/494394/202006/494394-20200628235051346-428277259.png)
發現這裡where條件用到了索引,order by limit用到了排序。我們進一步看看執行的optimizer_trace日誌:
```json
"filesort_priority_queue_optimization": {
"limit": 10,
"rows_estimate": 27033,
"row_size": 123,
"memory_available": 32768,
"chosen": true // 使用優先順序佇列進行排序
},
"filesort_execution": [
],
"filesort_summary": {
"rows": 11,
"examined_rows": 8520,
"number_of_tmp_files": 0,
"sort_buffer_size": 1448,
"sort_mode": "sort_key, additional_fields"
}
```
發現這裡是用到了優先順序佇列進行排序。排序模式是:sort_key, additional_fields,即先回表查詢完整記錄,把排序需要查詢的所有欄位都放入sort buffer進行排序。
所以這個執行流程如下圖所示:
1. 通過where條件a=3掃描到8520條記錄;
2. 回表查詢記錄;
3. 把8520條記錄中需要的欄位放入sort buffer中;
4. 在sort buffer中進行堆排序;
5. 在排序好的結果中取limit 10前10條,寫入net buffer,準備傳送給客戶端。
![](https://img2020.cnblogs.com/blog/494394/202006/494394-20200628235133372-2136165743.png)
### 2.3.2、內部快速排序
如果排序limit n, m,n太大了,也就是說需要取排序很後面的資料,那麼會使用sort buffer進行快速排序。MySQL會對比優先順序佇列排序和歸併排序的開銷,選擇一個比較合適的排序演算法。
> **如何衡量究竟是使用優先順序佇列還是記憶體快速排序?**
> 一般來說,快速排序演算法效率高於堆排序,但是堆排序實現的優先順序佇列,無需排序完所有的元素,就可以得到order by limit的結果。
> MySQL原始碼中聲明瞭快速排序速度是堆排序的3倍,在實際排序的時候,會根據待排序數量大小進行切換演算法。如果資料量太大的時候,會轉而使用快速排序。
有如下SQL:
```sql
select a, b, c, d from t20 force index(idx_abc) where a=1 order by d limit 300,2;
```
我們把sort buffer設定為32k:
```
SET sort_buffer_size = 32*1024;
```
其中a=1的記錄有3條。檢視執行計劃:
![](https://img2020.cnblogs.com/blog/494394/202006/494394-20200628235439071-1419808000.png)
可以發現,這裡where條件用到了索引,order by limit 用到了排序。我們進一步看看執行的optimizer_trace日誌:
```json
"filesort_priority_queue_optimization": {
"limit": 302,
"rows_estimate": 27033,
"row_size": 123,
"memory_available": 32768,
"strip_additional_fields": {
"row_size": 57,
"sort_merge_cost": 33783,
"priority_queue_cost": 61158,
"chosen": false // 對比發現快速排序開銷成本比優先順序佇列更低,這裡不適用優先順序佇列
}
},
"filesort_execution": [
],
"filesort_summary": {
"rows": 3,
"examined_rows": 3,
"number_of_tmp_files": 0,
"sort_buffer_size": 32720,
"sort_