mysql5.7的sql優化
1.explain
(1).準備基礎資料(建立表,在c1欄位插入重複資料,並在c1欄位建立索引)
use testdb; create table t1_explain(id int,c1 char(20),c2 char(20),c3 char(20)); insert into t1_explain values(10,'a','b','c'); insert into t1_explain values(10,'a','b','c'); insert into t1_explain values(10,'a','b','c'); insert into t1_explain values(10,'a','b','c'); insert into t1_explain values(10,'a','b','c'); insert into t1_explain values(10,'a','b','c'); create index idx_c1 on t1_explain(c1);
(2).通過執行計劃看一下cost值的消耗(explain format=json)
mysql> explain format=json select * from t1_explain where c1='a'\G *************************** 1. row ***************************EXPLAIN: { "query_block": { "select_id": 1, "cost_info": { "query_cost": "1.10" }, "table": { "table_name": "t1_explain", "access_type": "ref", "possible_keys": [ "idx_c1" ], "key": "idx_c1", "used_key_parts": [ "c1" ], "key_length": "81", "ref": [ "const" ], "rows_examined_per_scan": 6, "rows_produced_per_join": 6, "filtered": "100.00", "index_condition": "(`testdb`.`t1_explain`.`c1` = 'a')", "cost_info": { "read_cost": "0.50", "eval_cost": "0.60", "prefix_cost": "1.10", "data_read_per_join": "1K" }, "used_columns": [ "id", "c1", "c2", "c3" ] } } } 1 row in set, 1 warning (0.00 sec) mysql>
(3).刪除索引,並通過執行計劃檢視cost值的消耗
mysql> ALTER TABLE `testdb`.`t1_explain` DROP INDEX `idx_c1` ; Query OK, 0 rows affected (0.28 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> mysql> explain format=json select * from t1_explain where c1='a'\G *************************** 1. row *************************** EXPLAIN: { "query_block": { "select_id": 1, "cost_info": { "query_cost": "0.85" }, "table": { "table_name": "t1_explain", "access_type": "ALL", "rows_examined_per_scan": 6, "rows_produced_per_join": 1, "filtered": "16.67", "cost_info": { "read_cost": "0.75", "eval_cost": "0.10", "prefix_cost": "0.85", "data_read_per_join": "248" }, "used_columns": [ "id", "c1", "c2", "c3" ], "attached_condition": "(`testdb`.`t1_explain`.`c1` = 'a')" } } } 1 row in set, 1 warning (0.01 sec) mysql>
兩次查詢的cost值不同,通過索引查詢的cost值比全表掃描的cost值大。這是因為當通過索引查詢時索引資料都是重複的(基數很低),所以要做一個索引全掃描;還因為“SELECT *”掃描完索引後要回表查詢id, c2,c3這幾個欄位。就好比你要讀完一本書,不會先把目錄全部讀一遍,然後再把後面的內容都讀一遍。
(4).如果將c1欄位的值改成不重複的,來看一下效果
重新寫入基礎資料
truncate table t1_explain; insert into t1_explain values(10,'a','b','c'); insert into t1_explain values(10,'b','b','c'); insert into t1_explain values(10,'c','b','c'); insert into t1_explain values(10,'d','b','c'); insert into t1_explain values(10,'e','b','c'); insert into t1_explain values(10,'f','b','c'); create index idx_c1 on t1_explain(c1);
檢視執行計劃
mysql> explain format=json select * from t1_explain where c1='a'\G *************************** 1. row *************************** EXPLAIN: { "query_block": { "select_id": 1, "cost_info": { "query_cost": "0.35" }, "table": { "table_name": "t1_explain", "access_type": "ref", "possible_keys": [ "idx_c1" ], "key": "idx_c1", "used_key_parts": [ "c1" ], "key_length": "81", "ref": [ "const" ], "rows_examined_per_scan": 1, "rows_produced_per_join": 1, "filtered": "100.00", "index_condition": "(`testdb`.`t1_explain`.`c1` = 'a')", "cost_info": { "read_cost": "0.25", "eval_cost": "0.10", "prefix_cost": "0.35", "data_read_per_join": "248" }, "used_columns": [ "id", "c1", "c2", "c3" ] } } } 1 row in set, 1 warning (0.00 sec) mysql>
刪除索引,並通過執行計劃檢視cost值的消耗
mysql> drop index idx_c1 on t1_explain; Query OK, 0 rows affected (0.24 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> explain format=json select * from t1_explain where c1='a'\G *************************** 1. row *************************** EXPLAIN: { "query_block": { "select_id": 1, "cost_info": { "query_cost": "0.85" }, "table": { "table_name": "t1_explain", "access_type": "ALL", "rows_examined_per_scan": 6, "rows_produced_per_join": 1, "filtered": "16.67", "cost_info": { "read_cost": "0.75", "eval_cost": "0.10", "prefix_cost": "0.85", "data_read_per_join": "248" }, "used_columns": [ "id", "c1", "c2", "c3" ], "attached_condition": "(`testdb`.`t1_explain`.`c1` = 'a')" } } } 1 row in set, 1 warning (0.00 sec) mysql>
這次c1欄位的值不重複(基數較高),則通過索引查詢的cost值比全表掃描的cost值小。
這裡可能沒有體現出選擇性,我們說基數高比較好,但是要有一個衡量目標。例如,某一欄位的基數是幾十萬條,但是表中資料有幾十億條,在這個欄位上建立索引就不是很合適,因為選擇性比較低,通過索引查詢在索引中可能就要掃描上億條資料。
通常在建立索引時要考慮以上內容(回表、基數、選擇性),在MySQL中可以通過系統表innodb_index_stats來檢視索引選擇性如何,並且可以看到組合索引中每一個欄位的選擇性如何,還可以計算索引的大小(單位:M)。
select stat_value as pages, index_name, stat_value * @@innodb_page_size / 1024 / 1024 as size from mysql.innodb_index_stats where table_name ='t1_explain' and database_name = 'testdb' and stat_name = 'size' and stat_description = 'Number of pages in the index' group by index_name;
如果是分割槽表,則使用下面的語句。
select stat_value as pages, index_name, sum(stat_value) * @@innodb_page_size / 1024 / 1024 as size from mysql.innodb_index_stats where table_name ='t#P%' and database_name = 'test' and stat_name = 'size' and stat_description = 'Number of pages in the index' group by index_name;
也可以通過show index from table_name 檢視Cardinality欄位的值,以及欄位的基數是多少。
2.MySQL中的優化特性
(1).Nested-Loop Join Algorithm(巢狀迴圈Join演算法)
最簡單的Join演算法及外迴圈讀取一行資料,根據關聯條件列到內迴圈中匹配關聯,在這種演算法中,我們通常稱外迴圈表為驅動表,稱內迴圈表為被驅動表。Nested-Loop Join演算法的虛擬碼如下:
for each row in t1 matching range{ for each row in t2 matching reference key { for each row in t3{ if row satisfies join conditions,send to client } } }
(2).Block Nested-Loop Join Algorithm(塊巢狀迴圈Join演算法,即BNL演算法)
BNL演算法是對Nested-Loop Join演算法的優化。具體做法是將外迴圈的行快取起來,讀取緩衝區中的行,減少內迴圈表被掃描的次數。例如,外迴圈表與內迴圈表均有100行記錄,普通的巢狀內迴圈表需要掃描100次,如果使用塊巢狀迴圈,則每次外迴圈讀取10行記錄到緩衝區中,然後把緩衝區資料傳遞給下一個內迴圈,將內迴圈讀取到的每行和緩衝區中的10行進行比較,這樣內迴圈表只需要掃描10次即可完成,使用塊巢狀迴圈後內迴圈整體掃描次數少了一個數量級。使用塊巢狀迴圈,內迴圈表掃描方式應是全表掃描,因為是內迴圈表匹配Join Buffer中的資料的。使用塊巢狀迴圈連線,MySQL會使用連線緩衝區(Join Buffer),且會遵循下面一些原則:
1.連線型別為ALL、index、range,會使用到Join Buffer。 2.Join Buffer是由join_buffer_size 變數控制的。 3.每次連線都使用一個Join Buffer,多表的連線可以使用多個Join Buffer。 4.Join Buffer只儲存與查詢操作相關的欄位資料,而不是整行記錄。
BNL演算法的虛擬碼如下:
for each row in t1 matching range{ for each row in t2 matching reference key { store used columns for 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 } } }
對上面的過程解釋如下:
①將t1、t2的連線結果放到緩衝區中,直到緩衝區滿為止。
②遍歷t3,與緩衝區內的資料匹配,找到匹配的行,傳送到客戶端。
③清空緩衝區。
④重複上面的步驟,直至緩衝區不滿。
⑤處理緩衝區中剩餘的資料,重複步驟②。
假設S是每次儲存t1、t2組合的大小,C是組合的數量,則t3被掃描的次數為:(S * C)/join_buffer_size+ 1。
由此可見,隨著join_buffer_size的增大,t3被掃描的次數會減少,如果join_buffer_size足夠大,大到可以容納所有t1和t2連線產生的資料,那麼t3只會被掃描一次。
(3).MySQL中的優化特性
1).Index Condition Pushdown(ICP,索引條件下推)
ICP是MySQL針對索引從表中檢索時的一種優化特性,在沒有ICP時處理過程如下圖所示:
①根據索引讀取一條索引記錄,然後使用索引的葉子節點中的主鍵值回表讀取整個錶行。
②判斷這行記錄是否符合where條件。
有ICP後處理過程如下圖所示:
①根據索引讀取一條索引記錄,但並不回表取出整行資料。
②判斷記錄是否滿足where條件的一部分,並且只能使用索引欄位進行檢查。如果不滿足條件,則繼續獲取下一條索引記錄。
③如果滿足條件,則使用索引回表取出整行資料。
④再判斷where條件的剩餘部分,選擇滿足條件的記錄。
ICP的意思就是篩選欄位在索引中的where條件從伺服器層下推到儲存引擎層,這樣可以在儲存引擎層過濾資料。由此可見,ICP可以減少儲存引擎訪問基表的次數和MySQL伺服器訪問儲存引擎的次數。
ICP的使用場景如下:
1.組合索引(a,b)where條件中的a欄位是範圍掃描,那麼後面的索引欄位b則無法使用到索引。在沒有ICP時需要把滿足a欄位條件的資料全部提取到伺服器層,並且會有大量的回表操作;而有了ICP之後,則會將b欄位條件下推到儲存引擎層,以減少回表次數和返回給伺服器層的資料量。 2.組合索引(a,b)的第一個欄位的選擇性非常低,第二個欄位查詢時又利用不到索引(%b%),在這種情況下,通過ICP也能很好地減少回表次數和返回給伺服器層的資料量。
ICP的使用限制如下:
1.只能用於InnoDB和MyISAM。 2.適用於range、ref、eq_ref和ref_or_null訪問方式,並且需要回表進行訪問。 3.適用於二級索引。 4.不適用於虛擬欄位的二級索引。
2).Multi-Range Read(MRR)
如果通過二級索引掃描時需要回表查詢資料,那麼此時由於主鍵順序與二級索引的順序不一致會導致大量的隨機I/O。而通過Multi-Range Read特性,MySQL會將索引掃描到的資料根據rowid進行一次排序,然後再回表查詢。此方式的好處是將回表查詢從隨機I/O轉換成順序I/O。
在沒有MRR時,通過索引查詢到資料之後回表形式如下圖所示:
從上圖中可以看到,當通過二級索引掃描完資料之後,根據rowid(或者主鍵)回表查詢,但是這個過程是隨機訪問的。如果表資料量非常大,在傳統的機械硬碟中IOPS不高的情況下效能會很差。
有了MRR之後,回表形式如下圖所示:
根據索引查詢完之後會將rowid放到緩衝區中進行排序,排序之後再回表訪問,此時是順序I/O。這裡排序所用到的緩衝區是由引數read_rnd_buffer_size所控制的。