優化select語句
- 爲了使慢速
SELECT ... WHERE
查詢更快,首先要檢查的是是否可以添加索引。爲避免浪費磁盤空間,請構造組合索引。 - 調整函數調用,使函數調用從一多次減少爲一次,從而極大地提高了效率。
- 定期使用
ANALYZE TABLE
使表統計信息保持最新 ,爲優化器提供構造有效執行計劃所需的信息。 - 閱讀
EXPLAIN
計劃並調整索引。 - 調整MySQL用於緩存的內存區域的大小和屬性。
- 避免鎖導致的查詢性能問題。
1. WHERE子句優化
-
刪除不必要的括號。
((a AND b) AND c OR (((a AND b) AND (c AND d)))) -> (a AND b AND c) OR (a AND b AND c AND d)
-
常量摺疊。
(a<b AND b=c) AND a=5 -> b>5 AND b=c AND a=5
-
常量條件消除。
(b>=5 AND b=5) OR (b=6 AND 5=5) OR (b=7 AND 5=6) -> b=5 OR b=6
-
使用索引的常量表達式僅計算一次。
-
直接從MyISAM和MEMORY表中檢索沒有WHERE的單個表上的COUNT(*)。
-
無效的常量表達式。
SELECT * FROM t WHERE a<0 AND a>1; ︶︵︶
-
如果沒有使用GROUP BY或者聚合函數(如
COUNT()
,MIN()
)等)就將HAVING字句合併到WHERE字句。SELECT * FROM t WHERE a=1 HAVING b>1; -> SELECT * FROM t WHERE a=1 AND b>1;
-
對於聯接中的每個表,構造一個更簡單的WHERE以獲得表的快速WHERE評估,並儘快跳過行。(複雜的WHERE會延遲每一行數據的過濾時間)
-
在查詢的所有表中,優先讀取常量表。常量表可以是以下任意一個
-
空表或者只有一行數據的表;
-
與PRIMARY KEY或UNIQUE索引上的WHERE子句一起使用的表,其中所有索引部分都與常量表達式進行比較,並定義爲NOT NULL;
以下所有表均用作常量表:
SELECT * FROM t WHERE primary_key=1; SELECT * FROM t1,t2 WHERE t1.primary_key=1 AND t2.primary_key=t1.id;
-
-
如果
ORDER BY
和GROUP BY
子句中的所有列都來自同一表,則在連接時優先使用該表。 -
ORDER BY
或GROUP BY
包含連接隊列中第一個表以外的表中的列,則會創建一個臨時表(用於連接後數據集的排序,排序後自動刪除)。 -
在索引中的所有列都是數字列的情況下,MySQL甚至可以在不查詢數據文件的情況下從索引讀取行。
-
在輸出每一行之前,
HAVING
將跳過不匹配該子句的那些行 。(HAVING時對結果集做裁剪,並不能減少掃描行數)
快速查詢示例:
SELECT COUNT(*) FROM tbl_name;
SELECT MIN(key_part1),MAX(key_part1) FROM tbl_name;
SELECT MAX(key_part2) FROM tbl_name WHERE key_part1=constant;
SELECT ... FROM tbl_name ORDER BY key_part1,key_part2,... LIMIT 10;
SELECT ... FROM tbl_name ORDER BY key_part1 DESC, key_part2 DESC, ... LIMIT 10;
假設索引列是數字,MySQL僅使用索引樹來解析以下查詢:
SELECT key_part1,key_part2 FROM tbl_name WHERE key_part1=val;
SELECT COUNT(*) FROM tbl_name
WHERE key_part1=val1 AND key_part2=val2;
SELECT key_part2 FROM tbl_name GROUP BY key_part1;
以下查詢使用索引來按排序順序檢索行,而無需單獨的排序遍歷:
SELECT ... FROM tbl_name
ORDER BY key_part1,key_part2,... ;
SELECT ... FROM tbl_name
ORDER BY key_part1 DESC, key_part2 DESC, ... ;
2. 範圍優化
對於 BTREE 和 HASH 索引,當使用=、<=>、IN、IS NULL 或者 IS NOT NULL 操作符時 ,
關鍵元素與常量值的比較關係對應一個範圍條件。
HASH
索引使用 =, <=>, IN(),IS NULL,或IS NOT NULL操作符的等式比較會很快。BTREE
索引使用 >, <, >=, <=, BETWEEN, !=,或 <> ,LIKE ‘pattern’(其中 'pattern’不以通配符開始)操作符,關鍵元素與常量值的比較關係對應一個範圍條件。
範圍優化器嘗試從WHERE
子句中爲每個可能的索引提取範圍條件 。在提取過程中,刪除了不能用於構建範圍條件的條件,合併了產生重疊範圍的條件,並刪除了產生空範圍的條件。優化過程請參考單部分索引的範圍訪問方法。
2.1 多值比較的等距範圍優化
CREATE TABLE t1 (f1 INT NOT NULL, f2 INT NOT NULL, PRIMARY KEY(f1, f2));
INSERT INTO t1 VALUES
(1,1), (1,2), (1,3), (1,4), (1,5),
(2,1), (2,2), (2,3), (2,4), (2,5);
INSERT INTO t1 SELECT f1, f2 + 5 FROM t1;
INSERT INTO t1 SELECT f1, f2 + 10 FROM t1;
INSERT INTO t1 SELECT f1, f2 + 20 FROM t1;
INSERT INTO t1 SELECT f1, f2 + 40 FROM t1;
ANALYZE TABLE t1;
EXPLAIN SELECT f1, f2 FROM t1 WHERE f2 > 40;
MySQL可以選擇索引掃描以獲取所有行,然後根據子句中的f2 > 40
條件WHERE
以產生最終結果集。範圍掃描比全索引掃描更有效,但是在這種情況下不能使用,因爲沒有條件在第一個索引列f1
上 。但是,從MySQL 8.0.13開始,優化器可以使用一種稱爲“skip scan”的方法,將一次範圍掃描分爲多次範圍掃描,再將多次掃描的結果合併返回。
算法的運行方式如下:
- 獲取第一個索引的第一個不同值(
f1 = 1
)。 - 根據第一和第二索引構造範圍(
f1 = 1 AND f2 > 40
)。 - 執行範圍掃描。
- 獲取第一個索引的下一個不同值(
f1 = 2
)。 - 根據第一和第二索引構造範圍(
f1 = 2 AND f2 > 40
)。 - 執行範圍掃描。
使用此策略可減少訪問的行數,因爲MySQL會跳過不符合每個構造範圍的行。此“skip scan”訪問方法適用於以下情況:
- 表T具有至少一個複合索引,其索引的形式爲([A_1,…,A_
k
,] B_1,…,B_m
,C [,D_1,…,D_n
])。關鍵部分A和D可能爲空,但B和C必須爲非空。。 - 該查詢僅使用一個表。
- 查詢不使用
GROUP BY
或DISTINCT
。 - 該查詢僅引用索引中的列。
- 前綴A_1,…,A_
k
必須是相等謂詞,並且它們必須是常量。這包括IN()
。 - 該查詢必須是一個聯合查詢。即,
AND
的OR
條件。 - C上必須有範圍條件。
- 允許在D字段上有過濾條件,但是必須和C上的範圍條件一起使用。
對於使用了跳躍範圍掃描特性的SQL,使用EXPLAIN查看其執行計劃,可以看到:
- 在執行計劃輸出的Extra一欄中有: Using index for skip scan
- 在執行計劃輸出的possible_keys一欄中會顯示可以使用到的索引
2.2 行構造函數表達式的範圍優化
優化程序可以將範圍掃描訪問方法應用於以下形式的查詢:
SELECT ... FROM t1 WHERE ( col_1, col_2 ) IN (( 'a', 'b' ), ( 'c', 'd' ));
爲了使優化器使用範圍掃描,查詢必須滿足以下條件:
-
僅使用IN()謂詞,不使用NOT IN()。
-
在IN()謂詞的左側 ,行構造器僅包含列引用。
-
在IN()謂詞的右側 ,行構造器僅包含運行時常量。
-
在IN()謂詞的右側 ,有多個行構造器。
3. 索引條件下推優化(ICP)
索引條件下推(ICP)是針對MySQL使用索引從表中檢索行的情況的一種優化。
沒有使用ICP的過程:
使用ICP的過程:
當使用“索引條件下推”時,EXPLAIN輸出將在 Extra
列中顯示 Using index condition
。默認情況下,索引條件下推處於啓用狀態。
限制:
- 對於
InnoDB
表,ICP僅用於二級索引。ICP的目標是減少全行讀取的次數,從而減少I / O操作。 - ICP只用於單表
- ICP用於range, ref, eq_ref, and ref_or_null,並且需要訪問表的全部行。
- ICP可用於
InnoDB
和MyISAM
表
4. Nested-Loop Join Algorithm
4.1 Simple Nested-Loop Join(NLJ)
EXPLAIN SELECT * FROM t1 STRAIGHT_JOIN t2 ON (t1.f1=t2.f1);
在這條語句裏,被驅動表t2的字段f1上有索引,join過程用上了這個索引,因此這個語句的執行流程是這樣的:
- 從表t1中讀入一行數據 R;
- 從數據行R中,取出f1字段到表t2裏去查找;
- 取出表t2中滿足條件的行,跟R組成一行,作爲結果集的一部分;
- 重複執行步驟1到3,直到表t1的末尾循環結束。
在這個流程裏:
- 對驅動表t1做了全表掃描,這個過程需要掃描160行;
- 而對於每一行R,根據f1字段去表t2查找,走的是樹搜索過程。由於兩個表的數據都是不是一一對應的,因此每次的搜索過程要掃描26行,也是總共掃描160*26行;
- 所以,整個執行流程,總掃描行數是160+160*26。
4.2 Block Nested-Loop Join(NBL)
BNL使用對在外部循環中讀取的行進行緩衝,以減少必須讀取內部循環表的次數。
EXPLAIN SELECT * FROM t1 STRAIGHT_JOIN t2 ON (t1.f1=t2.f1) WHERE t1.f1 = 2;
在這個語句中,被驅動表t2的字段f1上有索引,join過程沒有用上這個索引因而使用上了join buffer,因此這個語句的執行流程是這樣的:
- 將表t1,t2的已用列讀入join buffer中;
- 由於join_buffer是以無序數組的方式組織的,因此對錶t2中的每一行,都要做160次判斷,總共需要在內存中做的判斷次數是:160 * 160次。
在這個流程裏:
- 對驅動表t1做了全表掃描,這個過程需要掃描160行;
- 對被驅動表t2做了全表掃描,這個過程需要掃描160行。
使用Join Buffer有以下要點:
-
join_buffer_size變量決定buffer大小。join_buffer的大小是由參數join_buffer_size設定的,默認值是256k。如果放不下表t1的所有數據話,就分段放。
-
只有在join類型爲
ALL
、index
、range
的時候纔可以使用join buffer。 -
連接緩衝區永遠不會分配給第一個非常量表,即使它的類型是
ALL
或index
。 -
join buffer中只會保存參與join的列, 並非整個數據行。
-
爲每個可以緩衝的連接分配一個join buffer,因此可以使用多個join buffer來處理給定查詢。
eg.
EXPLAIN SELECT * FROM t1 LEFT JOIN t2 on t1.f1 = t2.f1 LEFT JOIN t3 on t3.f2 = t2.f1 where t1.f1 = 1;
NBL的算法邏輯:
for each row in t1 matching range {
for each row in t2 matching reference key {
store used columns from t1, t2 in join buffer
if buffer is full {
for each row in t3 {
for each t1, t2 combination in join buffer {
if row satisfies join conditions, send to client
}
}
empty join buffer
}
}
}
if buffer is not empty {
for each row in t3 {
for each t1, t2 combination in join buffer {
if row satisfies join conditions, send to client
}
}
}
結論:
-
如果可以使用Index Nested-Loop Join算法,也就是說可以用上被驅動表上的索引,其實是沒問題的。
-
在決定哪個表做驅動表的時候,應該是兩個表按照各自的條件過濾,過濾完成之後,計算參與join的各個字段的總數據量,數據量小的那個表,就是“小表”,應該作爲驅動表。
5. 嵌套連接優化
-
在使用外部聯接運算符的聯接表達式中省略括號,則可能會更改原始表達式的結果集。(對於外部聯接或與內部聯接混合的外部聯接,刪除括號也可能會改變結果。)
eg.
- 表格
t1
包含行(1)
,(2)
- 表
t2
包含行(1,101)
- 表
t3
包含行(101)
mysql> SELECT * FROM t1 LEFT JOIN (t2 LEFT JOIN t3 ON t2.b=t3.b OR t2.b IS NULL) ON t1.a=t2.a; +------+------+------+------+ | a | a | b | b | +------+------+------+------+ | 1 | 1 | 101 | 101 | | 2 | NULL | NULL | NULL | +------+------+------+------+ mysql> SELECT * FROM (t1 LEFT JOIN t2 ON t1.a=t2.a) LEFT JOIN t3 ON t2.b=t3.b OR t2.b IS NULL; +------+------+------+------+ | a | a | b | b | +------+------+------+------+ | 1 | 1 | 101 | 101 | | 2 | NULL | NULL | 101 | +------+------+------+------+
- 表格
-
對於內連接嵌套循環,支持類似“下推”條件,假設我們的
WHERE
條件P(T1,T2,T3)
可以用一個聯合公式表示:P(T1,T2,T2) = C1(T1) AND C2(T2) AND C3(T3)
SELECT * T1 LEFT JOIN (T2,T3) ON P1(T1,T2) AND P2(T1,T3) WHERE P(T1,T2,T3)
則上述語句的執行過程
FOR each row t1 in T1 such that C1(t1) { FOR each row t2 in T2 such that P1(t1,t2) AND C2(t2) { FOR each row t3 in T3 such that P2(t2,t3) AND C3(t3) { IF P(t1,t2,t3) { t:=t1||t2||t3; OUTPUT t; } } } }
AND
連接的C1(T1)
,C2(T2)
,C3(T3)
從最內環推到最外環的地方進行過濾。如果C1(T1)
是非常嚴格的條件,則此條件下推可能會大大減少表中T1
傳遞給內部循環的行數。 -
對於外聯接嵌套循環,只有在發現外部表中的當前行在內部表中具有匹配項之後,才檢查
WHERE
條件。因此,將條件從內部嵌套循環中推出的優化不能直接應用於具有外部聯接的查詢。在這裏,引入了條件下推謂詞,該條件下推謂詞由遇到匹配時打開的標誌保護。FOR each row t1 in T1 such that C1(t1) { BOOL f1:=FALSE; FOR each row t2 in T2 such that P1(t1,t2) AND (f1?C2(t2):TRUE) { BOOL f2:=FALSE; FOR each row t3 in T3 such that P2(t2,t3) AND (f1&&f2?C3(t3):TRUE) { IF (f1&&f2?TRUE:(C2(t2) AND C3(t3))) { t:=t1||t2||t3; OUTPUT t; } f2=TRUE; f1=TRUE; } IF (!f2) { IF (f1?TRUE:C2(t2) && P(t1,t2,NULL)) { t:=t1||t2||NULL; OUTPUT t; } f1=TRUE; } } IF (!f1 && P(t1,NULL,NULL)) { t:=t1||NULL||NULL; OUTPUT t; } }
所以對於外連接外聯接嵌套循環,只要關聯表中存在匹配的數據,查詢的執行時間就可以大大改善。
內連接比外聯接查詢效率要好,因爲內連接默認支出”下推“條件。
外聯接優化:對於
LEFT JOIN
,如果WHERE
條件始終爲false ,優化器則將LEFT JOIN
更改爲內聯接。eg. t2.column2不存在值爲5的數據。
SELECT * FROM t1 LEFT JOIN t2 ON (column1) WHERE t2.column2=5;
因此,將查詢轉換爲內部聯接是安全的:
SELECT * FROM t1, t2 WHERE t2.column2=5 AND t1.column1=t2.column1;
6. IS NULL優化
SELECT * FROM tbl_name WHERE key_col IS NULL;
ref_or_null
通過首先讀取參考鍵,然後單獨搜索具有NULL
鍵值的行來工作。
優化只能處理一個IS NULL
。在以下查詢中,MySQL僅在表達式上使用a
鍵查找(t1.a=t2.a AND t2.a IS NULL)
,而不能在b
鍵使用:
SELECT * FROM t1, t2
WHERE (t1.a=t2.a AND t2.a IS NULL)
OR (t1.b=t2.b AND t2.b IS NULL);
7. ORDER BY優化
MySQL可以使用索引排序,無法使用索引時使用的filesort
排序。對於組合索引(key_part1, key_part2):
SELECT * FROM t1
ORDER BY key_part1, key_part2;
上面這條語句,全表ORDER BY時,如果SELECT * 的查詢列僅包含索引列,優化器使用索引;如果SELECT * 的查詢列多於索引列,在這種情況下,掃描整個索引並查找錶行以查找索引中未包含的列可能比掃描表並對結果進行排序要低效。如果是這樣,優化器可能不會使用索引。
SELECT * FROM t1
WHERE key_part1 = constant
ORDER BY key_part2;
如果WHERE子句的選擇性足以使索引範圍掃描比表掃描高效,則(key_part1,key_part2)上的索引將避免排序:
SELECT key_part2 FROM t1
WHERE key_part1 = constant
ORDER BY key_part2;
在以下情況,MySQL 不能使用索引來優化ORDER BY
(使用filesort
排序),儘管它仍然可以使用索引來查找與該WHERE
子句匹配的行 。例子:
-
該查詢在
ORDER BY
上有不同的索引(兩個或以上);SELECT * FROM t1 ORDER BY key1, key2;
-
該查詢
ORDER BY
對索引的非連續使用:SELECT * FROM t1 WHERE key2=constant ORDER BY key1_part1, key1_part3;
-
查詢混合
ASC
和DESC
:SELECT * FROM t1 ORDER BY key_part1 DESC, key_part2 ASC;
-
用於獲取行的索引與在
ORDER BY
中使用的索引不同:SELECT * FROM t1 WHERE key2=constant ORDER BY key1;
-
該查詢使用
ORDER BY
的表達式包含除索引列名稱以外的術語:SELECT * FROM t1 ORDER BY ABS(key); SELECT * FROM t1 ORDER BY -key;
-
該查詢聯接了許多表,並且
ORDER BY
中的列並非全部來自用於檢索行的第一個非恆定表。(這是EXPLAIN
輸出中沒有const
聯接類型的第一個表 。) -
查詢具有
ORDER BY
和GROUP BY
表達式。 -
僅在前綴索引列上
ORDER BY
。在這種情況下,索引不能用於排序。 -
在下面的語句中,選擇列表中列的名稱也爲
a
,但它是別名。它指的是ABS(a)
,如同ORDER BY
中的a
,所以上的索引t1.a
不能使用:SELECT ABS(a) AS a FROM t1 ORDER BY a; =>SELECT ABS(a) FROM t1 ORDER BY ABS(a);
要使用上索引,
SELECT
中的別名和ORDER BY
中的列名不一樣即可。
優化filesort
排序
爲了提高ORDER BY
速度,請檢查是否可以讓MySQL使用索引而不是額外的排序階段。如果不可能,請嘗試以下策略:
- 增加 sort_buffer_size 變量值。
- 增加 read_rnd_buffer_size 變量值。
- 更改tmpdir 系統變量,使其指向具有大量可用空間的專用文件系統。
GROUP BY優化
GROUP BY子句的最常用方法是掃描整個表並創建一個新的臨時表,其中每個組中的所有行都是連續的,然後使用此臨時表來發現組並應用聚合函數(如果有的話)。在某些情況下,可使用索引訪問避免創建臨時表。
優化策略:使用索引訪問避免創建臨時表。
有兩種使用索引進行分組的方法:
- 將分組操作與所有範圍謂詞(如果有的話)一起應用。
- 首先執行範圍掃描,然後對結果元組進行分組。
8.1 鬆散索引掃描
-
單表查詢。
-
最左前綴原則。
-
只支持
MIN()
和MAX()
,並且是同一列。該列必須在索引中,並且必須是緊跟在GROUP BY
中的列之後 。 -
索引中除
GROUP BY
查詢中引用的那些部分以外的任何其他部分都必須是常量,MIN()
和MAX()
函數的參數除外 。 -
不支持前綴索引。
eg. 假設t1(c1,c2,c3,c4)
table上有一個索引idx(c1,c2,c3)
。鬆散索引掃描訪問方法可用於以下查詢:
SELECT c1, c2 FROM t1 GROUP BY c1, c2;
SELECT DISTINCT c1, c2 FROM t1;
SELECT c1, MIN(c2) FROM t1 GROUP BY c1;
SELECT c1, c2 FROM t1 WHERE c1 < const GROUP BY c1, c2;
SELECT MAX(c3), MIN(c3), c1, c2 FROM t1 WHERE c2 > const GROUP BY c1, c2;
SELECT c2 FROM t1 WHERE c1 < const GROUP BY c1, c2;
SELECT c1, c2 FROM t1 WHERE c3 = const GROUP BY c1, c2;
8.2 緊密索引掃描
緊密索引掃描可以是全索引掃描,也可以是範圍索引掃描,具體取決於查詢條件。
當不滿足寬鬆索引掃描的條件時,仍然有可能避免創建用於GROUP BY
查詢的臨時表。如果WHERE
子句中有範圍條件,則此方法僅讀取滿足這些條件的鍵,之後才執行分組操作。
爲了使該方法起作用,對於查詢中所有引用鍵部分之前或之間的部分的列,需要一個等式填充。
假設t1(c1,c2,c3,c4)
table上有一個索引 idx(c1,c2,c3)
。以下查詢不適用於前面所述的“鬆散索引掃描”訪問方法,但仍適用於“緊索引掃描”訪問方法。
-
GROUP BY
中存在一個缺口,但c2 = 'a'
覆蓋:SELECT c1, c2, c3 FROM t1 WHERE c2 = 'a' GROUP BY c1, c3;
-
GROUP BY
開頭不是鍵的第一部分,但是c1 = 'a'
爲該部分提供常數:SELECT c1, c2, c3 FROM t1 WHERE c1 = 'a' GROUP BY c2, c3;
9. DISTINCT優化
在大多數情況下,DISTINCT
子句可以視爲的特殊情況GROUP BY
。例如,以下兩個查詢是等效的:
SELECT DISTINCT c1, c2, c3 FROM t1
WHERE c1 > const;
SELECT c1, c2, c3 FROM t1
WHERE c1 > const GROUP BY c1, c2, c3;
由於這種等效性,適用於GROUP BY
查詢的優化 也可以應用於帶有DISTINCT
子句的查詢。
10. LIMIT優化
如果只需要結果集中指定的行數,則在查詢子句中使用LIMIT
,而不是獲取整個結果集並丟棄多餘的數據。
MySQL有時會優化包含LIMIT row_count
子句和no HAVING子句的查詢(HAVING是在結果集中篩選,無法優化):
- 如果只選擇有限制的幾行,MySQL在某些情況下會使用索引,而通常情況下,它更願意進行全表掃描。
- 如果
LIMIT
與ORDER BY
結合使用,當MySQL找到排序結果的第一個row_count
行後立刻停止排序。 - 如果
LIMIT row_count
與DISTINCT
結合使用,當MySQL找到row_count行唯一記錄後立刻停止。 LIMIT 0
快速返回一個空集。這對於檢查查詢的有效性很有用。
11. 全表掃描優化
當MySQL使用全表掃描來解析查詢時,EXPLAIN的輸出顯示type列中的所有內容。通常在以下情況下發生:
-
表太小了,執行表掃描比使用鍵查找更快。對於行數少於10行且行長度較短的表,這種情況很常見。
-
對於索引列,ON或WHERE子句中沒有可用的限制。
-
將索引列與常量值進行比較,MySQL已經計算出(基於索引樹)常量覆蓋了表的很大一部分,並且認爲表掃描會更快。
對於小表,表掃描通常是合適的,性能影響可以忽略不計。對於大型表,請嘗試以下技術以避免優化器錯誤地選擇表掃描:
-
ANALYZE TABLE
tbl_name
。 -
使用FORCE INDEX,強制MySQL使用指定的索引。
SELECT * FROM t1, t2 FORCE INDEX (index_for_column) WHERE t1.col_name=t2.col_name;
-
--max-seeks-for-key=1000
,告訴MySQL在超過1000個key查找之後再放棄使用key掃描。