一、IO成本
mysql的innodb存儲引擎會把數據存儲到磁盤上,這時候無論怎么優化SQL,都是需要從磁盤中讀取數據到內存,就是IO成本,每次讀取磁盤,至少耗時0.01秒,至少讀一頁,innodb一個頁的數據存儲大小是16KB,這個磁盤的IO時間成本是1.0,這里的1.0沒有單位,就是個比較值。
二、CPU成本
從磁盤讀到數據后要放到內存中處理數據的過程,這是CPU成本。讀取后並且檢測可能的where條件,這個CPU的IO時間成本為0.2,這里的1.0和0.2被稱之為成本常數。
三、單表查詢成本計算步驟
3.1、根據搜索條件,找出所有可能使用的索引,也就是EXPLAIN的possible_keys。參考《mysql5.7版本的explain解析》
3.2、計算全表掃描的代價
3.3、計算使用不同索引執行查詢的代價。尤其是可能的索引為多個的時候
3.4、對比各種執行方案的代價,找出成本最低的哪一個
四、例子
4.1、有這樣一個表:
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key',
`username` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'user name',
`age` int(4) NOT NULL DEFAULT 20 COMMENT 'user age',
`birthday_date_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'user birthday',
`address` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
`remark` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'remark something',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create time',
`version` int(4) NOT NULL DEFAULT 0 COMMENT 'update version',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `idx_name`(`username`) USING BTREE,
INDEX `idx_age_remark`(`age`, `remark`) USING BTREE,
INDEX `idx_create_time`(`create_time`) USING BTREE,
INDEX `idx_address`(`address`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 10003 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = DYNAMIC;
4.3【表的頁數據、索引數據、行數等信息】
SHOW TABLE STATUS like 't_user'\G
Row_format: 數據格式。
Rows 表數據行數,MyISAM的這個數據行數統計是准確的,而InnoDB是估值是不准確的。這里t_user表的預估行數是9964行。
Data_length: 該表所占用空間的字節數。 1589248 bytes = 1552 kb
4.4【SQL執行成本分析】
現在我們需要根據“三、單表查詢成本計算步驟”來逐個計算成本:
4.4.1 第一步:計算全表掃描成本
因為mysql每一頁存儲數據大小是16KB,所以InnoDB占用了 1552 (t_user的Data_length值) / 16 = 97 頁磁盤來存儲數據。
磁盤IO 成本 = 97 頁 * 1.0 (磁盤IO成本) + 1.1(微調數) = 98.1
CPU 成本 = 9964 (t_user的Rows數據) * 0.2 + 1.0(微調數) = 1993.8
全表掃描成本 = 98.1 + 1993.8 = 2091.9
4.4.2 第二步:計算走索引的查詢成本——create_time索引列查詢成本
mysql規定,當讀取索引掃描的時候,每當讀取一個掃描區間或者范圍區間的IO成本,和讀取一個頁面的IO成本,是一樣的,都是1.0。根據4.1章節的表結構分析,我們可能用到的索引列是address和create_time,由於這兩個列都是普通索引,我們這里就選擇create_time索引的range去計算索引成本。那么其他where條件直接置為true:
SELECT * FROM t_user WHERE
address in ('shanghaishi', 'beijingshi') -- true
AND create_time > '2021-04-14 07:00:00'
AND create_time < '2021-04-16 07:00:00'
AND birthday_date_time > create_time -- true
AND remark like '%7%' -- true
AND version = 0; -- true
最后簡化成SQL:
SELECT * FROM t_user WHERE create_time > '2021-04-14 07:00:00' AND create_time < '2021-04-16 07:00:00';
【IO成本】也就是說掃描區間只有一個,所以磁盤IO成本 = 1(個掃描區間) * 1.0(磁盤IO成本) = 1.0
【CPU成本】那么在兩個給定的日期參數這一個區間之內,大概有多少記錄數呢?第一,mysql會計算一個頁面大概有多少記錄,第二mysql會計算這兩個區間參數大概隔了多少個頁面,然后使用上EXPLAIN里的rows值大概有幾條記錄,這里參考EXPLAIN的rows值是8條。 8 * 0.2 (cpu成本) + 0.01(微調成本) = 1.61
【回表IO成本】讀取完數據后,由於查詢的是*所以需要回表,回表涉及IO成本,mysql認為每回表一個聚簇索引,都需要回表一個頁面,一個頁面的IO成本是1.0, 參考EXPLAIN的rows結果是8條,8*1.0 = 8.0
【回表CPU成本】回表的CPU成本 8 * 0.2 = 1.6 這一步包含了Where條件中其他過濾條件的時間成本
【計算出最終的走索引查詢成本】 1.0 + 1.61 + 8.0 + 1.6 = 12.21
4.4.3 第三步:計算走索引的查詢成本——address索引列查詢成本
EXPLAIN format = json SELECT * FROM t_user WHERE address in ('shanghaishi', 'beijingshi');
【IO成本】由於這里 where address in ('shanghaishi', 'beijingshi') 有兩個參數,就代表有兩個掃描區間,也就意味着訪問 address 的IO成本是: 2 * 1.0 = 2.0
【CPU成本】使用上EXPLAIN里的rows值 291 * 0.2(cpu成本) + 0.01(微調成本) = 58.21
【回表IO成本】291 * 1.0 = 291
【回表CPU成本】291 * 0.2 = 58.2 這一步包含了Where條件中其他過濾條件的時間成本
【計算出最終的走索引查詢成本】 2.0 + 58.21 + 291 + 58.2 = 409.41
這里計算出的409.41可以使用EXPLAIN的json輸出格式進行驗證: EXPLAIN format = json SELECT * FROM t_user WHERE address in ('shanghaishi', 'beijingshi'); 可以看到query_cost確實是:409.41
上面的rows_examined_per_scan的291條 = 普通EXPLAIN視圖的rows 數一致。比較重要的是cost_info里的信息:prefix_cost = 409.41 = read_cost + eval_cost,data_read_per_json = 145K 是指連接查詢時的數據量
4.4.4 第四步:執行成本比較
根據之前三個步驟的執行成本計算結果進行比較:
全表掃描成本:2091.9
create_time索引列查詢成本:12.21
address索引列查詢成本: 409.41
所以,mysql認為使用create_time索引列執行SQL是時間成本最低的。
4.5 EXPLAIN驗證
根據4.4的執行成本結果分析,SQL應該實際走create_time列索引,最后我們用EXPLAIN來驗證下key列,你看,mysql決定走idx_create_time索引:
EXPLAIN
SELECT * FROM t_user
WHERE address in ('shanghaishi', 'beijingshi')
AND create_time > '2021-04-14 07:00:00'
AND create_time < '2021-04-16 07:00:00'
AND birthday_date_time > create_time
AND remark like '%7%'
AND version = 0;
五、多表連接查詢成本
多表連接查詢成本 = 一次驅動表成本 + 從驅動表查出的記錄數 * 一次被驅動表的成本
六、mysql執行成本詳細步驟展示
在上面第四步驟詳細講述了如何手動計算出各個索引列的執行成本以及全表掃描執行成本,但是這個過程太繁瑣了,mysql提供了更簡便直觀的optimizer_trace功能來詳細列出了各個步驟的執行成本明細。
set optimizer_trace = "enabled=on,one_line=off"; 首先開啟optimizer_trace的開關,默認是關閉的。這里的one_line是說結果集要不要一行輸出來給你看,默認off就行,畢竟很大的一個json結果集一行顯示出來肉眼很難觀察。
show variables like 'optimizer_trace'; 然后再驗證下是否打開了
執行一下以下這個query SQL,其中包含了3個索引:
SELECT * FROM mytest.t_user
WHERE address in ('shanghaishi', 'beijingshi') -- address 是普通索引列
AND create_time > '2021-04-14 07:00:00' -- create_time 也是普通索引列
AND create_time < '2021-04-16 07:00:00'
AND birthday_date_time > create_time
AND remark like '%7%' -- remark是一個組合索引的第二順序位索引:idx_age_remark(age,remark)
AND version = 0;
select * from information_schema.OPTIMIZER_TRACE; mysql使用optimizer_trace來記錄剛剛那個query sql,mysql是如何詳細計算出各個索引的執行成本的。輸出的json結果集比較大,分query和trace,query列出了select sql,trace列出了詳細成本計算步驟,只要分3個階段:1)join_preparation 准備階段 2)join_optimization 優化階段 3)join_execution 執行階段,我們關注的重點是優化階段,我這里挑優化階段的幾個重點的講:
substitute_generated_columns: 這是mysql為了方便計算什么值時臨時添加的列,不在table里保存。
table_dependencies: 分析表的依賴信息
【index dive】在兩個區間之間計算有多少條記錄的方式,在mysql中被稱為index dive。如果一個SQL 用了 IN (2萬個參數,或者一個子查詢SQL結果集非常多的),那么mysql很有可能認為走全表掃描更快。參考《mysql5.7決定SQL中IN條件是否走索引的成本計算,mysql中的index dive是在兩個區間之間計算有多少條記錄的方式》
【mrr】當mysql讀取一批二級索引時,會將根據這些二級索引拿到的主鍵id進行排好序,去回表到主鍵索引拿,這個優化過程由Mysql自行控制,我們無法干預,這就是MRR技術,多范圍查詢技術。當然,實現這個條件比較苛刻。
多表連接查詢參數:
rows_examined_per_scan: 321 表示從驅動表結果集預估有321條記錄會對被驅動表進行掃描
rows_produced_per_join: 321 如果這兩個數值一樣,表示filter =100 即過濾100%的數據,但是如果這個值比上一個值少,則可能使用了覆蓋索引等進行了優化,那么filter也會少於100%
filtered: 100
prefix_cost是總成本
七、執行成本計算優化:提前結束
比如abcd四個表進行多表連接查詢,當mysql計算出按照a,b,c,d順序計算出查詢成本是100時,又在計算按照a,c,b,d這個順序執行成本的過程中,發現僅僅ac這兩個表的連接成本110就已經超越了100,就會提前結束放棄對a,c,b,d順序的計算。多表連接成本計算的計算次數 = 幾個表的N次方,四個表就是16次成本計算: abcd, acbd, abdc 等等各種排列。
但是如果多表連接中,表個數太多,mysql也不會窮舉各種排列,mysql有一個表數量設定 show variables like 'Optimizer_search_depth'; -- 默認62張表。小於62張表的,窮舉算法,大於62張表的,按照62種算法計算。
八、mysql成本cost成本參數設定
mysql有兩種成本計算engine_cost引擎成本和server_cost服務端成本, show tables from mysql like '%cost%';
select * from mysql.server_cost; 查看服務端成本計算參數都有哪些,cost_value列為null值的表示使用的是mysql的默認值。
disk_temptable_create_cost 創建基於磁盤的臨時表成本,默認值40
disk_temptable_row_cost 往磁盤的臨時表里寫入或讀取一條數據的成本:1
key_compare_cost 兩條記錄做值比較的成本:0.1,排序操作時用到這個值
memory_temptable_create_cost 創建基於內存的臨時表成本,默認值2
memory_temptable_row_cost 往內存中的臨時表里寫入或讀取一條數據的成本:0.2
row_evaluate_cost 讀取一條記錄過濾where條件看是否滿足我們的搜索條件,默認成本: 0.2
備注:如果你想修改以上默認值,可以update它,如果想設置回默認值,只要設置為null即可。
select * from mysql.engine_cost; 存儲引擎的成本
engine_name = default 表示對於所有引擎全部適用,也可能指定存儲引擎比如innodb
device_type = 0 存儲引擎所用的設備類型,為了支持機械硬盤或者固態硬盤
cost_name = io_block_read_cost 從磁盤里讀取一個塊的成本,默認值1
cost_name = memory_block_read_cost 從內存里讀取一個塊的成本,默認值1。從磁盤里讀取和從內存中讀取成本一樣,是因為mysql不知道要讀取的數據是在磁盤中還是在內存中,所以mysql簡單的認為都是1。
備注:如果你修改了成本參數,可能會造成一定后果,比如把row_evaluate_cost調大,mysql會更傾向於使用索引,而不是全表掃描,比如你把memory_temptable_create_cost調的比disk_temptable_create_cost還大,mysql會更傾向於從磁盤中創建臨時表,而不是內存中。
九、mysql對於表和索引的統計
show tables from mysql like 'innodb%;
desc mysql.innodb_table_stats; 表統計。database_name 數據庫名;table_name 表名;last_update 最后更新時間;n_rows 行數;clustered_index_size 表聚簇索引占用頁面數量; sum_of_other_index_sizes 其他索引占用頁面數量; 其中n_rows 這個預估數是mysql用20頁(默認頁數可調整)的數據平均下來的采樣數量乘以多少個頁得出來的預估數量,不是精確數值。
desc mysql.innodb_index_stats; 索引統計。stat_name 索引名稱;stat_value 不重復數量;mysql默認在數據有10%的更新量下,會對索引統計進行更新。我們也可以手動更新索引統計: analyze table t_order; 對於t_order表進行手動索引統計更新。手動更新統計要少做,是一個對表的阻塞操作,盡量讓mysql默認執行就行。
end.