最近工做上遇到一個「神奇」的問題,或許對你們有幫助,所以造成本文。mysql
圖片來自 Pexelssql
問題大概是,我有兩個表 TableA,TableB,其中 TableA 表大概百萬行級別(存量業務數據),TableB 表幾行(新業務場景,數據還未膨脹起來)。ide
語義上 TableA.columnA=TableB.columnA,其中 columnA 上創建了索引,但查詢的時候確巨慢無比,基本上到 5-6 秒,明顯跟預期不符合。工具
下面我以一個具體的例子來講明,模擬其中的 SQL 查詢場景。oop
場景重現測試
索引狀況以下圖:優化
查詢業務場景:已知 user_score.id,須要關聯查詢對應 user_info 的信息,(你們先忽略這個具體業務場景是否合理哈)。ui
那麼對應的 SQL 很天然的以下:
請忽略其中的數據,我剛開始 mock 了 100W,而後又重複導入了兩遍,所以數據有一些重複。spa
300W 數據,最後查詢出來也是 1.18 秒,按道理應該更快的,老規矩 explain 看看啥狀況?3d
發現 user_info 表沒用上索引,全表掃描近 300W 數據?現象是這樣,爲何呢?
你不妨思考一下,若是你遇到這種場景,應該怎麼去排查?
我當時也是「一頓操做猛如虎」,然並卵?嘗試了什麼多種 SQL 寫法來完成這個操做。
好比更換 Join 表的順序(驅動表/被驅動表),再好比用子查詢。最終,仍是沒有結果。但直接單表查詢寫 SQL 確能用上索引。
問題解決
在準備求助 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)
挖掘根因
此次這個 case,若是知道 explain extended+show warnings 這個工具的話,(之前都不知道 explain 後面還能加 extended 參數),可能就儘早「恍然大悟」了。(最新的 MySQL 8.0 版本貌似不須要另外加這個關鍵字)
看下效果:(啊,我還得把字符集改回去)
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。
①考慮新表的時候,忽略和原庫字符集的比較。其實,發現庫裏面的不一樣表可能都有不一樣的字符集,不一樣人建的時候可能都依據我的喜愛去選擇了不一樣的字符集。因而可知,開發規範有多重要。
②雖然知道索引列不能參與計算,但這個場景下都是相同的類型,varchar(64) 最終查詢過程當中仍然發生了類型轉換。所以須要把字段字符集不一致等同於字段類型不一致。
③若是這個 case,利用 fail-fast 的理念的話,發現不一致,直接不讓 join 會不會更好?(就像 char v.s varchar 不能 join 同樣)
說明:本文測試場景基於 MySQL 5.6,另外,本文案例只是爲了說明問題,其中的 SQL 並不規範(例如儘可能別用 select * 之類的),請勿模仿(模仿了我也不負責)。
最後留一個思考題供討論,歡迎留言說出你的見解。
你能解釋以下狀況嗎?查詢結果表現爲什麼不一致?注意一下 SQL 的執行順序,查詢優化器工做流程,以及其中的 Using join buffer(Block Nested Loop)。
能夠多看看 MySQL 官方手冊深刻了解背後的過程和原理:
https://dev.mysql.com/doc/refman/5.6/en/