震驚! 阿里的程序員也不過如此,竟被一個簡單的 SQL 查詢難住

(請原諒我, 標題黨一回, 花幾分鐘看看, 或許對你有幫助)
背景
最近工做上遇到一個」神奇」的問題, 或許對你們有幫助, 所以造成本文.
問題大概是, 我有兩個表 TableA, TableB, 其中 TableA 表大概百萬行級別(存量業務數據), TableB 表幾行(新業務場景, 數據還未膨脹起來), 語義上 TableA.columnA = TableB.columnA, 其中 columnA 上創建了索引, 但查詢的時候確巨慢無比, 基本上到 5-6 秒, 明顯跟預期不符合.
下面我以一個具體的例子來講明吧, 模擬其中的 SQL 查詢場景.
場景重現
• user_info 表, 爲了場景儘可能簡單, 我只 mock 了其中的三列數據.
• user_score 表, 其中 uid 和 user_info.uid 語義一致.
震驚! 阿里的程序員也不過如此,竟被一個簡單的 SQL 查詢難住
• 其中數據狀況以下, 都是很常見的場景.
震驚! 阿里的程序員也不過如此,竟被一個簡單的 SQL 查詢難住
• 索引狀況是
震驚! 阿里的程序員也不過如此,竟被一個簡單的 SQL 查詢難住
• 查詢業務場景: 已知 user_score.id, 須要關聯查詢對應user_info的信息, (你們先忽略這個具體業務場景是否合理哈). 那麼對應的 SQL 很天然的以下:
震驚! 阿里的程序員也不過如此,竟被一個簡單的 SQL 查詢難住
請忽略其中的數據, 我剛開始 mock 了 100W, 而後又重複導入了兩遍, 所以數據有一些重複. 300W 數據, 最後查詢出來也是 1.18 秒. 按道理應該更快的. 老規矩 explain 看看啥狀況?
震驚! 阿里的程序員也不過如此,竟被一個簡單的 SQL 查詢難住
發現 user_info表沒用上索引, 全表掃描近 300W 數據? 現象是這樣, 爲何呢?
你不妨思考一下, 若是你遇到這種場景, 應該怎麼去排查?
震驚! 阿里的程序員也不過如此,竟被一個簡單的 SQL 查詢難住
(分割線, 花 10 秒想一想?)mysql


我當時也是」一頓操做猛如虎」, 然並卵? 嘗試了什麼多種 sql 寫法來完成這個操做. 好比更換Join表的順序(驅動表/被驅動表), 再好比用子查詢. 最終, 仍是沒有結果. 但直接單表查詢寫 SQL 確能用上索引.
震驚! 阿里的程序員也不過如此,竟被一個簡單的 SQL 查詢難住
問題解決
嘗試更換檢索條件, 好比更換 uid 直接關聯查詢, 索引仍然用不上, 差點放棄了都. 在準備求助 DBA 前, 看了下表的建表語句.
震驚! 阿里的程序員也不過如此,竟被一個簡單的 SQL 查詢難住
徹底有理由懷疑由於字符集不一致的問題致使索引失效的問題了.
因而修改了小表(真實線上環境可別亂操做)的字符集與大表一致, 再測試下.程序員

mysql> select * from user_score us
    -> inner join user_info ui on us.uid = ui.uid
    -> where us.id = 5;
+----+-----------+-------+---------+-----------+---------+
| id | uid       | score | id      | uid       | name    |
+----+-----------+-------+---------+-----------+---------+
|  5 | 111111111 |   100 |       1 | 111111111 | tanglei |
|  5 | 111111111 |   100 | 3685399 | 111111111 | tanglei |
|  5 | 111111111 |   100 | 3685400 | 111111111 | tanglei |
|  5 | 111111111 |   100 | 3685401 | 111111111 | tanglei |
|  5 | 111111111 |   100 | 3685402 | 111111111 | tanglei |
|  5 | 111111111 |   100 | 3685403 | 111111111 | tanglei |
+----+-----------+-------+---------+-----------+---------+
6 rows in set (0.00 sec)

mysql> explain
    -> select * from user_score us
    -> inner join user_info ui on us.uid = ui.uid
    -> where us.id = 5;
+----+-------------+-------+-------+-------------------+-----------+---------+-------+------+-------+
| id | select_type | table | type  | possible_keys     | key       | key_len | ref   | rows | Extra |
+----+-------------+-------+-------+-------------------+-----------+---------+-------+------+-------+
|  1 | SIMPLE      | us    | const | PRIMARY,index_uid | PRIMARY   | 4       | const |    1 | NULL  |
|  1 | SIMPLE      | ui    | ref   | index_uid         | index_uid | 194     | const |    6 | NULL  |
+----+-------------+-------+-------+-------------------+-----------+---------+-------+------+-------+
2 rows in set (0.00 sec)

果真 work 了.
挖掘根因
其實深究緣由, 就是網上各類 MySQL軍規/規約所提到的, 「索引列不要參與計算」. 此次這個 case, 若是知道 explain extended + show warnings 這個工具的話, (之前都不知道explain後面還能加 extended 參數), 可能就儘早」恍然大悟」了. (最新的 MySQL 8.0版本貌似不須要另外加這個關鍵字).
看下效果. (啊, 我還得把字符集改回去!!!)sql

mysql> explain extended select * from user_score us  inner join user_info ui on us.uid = ui.uid where us.id = 5;
+----+-------------+-------+-------+-------------------+---------+---------+-------+---------+----------+-------------+
| id | select_type | table | type  | possible_keys     | key     | key_len | ref   | rows    | filtered | Extra       |
+----+-------------+-------+-------+-------------------+---------+---------+-------+---------+----------+-------------+
|  1 | SIMPLE      | us    | const | PRIMARY,index_uid | PRIMARY | 4       | const |       1 |   100.00 | NULL        |
|  1 | SIMPLE      | ui    | ALL   | NULL              | NULL    | NULL    | NULL  | 2989934 |   100.00 | Using where |
+----+-------------+-------+-------+-------------------+---------+---------+-------+---------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)
mysql> show warnings;
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                                                                                                                                                                              |
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1003 | /* select#1 */ select '5' AS `id`,'111111111' AS `uid`,'100' AS `score`,`test`.`ui`.`id` AS `id`,`test`.`ui`.`uid` AS `uid`,`test`.`ui`.`name` AS `name` from `test`.`user_score` `us` join `test`.`user_info` `ui` where (('111111111' = convert(`test`.`ui`.`uid` using utf8mb4))) |
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

(滑動看右邊)
索引列參與計算了, 每次都要根據字符集去轉換, 全表掃描, 你說能快得起來麼?
至於這個問題爲何會發生? 綜合來看, 就是由於歷史緣由, 老業務場景中的原表是假 utf8, 新業務新表採用了真 utf8mb4.後端

  1. 考慮新表的時候, 忽略和原庫字符集的比較. 其實, 發現庫裏面的不一樣表可能都有不一樣的字符集, 不一樣人建的時候可能都依據我的喜愛去選擇了不一樣的字符集. 因而可知,開發規範有多重要.
  2. 雖然知道索引列不能參與計算, 但這個場景下都是相同的類型, varchar(64) 最終查詢過程當中仍然發生了類型轉換. 所以須要把字段字符集不一致等同於字段類型不一致.
  3. 若是這個 case, 利用 fail-fast 的理念的話, 發現不一致, 直接不讓 join 會不會更好? (就像 char v.s varchar 不能 join 同樣).

說明: 本文測試場景基於 MySQL 5.6, 另外, 本文案例只是爲了說明問題, 其中的 SQL 並不規範(例如儘可能別用 select * 之類的), 請勿模仿(模仿了我也不負責 ). 爲了寫本文, 可花了很多時間, 建 DB, mock數據, 包括排版公衆號(啊,公衆號後臺對代碼格式仍是不友好, markdown 轉來代碼格式仍是有問題)等等, 若是以爲有用, 還望你幫忙"在看", "轉發". 最後留一個思考題供討論, 歡迎留言說出你的見解.
留一道思考題
你能解釋以下狀況嗎? 查詢結果表現爲什麼不一致? 注意一下 SQL 的執行順序, 查詢優化器工做流程, 以及其中的 Using join buffer (Block Nested Loop), 能夠多看看 MySQL 官方手冊 深刻了解背後的過程和原理.震驚! 阿里的程序員也不過如此,竟被一個簡單的 SQL 查詢難住安全

打個廣告
阿里雲ECS彈性計算服務是阿里雲的最重要的雲服務產品之一。彈性計算服務是一種簡單高效,處理能力可彈性伸縮的計算服務。咱們始終致力於利用和創造業界最新的前沿技術,讓更多的客戶輕鬆享受這些技術紅利,在雲上快速構建更穩定、安全的應用,提高運維效率,下降IT成本,使客戶更專一於本身的核心業務創新。彈性計算從新定義了人們使用計算資源的方式,這一新的方式正在而且將一直影響着關於計算資源的生態和經濟圈。咱們正在創造歷史,咱們真誠地邀請您加入咱們的隊伍。
最近團隊釋放很多 HC, 誠招 P6/P7/P8 的同窗, 本組同窗主要招聘後端研發同窗(JD在此), 感興趣的同窗可掃描下面二維碼加我聯繫.
另外, 2021 屆校招/實習生崗位也正在進行中(詳情請戳), 若是你是 2020-11 — 2021-07 月之間畢業, 同時對阿里巴巴感興趣, 也歡迎聯繫我幫忙內推.
震驚! 阿里的程序員也不過如此,竟被一個簡單的 SQL 查詢難住
參考資料
• explain-extended 文檔
• mock數據生成器
• Block Nested-Loop and Batched Key Access Joins
點閱讀原文, 有相關連接, 若以爲有用, 請右下角點擊"在看", 幫忙轉發, 感謝. markdown

碼農唐磊
有收穫 ? 請四連 : 默默點贊
喜歡做者運維

相關文章
相關標籤/搜索