自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

這個 MySQL bug 99% 的人會踩坑!

運維 數(shù)據(jù)庫運維
我們知道 MySQL 是基于成本來選擇是基于全表掃描還是選擇某個索引來執(zhí)行最終的執(zhí)行計劃的。

 

這周收到一個 sentry 報警,如下 SQL 查詢超時了。

  1. select * from order_info where uid = 5837661 order by id asc limit 1 

執(zhí)行 show create table order_info 發(fā)現(xiàn)這個表其實是有加索引的

  1. CREATE TABLE `order_info` ( 
  2.  
  3. `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 
  4.  
  5. `uid` int(11) unsigned, 
  6.  
  7. `order_status` tinyint(3) DEFAULT NULL, 
  8.  
  9. ... 省略其它字段和索引 
  10.  
  11. PRIMARY KEY (`id`), 
  12.  
  13. KEY `idx_uid_stat` (`uid`,`order_status`), 
  14.  
  15. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 

理論上執(zhí)行上述 SQL 會命中 idx_uid_stat 這個索引,但實際執(zhí)行 explain 查看

  1. 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 的具體過程如下

  1. SET optimizer_trace="enabled=on"// 打開 optimizer_trace 
  2.  
  3. SELECT * FROM order_info where uid = 5837661 order by id asc limit 1 
  4.  
  5. SELECT * FROM information_schema.OPTIMIZER_TRACE; // 查看執(zhí)行計劃表 
  6.  
  7. SET optimizer_trace="enabled=off"// 關閉 optimizer_trace 

MySQL 優(yōu)化器首先會計算出全表掃描的成本,然后選出該 SQL 可能涉及到的所有索引并且計算索引的成本,然后選出所有成本最小的那個來執(zhí)行,來看下 optimizer trace 給出的關鍵信息

  1.  
  2. "rows_estimation": [ 
  3.  
  4.  
  5. "table""`rebate_order_info`"
  6.  
  7. "range_analysis": { 
  8.  
  9. "table_scan": { 
  10.  
  11. "rows"21155996
  12.  
  13. "cost"4.45e6 // 全表掃描成本 
  14.  
  15.  
  16. }, 
  17.  
  18. ... 
  19.  
  20. "analyzing_range_alternatives": { 
  21.  
  22. "range_scan_alternatives": [ 
  23.  
  24.  
  25. "index""idx_uid_stat"
  26.  
  27. "ranges": [ 
  28.  
  29. "5837661 <= uid <= 5837661" 
  30.  
  31. ], 
  32.  
  33. "index_dives_for_eq_ranges"true
  34.  
  35. "rowid_ordered"false
  36.  
  37. "using_mrr"false
  38.  
  39. "index_only"false
  40.  
  41. "rows"255918
  42.  
  43. "cost"307103// 使用idx_uid_stat索引的成本 
  44.  
  45. "chosen"true 
  46.  
  47.  
  48. ], 
  49.  
  50. "chosen_range_access_summary": { // 經(jīng)過上面的各個成本比較后選擇的最終結果 
  51.  
  52. "range_access_plan": { 
  53.  
  54. "type""range_scan"
  55.  
  56. "index""idx_uid_stat"// 可以看到最終選擇了idx_uid_stat這個索引來執(zhí)行 
  57.  
  58. "rows"255918
  59.  
  60. "ranges": [ 
  61.  
  62. "58376617 <= uid <= 58376617" 
  63.  
  64.  
  65. }, 
  66.  
  67. "rows_for_plan"255918
  68.  
  69. "cost_for_plan"307103
  70.  
  71. "chosen"true 
  72.  
  73.  
  74.  
  75. ... 

可以看到全表掃描的成本是 4.45e6,而選擇索引 idx_uid_stat 的成本是 307103,遠小于全表掃描的成本,而且從最終的選擇結果(chosen_range_access_summary)來看,確實也是選擇了 idx_uid_stat 這個索引,但為啥從 explain 看到的選擇是執(zhí)行 PRIMARY 也就是全表掃描呢,難道這個執(zhí)行計劃有誤?

仔細再看了一下這個執(zhí)行計劃,果然發(fā)現(xiàn)了貓膩,執(zhí)行計劃中有一個 reconsidering_access_paths_for_index_ordering 選擇引起了我的注意

  1.  
  2. "reconsidering_access_paths_for_index_ordering": { 
  3.  
  4. "clause""ORDER BY"
  5.  
  6. "index_order_summary": { 
  7.  
  8. "table""`rebate_order_info`"
  9.  
  10. "index_provides_order"true
  11.  
  12. "order_direction""asc"
  13.  
  14. "index""PRIMARY"// 可以看到選擇了主鍵索引 
  15.  
  16. "plan_changed"true
  17.  
  18. "access_type""index_scan" 
  19.  
  20.  
  21.  

這個選擇表示由于排序的原因再進行了一次索引選擇優(yōu)化,由于我們的 SQL 使用了 id 排序(order by id asc limit 1),優(yōu)化器最終選擇了 PRIMARY 也就是全表掃描來執(zhí)行,也就是說這個選擇會無視之前的基于索引成本的選擇,為什么會有這樣的一個選項呢,主要原因如下:

  1. The short explanation is that the optimizer thinks — or should I say hopes — that scanning the whole table (which is already sorted by the id field) will find the limited rows quick enough, and that this will avoid a sort operation. So by trying to avoid a sort, the optimizer ends-up losing time scanning the table. 

從這段解釋可以看出主要原因是由于我們使用了 order by id asc 這種基于 id 的排序寫法,優(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 這種寫法有關,如果 n 比較小很大概率會走全表掃描,如果 n 比較大則會選擇正確的索引。

這個 bug 最早追溯到 2014 年,不少人都呼吁官方及時修正這個bug,可能是實現(xiàn)比較困難,直到 MySQL 5.7,8.0 都還沒解決,所以在官方修復前我們要盡量避免這種寫法,如果一定要用這種寫法,怎么辦呢,主要有兩種方案

使用 force index 來強制使用指定的索引,如下:

  1. select * from order_info force index(idx_uid_stat) where uid = 5837661 order by id asc limit 1 

這種寫法雖然可以,但不夠優(yōu)雅,如果這個索引被廢棄了咋辦?于是有了第二種比較優(yōu)雅的方案

使用 order by (id+0) 方案,如下

  1. select * from order_info where uid = 5837661 order by (id+0) asc limit 1 

這種方案也可以讓優(yōu)化器選擇正確的索引,更推薦!為什么這個 trick 可以呢,因為此 SQL 雖然是按 id 排序的,但在 id 上作了加法這樣耗時的操作(雖然只是加個無用的 0,但足以騙過優(yōu)化器),優(yōu)化器認為此時基于全表掃描會更耗性能,于是會選擇基于成本大小的方式來選擇索引

巨人的肩膀

mysql 優(yōu)化器 bug http://4zsw5.cn/L1zEi

 

責任編輯:張燕妮 來源: 碼海
相關推薦

2021-10-15 06:49:37

MySQL

2025-04-14 09:31:03

2024-10-08 08:14:08

用戶生命周期分析服務

2022-07-15 08:20:54

Java基礎知識

2020-07-20 09:40:49

MySQLBUG數(shù)據(jù)庫

2020-04-02 14:33:42

MySQLBUG解決方案

2021-12-28 08:17:41

循環(huán) forgo

2022-10-31 18:38:24

MySQL數(shù)據(jù)訂單表

2024-09-29 09:27:10

2024-09-27 09:31:25

2025-04-29 08:30:00

迭代器失效C++編程

2024-11-26 08:20:53

程序數(shù)據(jù)歸檔庫

2018-01-10 13:40:03

數(shù)據(jù)庫MySQL表設計

2025-04-03 12:30:00

C 語言隱式類型轉換代碼

2021-07-29 10:39:50

MySQLMySQL5.7MySQL8

2023-01-18 23:20:25

編程開發(fā)

2020-09-15 08:46:26

Kubernetes探針服務端

2022-07-26 09:34:23

監(jiān)控系統(tǒng)

2023-02-20 08:11:04

2017-05-05 08:12:51

Spark共享變量
點贊
收藏

51CTO技術棧公眾號