(請原諒我, 標題黨一回, 花幾分鐘看看, 或許對你有幫助)
背景
最近工做上遇到一個」神奇」的問題, 或許對你們有幫助, 所以造成本文.
問題大概是, 我有兩個表 TableA, TableB, 其中 TableA 表大概百萬行級別(存量業務數據), TableB 表幾行(新業務場景, 數據還未膨脹起來), 語義上 TableA.columnA = TableB.columnA, 其中 columnA 上創建了索引, 但查詢的時候確巨慢無比, 基本上到 5-6 秒, 明顯跟預期不符合.
下面我以一個具體的例子來講明吧, 模擬其中的 SQL 查詢場景.
場景重現
• user_info 表, 爲了場景儘可能簡單, 我只 mock 了其中的三列數據.
• user_score 表, 其中 uid 和 user_info.uid 語義一致.
• 其中數據狀況以下, 都是很常見的場景.
• 索引狀況是
• 查詢業務場景: 已知 user_score.id, 須要關聯查詢對應user_info的信息, (你們先忽略這個具體業務場景是否合理哈). 那麼對應的 SQL 很天然的以下:
請忽略其中的數據, 我剛開始 mock 了 100W, 而後又重複導入了兩遍, 所以數據有一些重複. 300W 數據, 最後查詢出來也是 1.18 秒. 按道理應該更快的. 老規矩 explain 看看啥狀況?
發現 user_info表沒用上索引, 全表掃描近 300W 數據? 現象是這樣, 爲何呢?
你不妨思考一下, 若是你遇到這種場景, 應該怎麼去排查?
(分割線, 花 10 秒想一想?)mysql
我當時也是」一頓操做猛如虎」, 然並卵? 嘗試了什麼多種 sql 寫法來完成這個操做. 好比更換Join表的順序(驅動表/被驅動表), 再好比用子查詢. 最終, 仍是沒有結果. 但直接單表查詢寫 SQL 確能用上索引.
問題解決
嘗試更換檢索條件, 好比更換 uid 直接關聯查詢, 索引仍然用不上, 差點放棄了都. 在準備求助 DBA 前, 看了下表的建表語句.
徹底有理由懷疑由於字符集不一致的問題致使索引失效的問題了.
因而修改了小表(真實線上環境可別亂操做)的字符集與大表一致, 再測試下.程序員
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.後端
說明: 本文測試場景基於 MySQL 5.6, 另外, 本文案例只是爲了說明問題, 其中的 SQL 並不規範(例如儘可能別用 select * 之類的), 請勿模仿(模仿了我也不負責 ). 爲了寫本文, 可花了很多時間, 建 DB, mock數據, 包括排版公衆號(啊,公衆號後臺對代碼格式仍是不友好, markdown 轉來代碼格式仍是有問題)等等, 若是以爲有用, 還望你幫忙"在看", "轉發". 最後留一個思考題供討論, 歡迎留言說出你的見解.
留一道思考題
你能解釋以下狀況嗎? 查詢結果表現爲什麼不一致? 注意一下 SQL 的執行順序, 查詢優化器工做流程, 以及其中的 Using join buffer (Block Nested Loop), 能夠多看看 MySQL 官方手冊 深刻了解背後的過程和原理.安全
打個廣告
阿里雲ECS彈性計算服務是阿里雲的最重要的雲服務產品之一。彈性計算服務是一種簡單高效,處理能力可彈性伸縮的計算服務。咱們始終致力於利用和創造業界最新的前沿技術,讓更多的客戶輕鬆享受這些技術紅利,在雲上快速構建更穩定、安全的應用,提高運維效率,下降IT成本,使客戶更專一於本身的核心業務創新。彈性計算從新定義了人們使用計算資源的方式,這一新的方式正在而且將一直影響着關於計算資源的生態和經濟圈。咱們正在創造歷史,咱們真誠地邀請您加入咱們的隊伍。
最近團隊釋放很多 HC, 誠招 P6/P7/P8 的同窗, 本組同窗主要招聘後端研發同窗(JD在此), 感興趣的同窗可掃描下面二維碼加我聯繫.
另外, 2021 屆校招/實習生崗位也正在進行中(詳情請戳), 若是你是 2020-11 — 2021-07 月之間畢業, 同時對阿里巴巴感興趣, 也歡迎聯繫我幫忙內推.
參考資料
• explain-extended 文檔
• mock數據生成器
• Block Nested-Loop and Batched Key Access Joins
點閱讀原文, 有相關連接, 若以爲有用, 請右下角點擊"在看", 幫忙轉發, 感謝. markdown
碼農唐磊
有收穫 ? 請四連 : 默默點贊
喜歡做者運維