很高興!終於踩到了慢查詢的坑

何甜甜在嗎發表於2018-10-21

之前看了餓了麼團隊寫的一篇部落格:等等!這兩個 Spring-RabbitMQ 的坑我們已經替你踩了。深受啟發,一定要取個能吸引讀者眼球的標題,當然除了響噹噹的標題以外,內容也要是乾貨。為什麼會想取這樣一個標題,因為看了理論上的慢查詢優化,今天!!!終於在生產上實戰了

(一)慢sql一

問題發現

將應用釋出到生產環境後,前端頁面請求後臺API返回資料,發現至少需要6s。檢視到慢sql:

慢sql定位.png

復現慢sql

執行sql:

select count(*) from sync_block_data
where unix_timestamp(sync_dt) >= 1539101010
AND unix_timestamp(sync_dt) <= 1539705810
複製程式碼

檢視耗時:

慢查詢耗時.png
一共耗時為2658ms 檢視執行計劃:

explain select count(*) from sync_block_data
where unix_timestamp(sync_dt) >= 1539101010
AND unix_timestamp(sync_dt) <= 1539705810
複製程式碼

執行計劃結果:

慢查詢執行計劃.png

優化慢sql一

sync_dt的型別為datetime型別。換另外一種sql寫法,直接通過比較日期而不是通過時間戳進行比較。將sql中的時間戳轉化為日期,分別為2018-10-10 00:03:30和2018-10-17 00:03:30 執行sql:

select count(*) from sync_block_data
where sync_dt >= "2018-10-10 00:03:30"
AND sync_dt <= "2018-10-17 00:03:30"
複製程式碼

檢視耗時:

快查詢耗時.png
一共耗時419毫秒,和慢查詢相比速度提升六倍多 檢視執行計劃:

explain select count(*) from sync_block_data
where sync_dt >= "2018-10-10 00:03:30"
AND sync_dt <= "2018-10-17 00:03:30"
複製程式碼

執行計劃結果:

快查詢執行計劃.png
訪問頁面,優化完成後請求時間平均為900毫秒
image.png

執行計劃中慢查詢和快查詢唯一的區別就是type不一樣:慢查詢中type為index,快查詢中type為range。

優化慢查詢二

這條sql的業務邏輯為統計出最近七天該表的資料量,可以去掉右邊的小於等於 執行sql:

select count(*) from sync_block_data
where sync_dt >= "2018-10-10 00:03:30"
複製程式碼

檢視耗時:

image.png
一共耗時275毫秒,又將查詢時間減少了一半 檢視執行計劃:

explain select count(*) from sync_block_data
where sync_dt >= "2018-10-10 00:03:30"
複製程式碼

執行計劃結果:

image.png
type仍是range。但是通過少比較一次將查詢速度提高一倍

優化慢查詢三

新建一個bigint型別欄位sync_dt_long儲存sync_dt的毫秒值,並在sync_dt_long欄位上建立索引 測試環境下: 優化慢查詢二sql

select count(*) from copy_sync_block_data
where sync_dt >="2018-10-10 13:15:02"
複製程式碼

耗時為34毫秒 優化慢查詢三sql

select count(*) from copy_sync_block_data
where sync_dt_long >= 1539148502916
複製程式碼

耗時為22毫秒 測試環境中速度提升10毫秒左右

優化慢查詢三sql測試小結:在InnoDB儲存引擎下,比較bigint的效率高於datetime 完成三步優化以後生產環境中請求耗時:

image.png
速度又快了200毫秒左右。通過給查詢的資料加10s快取,響應速度最快平均為20ms

explain使用介紹

通過explain,可以檢視sql語句的執行情況(比如查詢的表,使用的索引以及mysql在表中找到所需行的方式等) 用explain查詢mysql查詢計劃的輸出引數有:

列名 說明
id 執行編號,標識select所屬的行。如果在語句中沒子查詢或關聯查詢,只有唯一的select,每行都將顯示1。否則,內層的select語句一般會順序編號,對應於其在原始語句中的位置
select_type 顯示本行是簡單或複雜select。如果查詢有任何複雜的子查詢,則最外層標記為PRIMARY(DERIVED、UNION、UNION RESUlT)
table 訪問引用哪個表(引用某個查詢,如“derived3”)
type 資料訪問/讀取操作型別(ALL、index、range、ref、eq_ref、const/system、NULL)
possible_keys 揭示哪一些索引可能有利於高效的查詢
key 顯示mysql決定採用哪個索引來優化查詢
key_len 顯示mysql在索引裡使用的位元組數
ref 顯示了之前的表在key列記錄的索引中查詢值所用的列或常量
rows 為了找到所需的行而需要讀取的行數,估算值,不精確。通過把所有rows列值相乘,可粗略估算整個查詢會檢查的行數
Extra 額外資訊,如using index、filesort等

重點關注type,type型別的不同竟然導致效能差六倍!!!

很高興!終於踩到了慢查詢的坑

type顯示的是訪問型別,是較為重要的一個指標,結果值從好到壞依次是: system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL ,一般來說,得保證查詢至少達到range級別,最好能達到ref。

型別 說明
All 最壞的情況,全表掃描
index 和全表掃描一樣。只是掃描表的時候按照索引次序進行而不是行。主要優點就是避免了排序, 但是開銷仍然非常大。如在Extra列看到Using index,說明正在使用覆蓋索引,只掃描索引的資料,它比按索引次序全表掃描的開銷要小很多
range 範圍掃描,一個有限制的索引掃描。key 列顯示使用了哪個索引。當使用=、 <>、>、>=、<、<=、IS NULL、<=>、BETWEEN 或者 IN 操作符,用常量比較關鍵字列時,可以使用 range
ref 一種索引訪問,它返回所有匹配某個單個值的行。此類索引訪問只有當使用非唯一性索引或唯一性索引非唯一性字首時才會發生。這個型別跟eq_ref不同的是,它用在關聯操作只使用了索引的最左字首,或者索引不是UNIQUE和PRIMARY KEY。ref可以用於使用=或<=>操作符的帶索引的列。
eq_ref 最多隻返回一條符合條件的記錄。使用唯一性索引或主鍵查詢時會發生 (高效)
const 當確定最多隻會有一行匹配的時候,MySQL優化器會在查詢前讀取它而且只讀取一次,因此非常快。當主鍵放入where子句時,mysql把這個查詢轉為一個常量(高效)
system 這是const連線型別的一種特例,表僅有一行滿足條件。
Null 意味說mysql能在優化階段分解查詢語句,在執行階段甚至用不到訪問表或索引(高效)

出現慢查詢的原因

在where子句中使用了函式操作 出現慢查詢的sql語句中使用了unix_timestamp函式統計出自'1970-01-01 00:00:00'的到當前時間的秒數差。導致索引全掃描統計出近七天的資料量的

解決方案

儘量避免在where子句中對欄位進行函式操作,這將導致儲存引擎放棄使用索引而進行全表掃描。對於需要計算的值最好通過程式計算好傳入而不是在sql語句中做計算,比如這個sql中我們將當前的日期和七天前的日期計算好傳入

後記

這個問題當時在測試環境沒有發現,測試環境的請求速度還是可以的。沒有被發現可以歸結為資料量。生產資料量為百萬級別,測試環境資料量為萬級,資料量差50倍,資料量的增大把慢查詢的問題也放大了。

(二)慢sql二

因為線上出現了很明顯的請求響應慢的問題,又去看了專案中的其他sql,發現還有sql執行的效率比較低

復現慢sql

執行sql

select FROM_UNIXTIME(copyright_apply_time/1000,'%Y-%m-%d') point,count(1) nums
from resource_info where copyright_apply_time >= 1539336488355 and copyright_apply_time <= 1539941288355 group by point
複製程式碼

檢視耗時:

image.png
耗時為1123毫秒 檢視執行計劃:

explain select FROM_UNIXTIME(copyright_apply_time/1000,'%Y-%m-%d') point,count(1) nums
from resource_info where copyright_apply_time >= 1539336488355 and copyright_apply_time <= 1539941288355 group by point
複製程式碼

執行計劃結果:

image.png
索引是命中了,但是extra欄位中出現了Using temporary和Using filesort

優化慢sql一

group by實質是先排序後分組,也就是分組之前必排序。通過分組的時候禁止排序優化sql 執行sql:

select FROM_UNIXTIME(copyright_apply_time/1000,'%Y-%m-%d') point,count(1) nums
from resource_info where copyright_apply_time >= 1539336488355 and copyright_apply_time <= 1539941288355 group by point order by null
複製程式碼

檢視耗時:

image.png
一共耗時1068毫秒,提高100毫秒左右,效果並不是特別明顯 檢視執行計劃:
image.png
extra欄位已經沒有Using filesort了,filesort表示通過對返回資料進行排序。所有不是通過索引直接返回排序結果的排序都是FileSort排序,說明優化後通過索引直接返回排序結果 Using temporary依然存在,出現Using temporary表示查詢有使用臨時表, 一般出現於排序, 分組和多表join的情況, 查詢效率不高, 仍需要進行優化,這裡出現臨時表的原因是資料量過大使用了臨時表進行分組運算

優化慢sql二

慢查詢的sql業務邏輯為根據時間段分類統計出條件範圍內各個時間段的數量 比如給定的條件範圍為2018-10-20~2018-10-27的時間戳,這條sql就會統計出2018-10-20~2018-10-27每天的資料增量。現在優化成一天一天查,分別查七次資料,去掉分組操作

select FROM_UNIXTIME(copyright_apply_time/1000,'%Y-%m-%d') point,count(1) nums
from resource_info where copyright_apply_time >= 1539855067355 and copyright_apply_time <= 1539941467355 
複製程式碼

檢視耗時:

image.png
耗時為38毫秒,即使查7次所用時間也比1123毫秒少 檢視執行計劃:
image.png
extra欄位中和慢查詢的extra相比少了Using temporary和Using filesort。完美

就這樣第一次經歷了真正的慢查詢以及慢查詢優化,終於理論和實踐相結合了

相關文章