這周收到一個 sentry 報警,如下 SQL 查詢超時了。 select * from order_info where uid = 5837661 order by id asc limit 1 執(zhí)行show create table order_info 發(fā)現(xiàn)這個表其實是有加索引的
理論上執(zhí)行上述 SQL 會命中 idx_uid_stat 這個索引,但實際執(zhí)行 explain 查看 explain select * from order_info where uid = 5837661 order by id asc limit 1 可以看到它的 possible_keys(此 SQL 可能涉及到的索引) 是 idx_uid_stat,但實際上(key)用的卻是全表掃描 我們知道 MySQL 是基于成本來選擇是基于全表掃描還是選擇某個索引來執(zhí)行最終的執(zhí)行計劃的,所以看起來是全表掃描的成本小于基于 idx_uid_stat 索引執(zhí)行的成本,不過我的第一感覺很奇怪,這條 SQL 雖然是回表,但它的 limit 是 1,也就是說只選擇了滿足 uid = 5837661 中的其中一條語句,就算回表也只回一條記錄,這種成本幾乎可以忽略不計,優(yōu)化器怎么會選擇全表掃描呢。 當然懷疑歸懷疑,為了查看 MySQL 優(yōu)化器為啥選擇了全表掃描,我打開了 optimizer_trace 來一探究竟 畫外音:在MySQL 5.6 及之后的版本中,我們可以使用 optimizer trace 功能查看優(yōu)化器生成執(zhí)行計劃的整個過程 使用 optimizer_trace 的具體過程如下
MySQL 優(yōu)化器首先會計算出全表掃描的成本,然后選出該 SQL 可能涉及到到的所有索引并且計算索引的成本,然后選出所有成本最小的那個來執(zhí)行,來看下 optimizer trace 給出的關(guān)鍵信息 { 'rows_estimation': [ { 'table': '`rebate_order_info`', 'range_analysis': { 'table_scan': { 'rows': 21155996, 'cost': 4.45e6 // 全表掃描成本 } }, ... 'analyzing_range_alternatives': { 'range_scan_alternatives': [ { 'index': 'idx_uid_stat', 'ranges': [ '5837661 <= uid <= 5837661' ], 'index_dives_for_eq_ranges': true, 'rowid_ordered': false, 'using_mrr': false, 'index_only': false, 'rows': 255918, 'cost': 307103, // 使用idx_uid_stat索引的成本 'chosen': true } ], 'chosen_range_access_summary': { // 經(jīng)過上面的各個成本比較后選擇的最終結(jié)果 'range_access_plan': { 'type': 'range_scan', 'index': 'idx_uid_stat', // 可以看到最終選擇了idx_uid_stat這個索引來執(zhí)行 'rows': 255918, 'ranges': [ '58376617 <= uid <= 58376617' ] }, 'rows_for_plan': 255918, 'cost_for_plan': 307103, 'chosen': true } } ... 可以看到全表掃描的成本是 4.45e6,而選擇索引 idx_uid_stat 的成本是 307103,遠小于全表掃描的成本,而且從最終的選擇結(jié)果( 仔細再看了一下這個執(zhí)行計劃,果然發(fā)現(xiàn)了貓膩,執(zhí)行計劃中有一個
這個選擇表示由于排序的原因再進行了一次索引選擇優(yōu)化,由于我們的 SQL 使用了 id 排序(order by id asc limit 1),優(yōu)化器最終選擇了 PRIMARY 也就是全表掃描來執(zhí)行,也就是說這個選擇會無視之前的基于索引成本的選擇,為什么會有這樣的一個選項呢,主要原因如下:
從這段解釋可以看出主要原因是由于我們使用了 order by id asc 這種基于 id 的排序?qū)懛?,?yōu)化器認為排序是個昂貴的操作,所以為了避免排序,并且它認為 limit n 的 n 如果很小的話即使使用全表掃描也能很快執(zhí)行完,這樣使用全表掃描也就避免了 id 的排序(全表掃描其實也就是基于 id 主鍵的聚簇索引的掃描,本身就是基于 id 排好序的) 如果這個選擇是對的那也罷了,然而實際上這個優(yōu)化卻是有 bug 的!實際選擇 idx_uid_stat 執(zhí)行會快得多(只要 28 ms)!網(wǎng)上有不少人反饋這個問題,而且出現(xiàn)這個問題基本只與 SQL 中出現(xiàn) order by id asc limit n這種寫法有關(guān),如果 n 比較小很大概率會走全表掃描,如果 n 比較大則會選擇正確的索引。 這個 bug 最早追溯到 2014 年,不少人都呼吁官方及時修正這個bug,可能是實現(xiàn)比較困難,直到 MySQL 5.7,8.0 都還沒解決,所以在官方修復前我們要盡量避免這種寫法,那么怎么避免呢,主要有兩種方案
select * from order_info force index(idx_uid_stat) where uid = 5837661 order by id asc limit 1 這種寫法雖然可以,但不夠優(yōu)雅,如果這個索引被廢棄了咋辦?于是有了第二種比較優(yōu)雅的方案
這種方案也可以讓優(yōu)化器選擇正確的索引,更推薦!
|
|
來自: Fengsq501u81r4 > 《計算機》