一次MySQL線上慢查詢分析及索引使用
本文由作者鄭智輝授權網易雲社群釋出。
0.前言
本文通過分析線上MySQL慢查詢日誌,定位出現問題的SQL,進行業務場景分析,結合索引的相關使用進行資料庫優化。在兩次處理問題過程中,進行的思考。
1.簡要描述
在九月底某個新上的遊戲業務MySQL慢查詢日誌
# Time: 2017-09-30T14:56:13.974292+08:00 # Query_time: 6.048835 Lock_time: 0.000038 Rows_sent: 0 Rows_examined: 12884410SET timestamp=1506754573;SELECT status, sdkid, appid, app_orderid, matrix_orderid, pay_orderid, platform, sdk_version, app_channel, pay_channel, serverid, roleid, INET6_NTOA(userip), deviceid, devic e_name, productid, product_count, product_name, matrix_uid, app_uid, order_currency, order_price, activityid, create_time, expired_time, pay_method, pay_mode, ship_url, rese rved, pay_time, recv_time, ship_time, pay_sub_method, pay_amount, free_amount, pay_currency, pay_total_money, pay_free_money, credit, pay_fee, extra_columns, is_test FROM MatrixOrderSucc WHERE status >= 200 AND status < 300 AND recv_time < DATE_SUB(NOW(), interval 20 SECOND) AND recv_time > DATE_SUB(NOW(), interval 24 HOUR) ORDER BY retry LIMIT 1;
第一次處理方式:在該表上添加了(recv_time,status)索引,然後慢查詢沒有;
正當以為事情解決的時候,該遊戲10月份大推,然後資料量激增,然後慢查詢又出現了。
第二次處理方式:刪除之前的索引,然後改為對(status,recv_time)新增索引。然後至今該SQL未出現慢查詢了。
線上環境說明:
MySQL 5.7.18
表引擎為Innodb
系統核心:Debian 3.16.43-2
接下來說說這兩次處理過程中的測試和分析。
2.SQL分析
sql分析:
當時九月底時該表的資料達到1200w行,但是由於沒有匹配得上的索引,所以全表掃描耗時6秒多。
業務分析:
聯絡了開發同事,瞭解一下這個語句的業務場景。 該語句用於查詢失敗訂單(status標記)並且時間在20秒之前一天以內(recv_time)的資料。並得知其實滿足status條件的訂單其實只是少量的。
小結:
可以看出資料和固定時間範圍內的資料量有關係。10月份大推後,固定時間範圍內的資料激增。
3.第一次處理
3.1 資料情況
將資料導到測試環境進行了資料測試。
通過下圖的sql,資料基本分析如下:
* 滿足單獨status條件的資料大概就3w條 * 滿足單獨recv_time條件的資料大概是77w條 * 雖然status欄位的資料離散型不是很好,但是滿足條件的資料很少,資料的篩選性還是很不錯的。
3.2 測試
加了索引之後。(recv_time,status)
mysql> explain select status, sdkid, appid, app_orderid, matrix_orderid, pay_orderid, platform, sdk_version, app_channel, pay_channel, serverid, roleid, INET6_NTOA(userip), deviceid, device_name, productid, product_count, product_name, matrix_uid, app_uid, order_currency, order_price, activityid, create_time, expired_time, pay_method, pay_mode, ship_url, reserved, pay_time, recv_time, ship_time, pay_sub_method, pay_amount, free_amount, pay_currency, pay_total_money, pay_free_money, credit, pay_fee, extra_columns, is_test from MatrixOrderSucc WHERE status >= 200 AND status < 300 AND recv_time < DATE_SUB('2017-10-12 14:48:49', interval 20 SECOND) AND recv_time > DATE_SUB('2017-10-12-14:48:49', interval 24 HOUR); +----+-------------+-----------------+------------+-------+---------------+-----------+---------+------+---------+----------+-----------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------------+------------+-------+---------------+-----------+---------+------+---------+----------+-----------------------+ | 1 | SIMPLE | MatrixOrderSucc | NULL | range | recv_time | recv_time | 6 | NULL | 1606844 | 11.11 | Using index condition | +----+-------------+-----------------+------------+-------+---------------+-----------+---------+------+---------+----------+-----------------------+1 row in set, 1 warning (0.00 sec)
執行計劃:剛加上的索引確實被用上了。
正式環境臨時添加了該索引之後慢查詢確實消失了。
隱憂:
從執行計劃裡的key_len可以知道該sql,在進行資料篩選的時候只以recv_time進行資料過濾的,status欄位並沒有用上場。因為聯合索引左側欄位用了範圍查詢,則其他欄位無法用上。
背景知識 資料查詢過程:1. 如果走了輔助索引* 先去輔助索引查詢。返回索引欄位和主鍵欄位(index_column, pk column),假設資料N行,那麼這裡是N次的資料順序訪問* 再去聚集索引查詢整行資料:N次隨機訪問 資料搜尋代價:b+樹高度次隨機訪問+N次順序訪問+N次隨機訪問。 ps:當然如果輔助索引能覆蓋了SQL查詢的欄位,就不需要去主表查完整整行資料了。 2.如果直接全表掃描: 資料搜尋代價:全表總數次順序訪問 磁碟順序訪問和隨機訪問時間消耗大概查了兩個數量級。 所以有可能:MySQL會估算一下,兩者的代價來決定是否走索引查詢。
所以上面的sql在mysql 5.6之前執行過程:
通過recv_time條件在輔助索引搜尋,返回N條記錄
聚集索引查詢整行資料
返回到server 段然後再進行status欄位的條件篩選
server層返回資料給客戶端
然而,MySQL 5.6之後多了index condition push down的優化功能,就是能將索引篩選下推。
例如:
執行計劃裡的Using index condition是index push down的意思,是mysql 5.6後做的優化,
這個功能的效果就是,能將步驟3的資料篩選放在步驟2之前,因為既然從輔助索引取回的資料包含status欄位,那麼進行一下資料過濾,然後再去主表拿資料,就能減少隨機訪問的次數。
4.第二次處理
4.1 線上資料
10月遊戲大推每日資料激增。此時全表資料大概2800w。
再去通過explain 檢視執行計劃的時候,已經從原來的走索引,又變回了全表掃描。
慢查詢的時間從之前的6秒上升到18秒
4.2 問題
為什麼之前走索引現在會不走了?
有同事說:在應用層 force index強制走之前的索引就好了。因為可能是MySQL的優化器優化得不夠好。導致走了不良的執行計劃。 我認為:這個問題和應用問題和MySQL優化關係不大,是索引建得不對。如果在應用層做修改,第一需要經過測試迴歸才能釋出版本,耗時長;第二,force index 感覺比較死板,萬一以後表結構發生變更,這個索引不存在了,會存在問題。
線上資料分析:
單獨滿足recv_time條件的資料達到600多萬行。(因為遊戲大推,每日資料激增),原來只有77w行。
單獨滿足status條件的資料變化不大。
MySQL採用全表掃描的結論:
因為輔助索引返回的資料激增,導致主表隨機訪問的次數增加,發現還不如直接全表掃描來得快。
當然MySQL的SQL優化代價模型應該包含很多因素,後續有待研究。
4.3 測試
還是利用之前匯出的1200w的測試資料,對(status,recv_time)條件索引進行測試。
通過下圖可以看到:
查詢能走上索引,並且key_len=10,表明索引的兩列都派上用上了。
並且執行計劃裡的rows數量明顯比(recv_time,status)索引的查詢要少很多。
4.4 問題
4.4.1 上文不是聯合索引用了範圍查詢,第二列排不上用場嗎? 為什麼這裡能用recv_time搜尋資料?
我的理解:
1.status雖然在sql裡看起來是範圍查詢,但是MySQL能感知到status資料的離散程度,然後將status查詢改為IN(200),IN在MySQL裡不算範圍查詢。
2.其實這個挺好理解的。結合索引的B+樹的結構。 如果是IN,相當於在輔助索引裡通過第一列得出的是N個B+子樹(以第二索引欄位進行構建的子樹),那麼肯定還是可以對第二列進行二叉樹搜尋的。
所以關鍵就是在第一列搜尋完後,剩下的資料是否能對第二列recv_time進行二叉樹搜尋。
4.4.2 為什麼recv_time範圍查詢沒做上面的IN操作轉換?
因為recv_time真的是足夠離散。
4.5 索引選擇
在索引選擇,在有(recvtime,status) (status,recvtime) (status)三個索引下
KEY `status` (`status`,`recv_time`), KEY `status_2` (`status`), KEY `recv_time` (`recv_time`,`status`) mysql> explain SELECT count(*) FROM MatrixOrderSucc WHERE status >= 200 AND status < 300 AND recv_time < DATE_SUB('2017-10-12 14:48:49', interval 20 SECOND) AND recv_time > DATE_SUB('2017-10-12 14:48:49', interval 24 HOUR); +----+-------------+-----------------+------------+-------+---------------------------+--------+---------+------+-------+----------+--------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------------+------------+-------+---------------------------+--------+---------+------+-------+----------+--------------------------+ | 1 | SIMPLE | MatrixOrderSucc | NULL | range | status,status_2,recv_time | status | 10 | NULL | 58650 | 8.94 | Using where; Using index | +----+-------------+-----------------+------------+-------+---------------------------+--------+---------+------+-------+----------+--------------------------+1 row in set, 1 warning (0.00 sec)
可以看出系統選擇了(status,recv_time)索引。
因此在正式環境刪除之前的索引,建新的索引,慢查詢消失。
5.小結
5.1 不是離散性不好的欄位就不能加索引,也要看資料篩選效能
5.2 時間型別的欄位不大合適放在聯合索引的左邊
5.3 索引最左匹配原則 5.4 測試說明
5.4.1 資料是通過mysqldump不加鎖方式導到測試環境重新import建立的。
5.4.2 測試的SQL:最好不要選select count() from table ,因為在這個場景中select count() 會走索引掃描,是不必再到主表拿整行資料的;和實際場景的SQL是不一樣。
參考文件
更多網易技術、產品、運營經驗分享請訪問網易雲社群。
相關文章:
【推薦】 資料庫路由中介軟體MyCat - 使用篇(4)
【推薦】 Docker容器的自動化監控實現