隨著業務變得複雜,一張 Table 的 Index 也會變多,此時 MySQL 會用哪個 Index 搜尋資料就成一個謎,例如 orders Table 有 (user_id, status) & (user_id, created_time) 兩個 Index,執行 SELECT * FROM orders WHERE user_id = 1 AND status BETWEEN 1 AND 3 AND created_time > NOW()
時究竟會用哪個 Index 呢?
執行 SQL 時,MySQL 會將語法解析成 Abstract Syntax Tree 後交由 Optimizer 制定 Query Plan 並決定要用哪個 Index,而 Optimizer 決策因素有:
首先 Index 篩選能力:
MySQL 會統計每個 Index 的 cardinality (基數),也就是該欄位中有多少種 Value,例如 orders 表中有 (status) & (user_id) Index,status 只有 處理中,失敗,完成 三種狀態,cardinality 就是 3,而 user_id 則可能有 1~10w 種,在相同查詢條件下,cardinality 越高篩選能力越好,所以說SELECT * FROM orders WHERE user_id = 1 AND status = 完成
的情況下,選用 (user_id) Index 效果更佳。
但只用 cardinality 會有誤判,因為真實資料會傾斜,例如 處理中 只是中間狀態,資料通常很少,因此 MySQL 會加上 histogram 統計出不同 Value 的佔比,假設在 user_id = 1 是一個大戶的情況下執行 SELECT * FROM orders WHERE user_id = 1 AND status = 處理中
,選用 (status) Index 篩選能力反而更好。
總 I/O 次數:
越少的 I/O 查詢效能一定越快,而 I/O 次數會受到 Index Tree 高度和是否要回 Clustered Index 影響,Index Tree 越高,Traversal 時就要載入越多節點 (Page) 到記憶體。
假設 orders Table 有 (user_id) & (user_id, status) 並執行下面查詢
`SELECT * FROM orders WHERE user_id = 123 AND status = 'PENDING'
依照篩選力 (user_id, status) 能篩選掉更多資料,但因為 (user_id, status) 一筆 Index Key 資料較多,等於一個 Page 能放的 Index Key 筆數變少,樹就會變高,因此在 I/O 次數上可能 (user_id) 更好。
不過更常見的情況是 orders Table (user_id) & (created_time, user_id) 並執行下面查詢SELECT user_id, created_time FROM orders WHERE user_id = 123 AND created_time > NOW()
雖然 (user_id) 能篩選掉更多資料,但因為 SELECT 需要 created_time 的欄位,使用 (user_id) Index 還需要回 Clustered Index 查到完整資料,但 (created_time, user_id) Index Tree 中就有 user_id & created_time 的欄位資料,不用回 Clustered Index,在 I/O 次數上 (created_time, user_id) 會更好。
最後是 CPU 消耗:
從 Index Tree 過濾完資料後,若資料還不精準,需要額外在記憶體中過濾,也就是說篩選力越好,對 CPU 的消耗就越小,但除了過濾資料 Order By 也可能消耗不少的 CPU。
例如 orders Table 有 (created_time) Index 並執行SELECT * FROM orders WHERE created_time > ? ORDER BY id
如果不用 created_time Index 先過濾資料,就必須把所有資料放進記憶體中一筆筆過濾,CPU 消耗肯定不小,但如果 orders 表不大且 created_time > ? 過濾不多資料時,由於 ORDER BY id 用 created_time index 過濾後,還要在記憶體中用 id 額外排序,此時 MySQL 會覺得用 primary key Index Tree CPU 消耗可能比較小,因為用 primary key Index Tree 把資料放到記憶體時,資料已是依照 id 排序,一筆筆比對只要 O(N),額外排序還要 O(nLogN),因此可能選用 primary key。
Optimizer 會透過 Index 統計數據依照上面三個因素計算出一個 cost 分數,最後選用 cost 最低的 Index Tree。
MySQL 有 EXPLAIN 功能,可以在不實際執行 Query 的情況下,查詢 Optimizer 制定的 Query Plan 細節,Explain 出來的內容有:
prossible_keys & key :Index 內容
ref: 與 index key 比較的資料類別
type :Index 的查詢方式
效能排序:system > const > eq_ref > ref > range > index > ALL
extra : 除了 Index Tree 搜尋外的其他操作
MMR 優化:
用 secondary index range 查詢且需要回 clustered index 找完整資料時,由於 secondary index 排序跟 primary key 排序不一樣,若依照 secondary index 排序回 clustered index 找資料會造成隨機 I/O,而 MMR 優化是將 secondary index 找到的資料 primary key 暫存到記憶體,排序後批次去 clustered index 查詢,將隨機 I/O 變成順序 I/O。
Index Merge 優化:
使用 OR 時,例如 SELECT * FROM t WHERe a = 1 OR b = 2
,會分別查詢 Index Tree 並把結果 Merge 起來並移除重複 primary key 資料,再用 Merge 的結果去 Clustered Index 找完整資料,降低去 Clustered Index 查找數量。
ICP 優化:
使用 index (a, b, c) 執行 SELECT * FROM t WHERE a = ? AND b > ? AND c = ?
時,由於 b 是範圍查詢 c 欄位無法用 binary search,此時因為 SELECT *
要回 clustered index 找完整資料,就會觸發 ICP,ICP 會在 index (a, b, c) tree 中一筆筆比對 c = ?
的資料,藉此過濾掉更多資料,減少要回 clustered index 查找的數量,降低 I/O 次數。
rows & filtered : Optimizer 根據 Index Tree 統計資料估數的查詢數量
key_len :預估使用到的 Index Key 長度,可用來判斷 Composite Index 是否所有欄位都用到
EXPLAIN 主要是看 Optimizer 透過統計資料制定的 Query Plan,如果你想看到實際 Query 的執行時間和 cost 分數,可以執行 EXPLAIN ANALYZE。
EXPLAIN ANALYZE 會實際執行 Query 並輸出:
EXPLAIN ANALYZE SELECT * FROM orders o JOIN users u ON o.user_id = u.id WHERE u.country = 'US';
-> Nested loop inner join (cost=1000.25 rows=5000)
(actual time=0.020..10.532 rows=100000 loops=1)
-> Filter: (u.country = 'US')
(cost=500.12 rows=5000)
(actual time=0.010..2.354 rows=100000 loops=1)
-> Table scan on u
(cost=500.12 rows=100000)
(actual time=0.009..1.500 rows=100000 loops=1)
-> Index lookup on o (user_id=u.id)
(cost=0.20 rows=1)
(actual time=0.000..0.050 rows=1 loops=100000)
由於 Optimizer 使用的統計數據是抽樣調查,難免會有誤差,此時可透過ANALYZE TABLE your_table;
重新抽樣,若發現光靠 cardinality 會不準,可透過
`ANALYZE TABLE your_table UPDATE HISTOGRAM ON column_name WITH 100 BUCKETS;
建立 histogram 統計數據。
如果還是不準,最後可試 ALTER TABLE your_table STATS_SAMPLE_PAGES=100;
調整抽樣數量,不過要注意 MySQL 執行抽樣統計是會消耗 CPU 的,尤其建立 Histogram 時是需要逐筆計算的,可能造成 Full Table Scan。
About Me
歡迎大家追蹤我的 Thread,平常會在上面分享技術文章
https://www.threads.com/@chill.vic.22