12 誰最便宜就選誰 ----MySQL基於成本的優化
查詢成本由兩方面組成:
- IO成本
MyISAM、InnoDB存儲引擎都是將數據和索引都存儲到磁盤上的,從磁盤到內存這個加載的過程損耗的時間稱之爲I/O成本。 - CPU成本
讀取以及檢測記錄是否滿⾜對應的搜索條件、對結果集進⾏排序等這些操作損耗的時間稱之爲CPU成本。
設計MySQL的⼤叔規定讀取⼀個⻚⾯花費的成本默認是1.0,讀取以及檢測⼀條記錄是否符 合搜索條件的成本默認是0.2。1.0、0.2這些數字稱之爲成本常數。
單表查詢的成本
基於成本的優化步驟
- 根據搜索條件,找出所有可能使⽤的索引
- 計算全表掃描的代價
- 計算使⽤不同索引執⾏查詢的代價
- 對⽐各種執⾏⽅案的代價,找出成本最低的那⼀個
1.根據搜索條件,找出所有可能使用的索引:
通過對where條件和order的分析,可以查詢到所有使的索引
2.計算全表掃描的代價:
由於查詢成本=I/O成本+CPU成本,所以計算全表掃描的代價需要兩個信息:
- 聚簇索引佔⽤的⻚⾯數
- 該表中的記錄數
首先使用命令:SHOW TABLE STATUS LIKE 'single_table'\G
查詢table的查詢狀態
mysql> SHOW TABLE STATUS LIKE 'single_table'\G
*************************** 1. row ***************************
Name: single_table
Engine: InnoDB
Version: 10
Row_format: Dynamic
Rows: 9693
Avg_row_length: 163
Data_length: 1589248
Max_data_length: 0
Index_length: 2752512
Data_free: 4194304
Auto_increment: 10001
Create_time: 2018-12-10 13:37:23
Update_time: 2018-12-10 13:38:03
Check_time: NULL
Collation: utf8_general_ci
Checksum: NULL
Create_options:
Comment:
1 row inset (0.01 sec)
其中有兩個參數我們需要關注:
- rows : 在innodb引擎中這是一個估算值;估算有多少條記錄
- data_length : innodb表示聚簇索引佔用存儲空間的大小;
Data_length = 聚簇索引的⻚⾯數量 x 每個⻚⾯的⼤⼩
IO成本 = Data_length/每個頁面大小(默認16kb)+1.1(微調值)
CPU成本 = rows * 0.2 + 1.0(微調值)
全表掃描的代價 = Data_length/每個頁面大小(16kb)+1.1 + rows*0.2 + 1.0;
3. 計算使用不同索引查詢的代價
使用二級索引+回表查詢的方式主要依賴兩個參數:
- 索引範圍區間數量:無論某個範圍區間內二級索引佔用的頁面有多大,查詢優化器粗略的認爲讀取索引的一個區間範圍和讀取一個頁面的成本是相同的;即IO成本=1;如果是用in查詢;每個值爲一個區間;如果有n個值,那麼查詢的IO成本 = n * 1;
- 需要回表的記錄數:如果最左區間和最右區間的距離不是很大(沒有超過10個頁面)那麼查詢優化器可以精確的統計出索引之間的記錄數;如果超過10頁面;那麼在這10頁面取平均值,然後判斷整個區間的頁面數量;記錄數 = 索引頁面數量10頁面的平均值;CPU成本 = 記錄數0.2;
所以這裏的 二級索引的查詢成本 = 1 + 記錄數0.2;
根據二級索引返回的記錄數需要對聚簇索引進行回表查詢;
查詢的IO成本 = 記錄數1;
查詢的CPU成本 = 記錄數*0.2;
使用二級索引查詢成本 = 索引查詢成本 + 回表查詢成本 = n個區間 * 1 + 記錄數 * 0.2 + 記錄數 * 1 + 記錄數*0.2;
4. in 查詢在其中的特例
有時候使⽤索引執⾏查詢時會有許多單點區間,⽐如使⽤IN語句就很容易產⽣⾮常多的單點區間;這種通過直接訪問索引對應的B+樹來計算某個範圍區間對應的索引記錄條數的⽅ 式稱之爲index dive。少數的單點區間是沒有任何問題的;但是超過一定範圍之後:系統變量eq_range_index_dive_limit
mysql> SHOW VARIABLES LIKE '%dive%';
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| eq_range_index_dive_limit | 200 |
+---------------------------+-------+
1 row inset (0.08 sec)
查詢優化器就不會使用 index dive的方式了;接下來,我們看一下他採用的計算方式;
首先使用命令 SHOW INDEX FROM single_table
獲取索引中的參數Cardinality
(區分度;distinct的值);該值是估算出來的;同時,結合上面的 SHOW TABLE STATUS
中的rows,我們就可以估算出該索引重複的數據 = rows / Cardinality;結合in中的數據量 n 我們就可以估算出 查詢的記錄數 = n * 索引重複數;
聯表查詢的成本
兩表連接的成本分析(多表類似)
連接查詢總成本 = 單次訪問驅動表的成本 + 驅動表扇出數(記錄數) x 單次訪問被驅動表的成本
調節成本常數
我們前邊之介紹了兩個成本常數:
- 讀取⼀個⻚⾯花費的成本默認是1.0
- 檢測⼀條記錄是否符合搜索條件的成本默認是0.2
- 其實除了這兩個成本常數,MySQL還⽀持好多呢,它們被存儲到了mysql數據庫(這是⼀個系統數據庫,我們之前介紹過)的兩個表中:
mysql> SHOW TABLES FROM mysql LIKE '%cost%';
+--------------------------+
| Tables_in_mysql (%cost%) |
+--------------------------+
| engine_cost |
| server_cost |
+--------------------------+
2 rows inset (0.00 sec)
⼀條語句的執⾏其實是分爲兩層的:
- server層 (在server層進⾏連接管理、查詢緩存、語法解析、查詢優化等操作;關於這些操作對應的成本常數就存儲在了server_cost表中)
- 存儲引擎層(依賴於存儲引擎的⼀些操作對應的成本常數就存儲在了engine_cost表中。)
從server_cost中的內容可以看出來,⽬前在server層的⼀些操作對應的成本常數有以下⼏種:
成本常數名稱 | 默認值 | 描述 |
---|---|---|
disk_temptable_create_cost | 40.0 | 創建基於磁盤的臨時表的成本,如果增⼤這個 值的話會讓優化器儘量少的創建基於磁盤的臨 時表。 |
disk_temptable_row_cost | 1.0 | 向基於磁盤的臨時表寫⼊或讀取⼀條記錄的成 本,如果增⼤這個值的話會讓優化器儘量少的 創建基於磁盤的臨時表。 |
key_compare_cost | 0.1 | 兩條記錄做⽐較操作的成本,多⽤在排序操作上,如果增⼤這個值的話會提升filesort的成 本,讓優化器可能更傾向於使⽤索引完成排序 ⽽不是filesort。 |
memory_temptable_create_cost | 2.0 | 創建基於內存的臨時表的成本,如果增⼤這個 值的話會讓優化器儘量少的創建基於內存的臨 時表。 |
memory_temptable_row_cost | 0.2 | 向基於內存的臨時表寫⼊或讀取⼀條記錄的成 本,如果增⼤這個值的話會讓優化器儘量少的 創建基於內存的臨時表。 |
row_evaluate_cost | 0.2 | 這個就是我們之前⼀直使⽤的檢測⼀條記錄是 否符合搜索條件的成本,增⼤這個值可能讓優 化器更傾向於使⽤索引⽽不是直接全表掃描。 |