1. 程式人生 > >SQL執行內幕:從執行原理看調優的本質

SQL執行內幕:從執行原理看調優的本質

相信大家看過無數的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_