相信你們看過無數的MySQL調優經驗貼了,會告訴你各類調優手段,如:html
其中有些手段也許跟隨者MySQL版本的升級過期了。咱們真的須要背這些調優手段嗎?我以爲是沒有必要的,在掌握MySQL存儲架構
和SQL執行原理
的狀況下,咱們就很天然的明白,爲何要提議這麼優化了,甚至可以發現別人提的不太合理的優化手段。java
在 洞悉MySQL底層架構:遊走在緩衝與磁盤之間 這篇文章中,咱們已經介紹了MySQL的存儲架構,詳細對你在MySQL存儲
、索引
、緩衝
、IO
相關的調優經驗中有了必定的其實。mysql
本文,咱們重點講解經常使用的SQL的執行原理,從執行原理,以及MySQL內部對SQL的優化機制,來分析SQL要如何調優,理解爲何要這樣...那樣...那樣...調優。算法
若是沒有特別說明,本文以MySQL5.7版本做爲講解和演示。sql
閱讀完本文,您將瞭解到:數據庫
存儲引擎的區別編程
MyISAM引擎每張表中存放了一個meta信息,裏面包含了row_count屬性,內存和文件中各有一份,內存的count變量值經過讀取文件中的count值來進行初始化。[1]可是若是帶有where條件,仍是必須得進行表掃描json
InnoDB引擎執行count()的時候,須要把數據一行行從引擎裏面取出來進行統計。後端
下面咱們介紹InnoDB中的count()。數組
InnoDB中爲什麼不像MyISAM那樣維護一個row_count變量呢?
前面 洞悉MySQL底層架構:遊走在緩衝與磁盤之間 一文咱們瞭解到,InnoDB爲了實現事務,是須要MVCC支持的。MVCC的關鍵是一致性視圖。一個事務開啓瞬間,全部活躍的事務(未提交)構成了一個視圖數組,InnoDB就是經過這個視圖數組來判斷行數據是否須要undo到指定的版本。
以下圖,假設執行count的時候,一致性視圖獲得當前事務可以取到的最大事務ID DATA_TRX_ID=1002,那麼行記錄中事務ID超過1002都都要經過undo log進行版本回退,最終才能得出最終哪些行記錄是當前事務須要統計的:
row1是其餘事務新插入的記錄,當前事務不該該算進去。因此最終得出,當前事務應該統計row2,row3。
執行count會影響其餘頁面buffer pool的命中率嗎?
咱們知道buffer pool中的LRU算法是是通過改進的,默認狀況下,舊子列表(old區)佔3/8,count加載的頁面一直往舊子列表中插入,在舊子列表中淘汰,不會晉升到新子列表中。因此不會影響其餘頁面buffer pool的命中率。
count(主鍵)執行流程以下:
count(1)與count(主鍵)執行流程基本一致,區別在於,針對查詢出的每一條記錄,不會取記錄中的值,而是直接返回一個"1"用於統計累加。統計了全部的行。
與count(主鍵)相似,會篩選非空的字段進行統計。若是字段沒有添加索引,那麼會掃描彙集索引樹,致使掃描的數據頁會比較多,效率相對慢點。
count(*)不會取記錄的值,與count(1)相似。
執行效率對比:count(字段) < count(主鍵) < count(1)
如下是咱們本節做爲演示例子的表,假設咱們有以下表:
索引以下:
對應的idx_d索引結構以下(這裏咱們作了一些誇張的手法,讓一個頁數據變小,爲了展示在索引樹中的查找流程):
爲了方便分析sql的執行流程,咱們能夠在當前session中開啓 optimizer_trace:
SET optimizer_trace='enabled=on';
而後執行sql,執行完以後,就能夠經過如下堆棧信息查看執行詳情了:
SELECT * FROM information_schema.OPTIMIZER_TRACE\G;
如下是
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重點關注如下屬性:
"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" // 排序模式 }
其中 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_key, additional_fields
或者sort_key, packed_additional_fields
模式;sort_key, packed_additional_fields
對可變列進行壓縮。基於參與排序的數據量的不一樣,能夠選擇不一樣的排序算法:
若是排序取的結果很小,小於內存,那麼會使用優先級隊列
進行堆排序;
例如,如下只取了前面10條記錄,會經過優先級隊列進行排序:
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進行快速排序:
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進行分批快排,把最終結果進行歸併排序:
select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 1000,10;
執行以下sql:
select a, b, c, d from t20 force index(idx_d) where d like 't%' order by d limit 2;
咱們看一下執行計劃:
發現Extra列爲:Using index condition
,也就是這裏只走了索引。
執行流程以下圖所示:
經過idx_d索引進行range_scan查找,掃描到4條記錄,而後order by繼續走索引,已經排好序,直接取前面兩條,而後去彙集索引查詢完整記錄,返回最終須要的字段做爲查詢結果。這個過程只須要藉助索引。
如何查看和修改sort buffer大小?
咱們看一下當前的sort buffer大小:
能夠發現,這裏默認配置了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;
若是排序取的結果很小,而且小於sort buffer,那麼會使用優先級隊列進行堆排序;
例如,如下只取了前面10條記錄:
select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 10;
a=3的總記錄數:8520
。查看執行計劃:
發現這裏where條件用到了索引,order by limit用到了排序。咱們進一步看看執行的optimizer_trace日誌:
"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進行排序。
因此這個執行流程以下圖所示:
若是排序limit n, m,n太大了,也就是說須要取排序很後面的數據,那麼會使用sort buffer進行快速排序。MySQL會對比優先級隊列排序和歸併排序的開銷,選擇一個比較合適的排序算法。
如何衡量到底是使用優先級隊列仍是內存快速排序?
通常來講,快速排序算法效率高於堆排序,可是堆排序實現的優先級隊列,無需排序完全部的元素,就能夠獲得order by limit的結果。
MySQL源碼中聲明瞭快速排序速度是堆排序的3倍,在實際排序的時候,會根據待排序數量大小進行切換算法。若是數據量太大的時候,會轉而使用快速排序。
有以下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條。查看執行計劃:
能夠發現,這裏where條件用到了索引,order by limit 用到了排序。咱們進一步看看執行的optimizer_trace日誌:
"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_mode": "<sort_key, packed_additional_fields>" }
能夠發現這裏最終放棄了優先級隊列,轉而使用sort buffer進行快速排序。
因此這個執行流程以下圖所示:
sort buffer
中;快速排序
;當參與排序的數據太多,一次性放不進去sort buffer的時候,那麼咱們會一批一批的給sort buffer進行內存排序,結果放入排序臨時文件,最終使對全部排好序的臨時文件進行歸併排序,獲得最終的結果。
有以下sql:
select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 1000,10;
其中a=3的記錄有8520條。執行計劃以下:
能夠發現,這裏where用到了索引,order by limit用到了排序。進一步查看執行的optimizer_trace日誌:
"filesort_priority_queue_optimization": { "limit": 1010, "rows_estimate": 27033, "row_size": 123, "memory_available": 32768, "strip_additional_fields": { "row_size": 57, "chosen": false, "cause": "not_enough_space" // sort buffer空間不夠,沒法使用優先級隊列進行排序了 } }, "filesort_execution": [ ], "filesort_summary": { "rows": 8520, "examined_rows": 8520, "number_of_tmp_files": 24, // 用到了24個外部文件進行排序 "sort_buffer_size": 32720, "sort_mode": "<sort_key, packed_additional_fields>" }
咱們能夠看到,因爲limit 1000,要返回排序後1000行之後的記錄,顯然sort buffer已經不能支撐這麼大的優先級隊列了,因此轉而使用sort buffer內存排序,而這裏須要在sort buffer中分批執行快速排序,獲得多個排序好的外部臨時文件,最終執行歸併排序。(外部臨時文件的位置由tmpdir參數指定)
其流程以下圖所示:
sort_key, additional_fields
,排序緩衝區元組包含排序鍵值和查詢所須要的列(先回表取須要的數據,存入排序緩衝區中),排序後直接從緩衝區元組取數據,無需再次回表。
上面 2.3.一、2.3.2節的例子都是這種排序模式,就不繼續舉例了。
sort_key, packed_additional_fields
:相似上一種形式,可是附加的列(如varchar類型)緊密地打包在一塊兒,而不是使用固定長度的編碼。
上面2.3.3節的例子就是這種排序模式,因爲參與排序的總記錄大小太大了,所以須要對附加列進行緊密地打包操做,以節省內存。
前面咱們提到,選擇哪一種排序模式,與max_length_for_sort_data
[2]這個屬性有關,max_length_for_sort_data
規定了排序行的最大大小,這個屬性默認值大小爲1024字節:
也就是說若是查詢列和排序列佔用的大小小於這個值,這個時候會走sort_key, additional_fields
或者sort_key, packed_additional_fields
算法,不然,那麼會轉而使用sort_key, rowid
模式。
如今咱們特地把這個值設置小一點,模擬sort_key, rowid
模式:
SET max_length_for_sort_data = 100;
這個時候執行sql:
select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 10;
這個時候再查看sql執行的optimizer_trace日誌:
"filesort_priority_queue_optimization": { "limit": 10, "rows_estimate": 27033, "row_size": 49, "memory_available": 32768, "chosen": true }, "filesort_execution": [ ], "filesort_summary": { "rows": 11, "examined_rows": 8520, "number_of_tmp_files": 0, "sort_buffer_size": 632, "sort_mode": "<sort_key, rowid>" }
能夠發現這個時候切換到了sort_key, rowid
模式,在這個模式下,執行流程以下:
id
和d
字段,放入sort buffer中進行堆排序;能夠發現,正由於行記錄太大了,因此sort buffer中只存了須要排序的字段和主鍵id,以時間換取空間,最終排序完成,再次從彙集索引中查找到全部須要的字段返回給客戶端,很明顯,這裏多了一次回表操做的磁盤讀,總體效率上是稍微低一點的。
根據以上的介紹,咱們能夠總結出如下的order by語句的相關優化手段:
max_length_for_sort_data
致使走了sort_key, rowid
排序模式,使得產生了更多的磁盤讀,影響性能;爲了演示join,接下來咱們須要用到這兩個表:
CREATE TABLE `t30` ( `id` int(11) NOT NULL AUTO_INCREMENT, `a` int(11) NOT NULL, `b` int(11) NOT NULL, `c` int(11) NOT NULL, PRIMARY KEY (`id`), KEY idx_a(a) ) ENGINE=InnoDB CHARSET=utf8mb4; CREATE TABLE `t31` ( `id` int(11) NOT NULL AUTO_INCREMENT, `a` int(11) NOT NULL, `f` int(11) NOT NULL, `c` int(11) NOT NULL, PRIMARY KEY (`id`), KEY idx_a(a) ) ENGINE=InnoDB CHARSET=utf8mb4; insert into t30(a,b,c) values(1, 1, 1),(12,2,2),(3,3,3),(11, 12, 31),(15,1,32),(33,33,43),(5,13,14),(4,13,14),(16,13,14),(10,13,14); insert into t31(a,f,c) values(1, 1, 1),(21,2,2),(3,3,3),(12, 1, 1),(31,20,2),(4,10,3),(2,23,24),(22,23,24),(5,23,24),(20,23,24);
在MySQL官方文檔中 8.8.2 EXPLAIN Output Format
[3] 提到:MySQL使用Nested-Loop Loin
算法處理全部的關聯查詢。使用這種算法,意味着這種執行模式:
下面咱們所講到的都是Nested-Loop Join
算法的不一樣實現。
多表join:無論多少個表join,都是用的Nested-Loop Join實現的。若是有第三個join的表,那麼會把前兩個表的join結果集做爲循環基礎數據,在執行一次Nested-Loop Join,到第三個表中匹配數據,更多多表同理。
咱們執行如下sql:
select * from t30 straight_join t31 on t30.a=t31.a;
查看執行計劃:
能夠發現:
該sql語句的執行流程以下圖:
因爲這個過程當中用到了idx_a索引,因此這種算法也稱爲:Index Nested-Loop
(索引嵌套循環join)。其僞代碼結構以下:
// A 爲t30彙集索引 // B 爲t31彙集索引 // BIndex 爲t31 idx_a索引 void indexNestedLoopJoin(){ List result; for(a in A) { for(bi in BIndex) { if (a satisfy condition bi) { output <a, b>; } } } }
假設t30記錄數爲m,t31記錄數爲n,每一次查找索引樹的複雜度爲log2(n),因此以上場景,總的複雜度爲:m + m*2*log2(n)
。
也就是說驅動表越小,複雜度越低,越能提升搜索效率。
咱們能夠發現,以上流程,每次從驅動表取一條數據,而後去被驅動表關聯取數,表現爲磁盤的隨記讀,效率是比較低低,有沒有優化的方法呢?
這個就得從MySQL的MRR(Multi-Range Read)
[4]優化機制提及了。
咱們執行如下代碼,強制開啓MMR功能:
set optimizer_switch="mrr_cost_based=off"
而後執行如下SQL,其中a是索引:
select * from t30 force index(idx_a) where a<=12 limit 10;
能夠獲得以下執行計劃:
能夠發現,Extra列提示用到了MRR優化。
這裏爲了演示走索引的場景,因此加了force index關鍵詞。
正常不加force index的狀況下,MySQL優化器會檢查到這裏即便走了索引仍是須要回表查詢,而且表中的數據量很少,那乾脆就直接掃描全表,不走索引,效率更加高了。
若是沒有MRR優化,那麼流程是這樣的:
使用了MRR優化以後,這個執行流程是這樣的:
read rnd buffer
;read rnd buffer
中的id排序;與Multi-Range Read的優化思路相似,MySQL也是經過把隨機讀改成順序讀,讓Index Nested-Loop Join
提高查詢效率,這個算法稱爲Batched Key Access(BKA)
[5]算法。
咱們知道,默認狀況下,是掃描驅動表,一行一行的去被驅動表匹配記錄。這樣就沒法觸發MRR優化了,爲了可以觸發MRR,因而BKA算法登場了。
在BKA算法中,驅動表
經過使用join buffer
批量在被驅動表
的輔助索引
中關聯匹配數據,獲得一批結果,一次性傳遞個數據庫引擎的MRR接口,從而能夠利用到MRR對磁盤讀的優化。
爲了啓用這個算法,咱們執行如下命令(BKA依賴於MRR):
set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';
咱們再次執行如下關聯查詢sql:
select * from t30 straight_join t31 on t30.a=t31.a;
咱們能夠獲得以下的執行計劃:
能夠發現,這裏用到了:Using join buffer(Batched Key Access)
。
執行流程以下:
若是join條件沒走索引,又會是什麼狀況呢,接下來咱們嘗試執行下對應的sql。
咱們執行如下sql:
select * from t30 straight_join t31 on t30.c=t31.c;
查看執行計劃:
能夠發現:
該語句的執行流程以下圖:
而後清空join buffer,存入下一批t30的數據,重複以上流程。
顯然,每批數據都須要掃描一遍被驅動表,批次越多,掃描越多,可是內存判斷總次數是不變的。因此總批次越小,越高效。因此,跟上一個算法同樣,驅動表越小,複雜度越低,越能提升搜索效率。
在 洞悉MySQL底層架構:遊走在緩衝與磁盤之間 一文中,咱們介紹了MySQL Buffer Pool的LRU算法,以下:
默認狀況下,同一個數據頁,在一秒鐘以後再次訪問,那麼就會晉升到新子列表(young區)。
恰巧,若是咱們用到了BNL算法,那麼分批執行的話,就會重複掃描被驅動表去匹配每個批次了。
考慮如下兩種會影響buffer pool的場景:
針對以上這種場景,爲了不影響buffer pool,最直接的辦法就是增長join_buffer_size的值,以減小對被驅動表的掃描次數。
咱們能夠經過把join的條件加上索引,從而避免了BNL算法,轉而使用BKA算法,這樣也能夠加快記錄的匹配速度,以及從磁盤讀取被驅動表記錄的速度。
有時候,被驅動表很大,可是關聯查詢又不多使用,直接給關聯字段加索引太浪費空間了,這個時候就能夠經過把被驅動表的數據放入臨時表,在零時表中添加索引的方式,以達成3.2.3.2的優化效果。
什麼是hash join呢,簡單來講就是這樣的一種模型:
把驅動表知足條件的數據取出來,放入一個hash結構中,而後把被驅動表知足條件的數據取出來,一行一行的去hash結構中尋找匹配的數據,依次找到知足條件的全部記錄。
通常狀況下,MySQL的join實現都是以上介紹的各類nested-loop算法的實現,可是從MySQL 8.0.18[6]開始,咱們可使用hash join來實現表連續查詢了。感興趣能夠進一步閱讀這篇文章進行了解:[Hash join in MySQL 8 | MySQL Server Blog](https://mysqlserverteam.com/hash-join-in-mysql-8/#:~:text=MySQL only supports inner hash,more often than it does.)
咱們在平時工做中,會遇到各類各樣的join語句,主要有以下:
INNER JOIN
LEFT JOIN
RIGHT JOIN
FULL OUTER JOIN
LEFT JOIN EXCLUDING INNER JOIN
RIGHT JOIN EXCLUDING INNER JOIN
OUTER JOIN EXCLUDING INNER JOIN
更詳細的介紹,能夠參考:
經過使用union能夠把兩個查詢結果合併起來,注意:
union all不會去除重複的行,union則會去除重複讀的行。
執行下面sql:
(select id from t30 order by id desc limit 10) union all (select c from t31 order by id desc limit 10)
該sql執行計劃以下圖:
執行流程以下:
執行下面sql:
(select id from t30 order by id desc limit 10) union (select c from t31 order by id desc limit 10)
該sql執行計劃以下圖:
執行流程以下:
咱們給t30加一個索引:
alter table t30 add index idx_c(c);
執行如下group bysql:
select c, count(*) from t30 group by c;
執行計劃以下:
發現這裏只用到了索引,緣由是idx_c
索引自己就是按照c排序好的,那麼直接順序掃描idx_c索引,能夠直接統計到每個c值有多少條記錄,無需作其餘的統計了。
如今咱們把剛剛的idx_c
索引給刪掉,執行如下sql:
select c, count(*) from t30 group by c order by null;
爲了不排序,因此咱們這裏添加了 order by null,表示不排序。
執行計劃以下:
能夠發現,這裏用到了內存臨時表。其執行流程以下:
若是咱們把上一步的order by null
去掉,默認狀況下,group by的結果是會經過c字段排序的。咱們看看其執行計劃:
能夠發現,這裏除了用到臨時表,還用到了排序。
咱們進一步看看其執行的OPTIMIZER_TRACE
日誌:
"steps": [ { "creating_tmp_table": { "tmp_table_info": { "table": "intermediate_tmp_table", // 建立中間臨時表 "row_length": 13, "key_length": 4, "unique_constraint": false, "location": "memory (heap)", "row_limit_estimate": 1290555 } } }, { "filesort_information": [ { "direction": "asc", "table": "intermediate_tmp_table", "field": "c" } ], "filesort_priority_queue_optimization": { "usable": false, "cause": "not applicable (no LIMIT)" // 因爲沒有 limit,不採用優先級隊列排序 }, "filesort_execution": [ ], "filesort_summary": { "rows": 7, "examined_rows": 7, "number_of_tmp_files": 0, "sort_buffer_size": 344, "sort_mode": "<sort_key, rowid>" // rowid排序模式 } } ]
經過日誌也能夠發現,這裏用到了中間臨時表,因爲沒有limit限制條數,這裏沒有用到優先級隊列排序,這裏的排序模式爲sort_key, rowid
。其執行流程以下:
臨時表是存放在磁盤仍是內存?
tmp_table_size 參數用於設置內存臨時表的大小,若是臨時表超過這個大小,那麼會轉爲磁盤臨時表:
能夠經過如下sql設置當前session中的內存臨時表大小:SET tmp_table_size = 102400;
查看官方文檔的 SELECT Statement
[9],能夠發現SELECT後面可使用許多修飾符來影響SQL的執行效果:
SELECT [ALL | DISTINCT | DISTINCTROW ] [HIGH_PRIORITY] [STRAIGHT_JOIN] [SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT] [SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS] select_expr [, select_expr] ... [into_option] [FROM table_references [PARTITION partition_list]] [WHERE where_condition] [GROUP BY {col_name | expr | position} [ASC | DESC], ... [WITH ROLLUP]] [HAVING where_condition] [ORDER BY {col_name | expr | position} [ASC | DESC], ...] [LIMIT {[offset,] row_count | row_count OFFSET offset}] [PROCEDURE procedure_name(argument_list)] [into_option] [FOR UPDATE | LOCK IN SHARE MODE] into_option: { INTO OUTFILE 'file_name' [CHARACTER SET charset_name] export_options | INTO DUMPFILE 'file_name' | INTO var_name [, var_name] ... }
這裏咱們重點關注下這兩個:
SQL_BIG_RESULT
:能夠在包含group by 和distinct的SQL中使用,提醒優化器查詢數據量很大,這個時候MySQL會直接選用磁盤臨時表取代內存臨時表,避免執行過程當中發現內存不足才轉爲磁盤臨時表。這個時候更傾向於使用排序取代二維臨時表統計結果。後面咱們會演示這樣的案例;SQL_SMALL_RESULT
:能夠在包含group by 和distinct的SQL中使用,提醒優化器數據量很小,提醒優化器直接選用內存臨時表,這樣會經過臨時表統計,而不是排序。固然,在平時工做中,不是特定的調優場景,以上兩個修飾符仍是比較少用到的。
接下來咱們就經過例子來講明下使用了SQL_BIG_RESULT
修飾符的SQL執行流程。
有以下SQL:
select SQL_BIG_RESULT c, count(*) from t30 group by c;
執行計劃以下:
能夠發現,這裏只用到了排序,沒有用到索引或者臨時表。這裏用到了SQL_BIG_RESULT
修飾符,告訴優化器group by的數據量很大,直接選用磁盤臨時表,但磁盤臨時表存儲效率不高,最終優化器使用數組排序的方式來完成這個查詢。(固然,這個例子實際的結果集並不大,只是做爲演示用)
其執行結果以下:
group by null
,避免進行排序;SQL_BIG_RESULT
修飾符,提醒優化器應該使用排序算法獲得group的結果。在大多數狀況下,DISTINCT
能夠考慮爲GROUP BY
的一個特殊案例,以下兩個SQL是等效的:
select distinct a, b, c from t30; select a, b, c from t30 group by a, b, c order by null;
這兩個SQL的執行計劃以下:
因爲這種等效性,適用於Group by的查詢優化也適用於DISTINCT。
區別:distinct是在group by以後的每組中取出一條記錄,distinct分組以後不進行排序。
在一個關聯查詢中,若是您只是查詢驅動表的列,而且在驅動表的列中聲明瞭distinct關鍵字,那麼優化器會進行優化,在被驅動表中查找到匹配的第一行時,將中止繼續掃描。以下SQL:
explain select distinct t30.a from t30, t31 where t30.c=t30.c;
執行計劃以下,能夠發現Extra列中有一個distinct,該標識即標識用到了這種優化[10:1]:
首先,咱們來明確幾個概念:
子查詢:能夠是嵌套在另外一個查詢(select insert update delete)內,子查詢也能夠是嵌套在另外一個子查詢裏面。
MySQL子查詢稱爲內部查詢,而包含子查詢的查詢稱爲外部查詢。子查詢能夠在使用表達式的任何地方使用。
接下來咱們使用如下表格來演示各類子查詢:
create table class ( id bigint not null auto_increment, class_num varchar(10) comment '課程編號', class_name varchar(100) comment '課程名稱', pass_score integer comment '課程及格分數', primary key (id) ) comment '課程'; create table student_class ( id bigint not null auto_increment, student_name varchar(100) comment '學生姓名', class_num varchar(10) comment '課程編號', score integer comment '課程得分', primary key (id) ) comment '學生選修課程信息'; insert into class(class_num, class_name, pass_score) values ('C001','語文', 60),('C002','數學', 70),('C003', '英文', 60),('C004', '體育', 80),('C005', '音樂', 60),('C006', '美術', 70); insert into student_class(student_name, class_num, score) values('James', 'C001', 80),('Talor', 'C005', 75),('Kate', 'C002', 65),('David', 'C006', 82),('Ann', 'C004', 88),('Jan', 'C003', 70),('James', 'C002', 97), ('Kate', 'C005', 90), ('Jan', 'C005', 86), ('Talor', 'C006', 92);
子查詢的用法比較多,咱們先來列舉下有哪些子查詢的使用方法。
可使用比較運算法,例如=,>,<將子查詢返回的單個值與where子句表達式進行比較,如
查找學生選擇的編號最大的課程信息:
SELECT class.* FROM class WHERE class.class_num = ( SELECT MAX(class_num) FROM student_class );
若是子查詢返回多個值,則能夠在WHERE子句中使用其餘運算符,例如IN或NOT IN運算符。如
查找學生都選擇了哪些課程:
SELECT class.* FROM class WHERE class.class_num IN ( SELECT DISTINCT class_num FROM student_class );
在FROM子句中使用子查詢時,從子查詢返回的結果集將用做臨時表。該表稱爲派生表或實例化子查詢。如 查找最熱門和最冷門的課程分別有多少人選擇:
SELECT max(count), min(count) FROM (SELECT class_num, count(1) as count FROM student_class group by class_num) as t1;
前面的示例中,您注意到子查詢是獨立的。這意味着您能夠將子查詢做爲獨立查詢執行。
與獨立子查詢不一樣,關聯子查詢是使用外部查詢中的數據的子查詢。換句話說,相關子查詢取決於外部查詢。對於外部查詢中的每一行,對關聯子查詢進行一次評估。
下面是比較運算符中的一個關聯子查詢。
查找每門課程超過平均分的學生課程記錄:
SELECT t1.* FROM student_class t1 WHERE t1.score > ( SELECT AVG(score) FROM student_class t2 WHERE t1.class_num = t2.class_num);
關聯子查詢中,針對每個外部記錄,都須要執行一次子查詢,由於每一條外部記錄的class_num可能都不同。
當子查詢與EXISTS或NOT EXISTS運算符一塊兒使用時,子查詢將返回布爾值TRUE或FALSE。
查找全部學生總分大於100分的課程:
select * from class t1 where exists( select sum(score) as total_score from student_class t2 where t2.class_num=t1.class_num group by t2.class_num having total_score > 100 )
上面咱們演示了子查詢的各類用法,接下來,咱們來說一會兒查詢的優化[11]。
子查詢主要由如下三種優化手段:
其中Semijoin只能用於IN,= ANY,或者EXISTS的子查詢中,不能用於NOT IN,<> ALL,或者NOT EXISTS的子查詢中。
下面咱們作一下詳細的介紹。
真的要儘可能使用關聯查詢取代子查詢嗎?
在《高性能MySQL》[12]一書中,提到:優化子查詢最重要的建議就是儘量使用關聯查詢代替,可是,若是使用的是MySQL 5.6或者更新版本或者MariaDB,那麼就能夠直接忽略這個建議了。由於這些版本對子查詢作了很多的優化,後面咱們會重點介紹這些優化。
in的效率真的這麼慢嗎?
在MySQL5.6以後是作了很多優化的,下面咱們就逐個來介紹。
Semijoin[13],半鏈接,所謂半鏈接,指的是一張表在另外一張表棧道匹配的記錄以後,返回第一張表的記錄。即便右邊找到了幾條匹配的記錄,也最終返回左邊的一條。
因此,半鏈接很是適用於查找兩個表之間是否存在匹配的記錄,而不關注匹配了多少條記錄這種場景。
半鏈接一般用於IN或者EXISTS語句的優化。
上面咱們講到:接很是適用於查找兩個表之間是否存在匹配的記錄,而不關注匹配了多少條記錄這種場景。
in關聯子查詢
這種場景,若是使用in來實現,可能會是這樣:
SELECT class_num, class_name FROM class WHERE class_num IN (SELECT class_num FROM student_class where condition);
在這裏,優化器能夠識別出IN子句要求子查詢僅從student_class表返回惟一的class_num。在這種狀況下,查詢會自動優化爲使用半聯接。
若是使用exists來實現,可能會是這樣:
SELECT class_num, class_name FROM class WHERE EXISTS (SELECT * FROM student_class WHERE class.class_num = student_class.class_num);
優化案例
統計有學生分數不及格的課程:
SELECT t1.class_num, t1.class_name FROM class t1 WHERE t1.class_num IN (SELECT t2.class_num FROM student_class t2 where t2.score < t1.pass_score);
咱們能夠經過執行如下腳本,查看sql作了什麼優化:
explain extended SELECT t1.class_num, t1.class_name FROM class t1 WHERE t1.class_num IN (SELECT t2.class_num FROM student_class t2 where t2.score < t1.pass_score); show warnings\G;
獲得以下執行執行計劃,和SQL重寫結果:
從這個SQL重寫結果中,能夠看出,最終子查詢變爲了semi join語句:
/* select#1 */ select `test`.`t1`.`class_num` AS `class_num`,`test`.`t1`.`class_name` AS `class_name` from `test`.`class` `t1` semi join (`test`.`student_class` `t2`) where ((`test`.`t2`.`class_num` = `test`.`t1`.`class_num`) and (`test`.`t2`.`score` < `test`.`t1`.`pass_score`))
而執行計劃中,咱們看Extra列:
Using where; FirstMatch(t1); Using join buffer (Block Nested Loop)
Using join buffer
這項是在join關聯查詢的時候會用到,前面講join語句的時候已經介紹過了,如今咱們重點看一下FirstMatch(t1)
這個優化項。
FirstMatch(t1)
是Semijoin優化策略中的一種。下面咱們詳細介紹下Semijoin有哪些優化策略。
MySQL支持5中Semijoin優化策略,下面逐一介紹。
在內部表尋找與外部表匹配的記錄,一旦找到第一條,則中止繼續匹配。
案例 - 統計有學生分數不及格的課程:
SELECT t1.class_num, t1.class_name FROM class t1 WHERE t1.class_num IN (SELECT t2.class_num FROM student_class t2 where t2.score < t1.pass_score);
執行計劃:
執行流程,圖比較大,請你們放大觀看:
您也能夠去MariaDB官網,查看官方的FirstMatch Strategy
[14]解釋。
將Semijoin做爲一個常規的inner join,而後經過使用一個臨時表去重。
具體演示案例,參考MariaDB官網:DuplicateWeedout Strategy[15],如下是官網例子的圖示:
能夠看到,灰色區域爲臨時表,經過臨時表惟一索引進行去重。
把內部表的數據基於索引進行分組,取每組第一條數據進行匹配。
具體演示案例,參考MariaDB官網:LooseScan Strategy[16],如下是官網例子的圖示:
若是子查詢是獨立的(非關聯子查詢),則優化器能夠選擇將獨立子查詢產生的結果存儲到一張物化臨時表中。
爲了觸發這個優化,咱們須要往表裏面添加多點數據,好讓優化器認爲這個優化是有價值的。
咱們執行如下SQL:
select * from class t1 where t1.class_num in(select t2.class_num from student_class t2 where t2.score > 80) and t1.class_num like 'C%';
執行流程以下:
物化表的惟一索引
MySQL會報物化子查詢全部查詢字段組成一個惟一索引,用於去重。如上面圖示,灰色連線的兩條記錄衝突去重了。
join操做能夠從兩個方向執行:
掃描物化表
,去與class表記錄進行匹配,這種咱們稱爲Materialize-scan
;物化表中查找
匹配記錄,這種咱們稱爲Materialize-lookup
,這個時候,咱們用到了物化表的惟一索引進行查找,效率會很快。下面咱們介紹下這兩種執行方式。
仍是以上面的sql爲例:
select * from class t1 where t1.class_num in(select t2.class_num from student_class t2 where t2.score > 80) and t1.class_num like 'C%';
執行計劃以下:
能夠發現:
subquery2
的表名,這個表正式咱們從id=2的查詢獲得的物化表。subquery2
執行eq_ref
,這裏用到了auto_key
,獲得匹配的記錄。也就是說,優化器選擇了對t1(class)表進行全表掃描,而後去物化表進行因此等值查找,最終獲得結果。
執行模型以下圖所示:
原則:小表驅動大表,關聯字段被驅動表添加索引
若是子查詢查出來的物化表很小,而外部表很大,而且關聯字段是外部表的索引字段,那麼優化器會選擇掃描物化表去關聯外部表,也就是Materialize-scan
,下面演示這個場景。
如今咱們嘗試給class表添加class_num惟一索引:
alter table class add unique uk_class_num(class_num);
而且在class中插入更多的數據。而後執行一樣的sql,獲得如下執行計劃:
能夠發現,這個時候id=1的查詢是選擇了subquery2,也就是物化表進行掃描,掃描結果逐行去t1表(class)進行eq_ref
匹配,匹配過程當中用到了t1表的索引。
這裏的執行流程正好與上面的相反,選擇了從class表關聯物化表。
如今,我問你們:Materialization策略何時會選擇從外部表關聯內部表?相信你們內心應該有答案了。
執行模型以下:
原則:小表驅動大表,關聯字段被驅動表添加索引
如今留給你們另外一個問題:以上例子中,這兩種Materialization的開銷分別是多少(從行讀和行寫的角度統計)
答案:
Materialize-lookup:40次讀student_class表,40次寫物化臨時表,42次讀外部表,40次lookup檢索物化臨時表;
Materialize-scan:15次讀student_class表,15次寫物化臨時表,15次掃描物化臨時表,執行15次class表索引查詢。
優化器使用Materialization
(物化)來實現更加有效的子查詢處理。物化針對非關聯子查詢進行優化。
物化經過把子查詢結果存儲爲臨時表(一般在內存中)來加快查詢的執行速度。MySQL在第一次獲取子查詢結果時,會將結果物化爲臨時表。隨後若是再次須要子查詢的結果,則直接從臨時表中讀取。
優化器可使用哈希索引爲臨時表創建索引,以使查找更加高效,而且經過索引來消除重複項,讓表保持更小。
子查詢物化的臨時表在可能的狀況下存儲在內存中,若是表太大,則會退回到磁盤上進行存儲。
若是未開啓物化優化,那麼優化器有時會將非關聯子查詢重寫爲關聯子查詢。
能夠經過如下命令查詢優化開關(Switchable Optimizations
[18])狀態:
SELECT @@optimizer_switch\G;
也就是說,以下的in獨立子查詢語句:
SELECT * FROM t1 WHERE t1.a IN (SELECT t2.b FROM t2 WHERE where_condition);
會重寫爲exists關聯子查詢語句:
SELECT * FROM t1 WHERE EXISTS (SELECT t2.b FROM t2 WHERE where_condition AND t1.a=t2.b);
開啓了物化開關以後,獨立子查詢避免了這樣的重寫,使得子查詢只會查詢一次,而不是重寫爲exists語句致使外部每一行記錄都會執行一次子查詢,嚴重下降了效率。
考慮如下的子查詢:
outer_expr IN (SELECT inner_expr FROM ... WHERE subquery_where)
MySQL「從外到內」來評估查詢。也就是說,它首先獲取外部表達式outer_expr的值,而後運行子查詢並獲取其產生的結果集用於比較。
若是咱們能夠把outer_expr下推到子查詢中進行條件判斷,以下:
EXISTS (SELECT 1 FROM ... WHERE subquery_where AND outer_expr=inner_expr)
這樣就可以減小子查詢的行數了。相比於直接用IN來講,這樣就能夠加快SQL的執行效率了。
而涉及到NULL值的處理,相對就比較複雜,因爲篇幅所限,這裏做爲延伸學習,感興趣的朋友能夠進一步閱讀:
8.2.2.3 Optimizing Subqueries with the EXISTS Strategy[19]
延伸:
除了讓關聯的in子查詢轉爲exists進行優化以外。在MariaDB 10.0.2版本中,引入了另外一種相反的優化措施:可讓exists子查詢轉換爲非關聯in子查詢,這樣就能夠用上非關聯資產性的物化優化策略了。
總結一會兒查詢的優化方式:
condition push down
把條件下推到exists子查詢中,減小子查詢的結果集,從而達到優化的目的。limit的用法:
limit [offset], [rows]
其中 offset表示偏移量,rows表示須要返回的行數。
offset limit 表中的剩餘數據 _||_ __||__ __||__ | | | | | RRRRRR RRRRRRRR RRR... |______| || 結果集
MySQL進行表掃描,讀取到第 offset + rows條數據以後,丟棄前面offset條記錄,返回剩餘的rows條記錄。
好比如下sql:
select * from t30 order by id limit 10000, 10;
這樣總共會掃描10010條。
若是查詢的offset很大,避免直接使用offset,而是經過id到彙集索引中檢索查找。
select * from t30 where id > 10000 limit 10;
固然,這也是會有問題的,若是id中間產生了非連續的記錄,這樣定位就不許確了。寫到這裏,篇幅有點長了,最後這個問題留給你們思考,感興趣的朋友能夠進一步思考探討與延伸。
這篇文章的內容就差很少介紹到這裏了,可以閱讀到這裏的朋友真的是頗有耐心,爲你點個贊。
本文爲arthinking
基於相關技術資料和官方文檔撰寫而成,確保內容的準確性,若是你發現了有何錯漏之處,煩請高擡貴手幫忙指正,萬分感激。
你們能夠關注個人博客:itzhai.com
獲取更多文章,我將持續更新後端相關技術,涉及JVM、Java基礎、架構設計、網絡編程、數據結構、數據庫、算法、併發編程、分佈式系統等相關內容。
若是您以爲讀完本文有所收穫的話,能夠關注
個人帳號,或者點贊
吧,碼字不易,您的支持就是我寫做的最大動力,再次感謝!
關注個人公衆號,及時獲取最新的文章。
更多文章
本文做者: arthinking
版權聲明:
BY-NC-SA
許可協議:創做不易,如需轉載,請聯繫做者,謝謝!
https://zhuanlan.zhihu.com/p/54378839. Retrieved from https://zhuanlan.zhihu.com/p/54378839 ↩︎
8.2.1.14 ORDER BY Optimization. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html ↩︎
8.8.2 EXPLAIN Output Format. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/explain-output.html ↩︎
Batched Key Access: a Significant Speed-up for Join Queries. Retrieved from https://conferences.oreilly.com/mysql2008/public/schedule/detail/582 ↩︎
Batched Key Access Joins. Retrieved from http://underpop.online.fr/m/mysql/manual/mysql-optimization-bka-optimization.html ↩︎
[Hash join in MySQL 8. MySQL Server Blog. Retrieved from https://mysqlserverteam.com/hash-join-in-mysql-8/#:~:text=MySQL only supports inner hash,more often than it does](https://mysqlserverteam.com/hash-join-in-mysql-8/#:~:text=MySQL only supports inner hash,more often than it does) ↩︎
MySQL JOINS Tutorial: INNER, OUTER, LEFT, RIGHT, CROSS. Retrieved from https://www.guru99.com/joins.html ↩︎
How the SQL join actually works?. Retrieved from https://stackoverflow.com/questions/34149582/how-the-sql-join-actually-works ↩︎
13.2.9 SELECT Statement. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/select.html ↩︎
8.2.1.18 DISTINCT Optimization. Retrieved from https://dev.mysql.com/doc/refman/8.0/en/distinct-optimization.html ↩︎ ↩︎
Subquery Optimizer Hints. Retrieved from https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html#optimizer-hints-subquery ↩︎
高性能MySQL第3版[M]. 電子工業出版社, 2013-5:239. ↩︎
8.2.2.1 Optimizing Subqueries, Derived Tables, and View References with Semijoin Transformations. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/semijoins.html ↩︎
FirstMatch Strategy. Retrieved from https://mariadb.com/kb/en/firstmatch-strategy/ ↩︎
DuplicateWeedout Strategy. Retrieved from https://mariadb.com/kb/en/duplicateweedout-strategy/ ↩︎
LooseScan Strategy. Retrieved from https://mariadb.com/kb/en/loosescan-strategy/ ↩︎
Semi-join Materialization Strategy. Retrieved from https://mariadb.com/kb/en/semi-join-materialization-strategy/ ↩︎
Switchable Optimizations. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/switchable-optimizations.html ↩︎
8.2.2.3 Optimizing Subqueries with the EXISTS Strategy. Retrieved from https://dev.mysql.com/doc/refman/8.0/en/subquery-optimization-with-exists.html ↩︎
EXISTS-to-IN Optimization. Retrieved from https://mariadb.com/kb/en/exists-to-in-optimization/ ↩︎