1. 程式人生 > >一次MySQL線上慢查詢分析及索引使用

一次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欄位的資料離散型不是很好,但是滿足條件的資料很少,資料的篩選性還是很不錯的。

image

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會估算一下,兩者的代價來決定是否走索引查詢。

image

所以上面的sql在mysql 5.6之前執行過程:

  1. 通過recv_time條件在輔助索引搜尋,返回N條記錄

  2. 聚集索引查詢整行資料

  3. 返回到server 段然後再進行status欄位的條件篩選

  4. 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)索引的查詢要少很多。image

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容器的自動化監控實現