一次對group by時間致使的慢查詢的優化

前言:

最近在測試環境中點擊一個圖表展現頁面時,半天才獲得後臺響應的數據進行頁面渲染展現,後臺的響應很慢,這樣極大的下降了用戶的體驗;sql

發現這個問題後立刻進行了排查 ,經過排查發現是由一個查詢很慢的 group by 語句致使的;數據庫

本文主線:微信

①、簡單描述下排查步驟;函數

②、對 group by 查詢慢進行優化;工具

簡單描述下排查步驟:

排查主要分爲了兩個步驟:性能

  • 後臺接口的監控,看看哪一個方法調用時耗時多
  • 數據庫開啓慢查詢日誌,記錄執行很慢的SQL

推薦使用阿里開源的Java線上診斷工具 Arthas ,使用其 trace 命令統計方法調用鏈路上各個方法節點的耗時;學習

Arthas 工具的具體使用方法可參考: 線上服務響應時間太長的排查心路測試

經過使用Arthas工具統計到一個進行數據庫的 group by查詢 方法耗時很嚴重;優化

爲了進一步肯定是這個查詢SQL 很耗時,將MySql 的慢查詢日誌開啓了,而後再次調用後臺這個接口,發現慢查詢日誌中確實存在了這個SQL語句;日誌

SQL語句以下:

SELECT
	date_format(createts, '%Y') AS YEAR
FROM
	t_test_log
GROUP BY
	date_format(createts, '%Y')
ORDER BY
	createts DESC

這個SQL語句是用來統計表中全部數據被建立時的年份;

下面就來聊聊這個SQL爲何會比較慢,而後進行了怎樣的優化;

對 group by 查詢慢進行優化:

在優化group by查詢的時候,通常會想到下面這兩個名詞,經過下面這兩種索引掃描能夠高效快速的完成group by操做:

  • 鬆散索引掃描(Loose Index Scan)
  • 緊湊索引掃描(Tight Index Scan)

group by操做在沒有合適的索引可用時,一般先掃描整個表提取數據並建立一個臨時表,而後按照group by指定的列進行排序;在這個臨時表裏面,對於每個group 分組的數據行來講是連續在一塊兒的。

完成排序以後,就能夠獲得全部的groups 分組,並能夠執行彙集函數(aggregate function)。

能夠看到,在沒有使用索引的時候,須要建立臨時表和排序;那在執行計劃的 Extra 額外信息中一般就會看到這些信息 Using temporary; Using filesort 出現 。

一、首先查看下SQL的執行計劃:

獲得這個慢查詢的SQL後,立刻使用 explain 關鍵字分析其執行計劃:

經過查看執行計劃發現,這個SQL語句走的是 全表掃描 ,而且經過掃描了大概 99974 行記錄後才獲得最終的結果集,而且執行過程當中使用到了臨時表和文件輔助排序;

二、SQL執行計劃內容簡述:

查看執行計劃時,主要看上圖中花圈的那三項數據便可:

  • type:訪問類型,這是sql查詢優化中一個很重要的指標,結果值從好到壞依次是:

  • Rows:數據行,根據表統計信息及索引選用狀況,大體估算出找到所需的記錄所須要讀取的行數;

  • Extra:額外信息,SQL執行時十分重要的額外信息,簡單說幾個常會出現的值:

    • Using filesort : 未利用到索引的默認排序,須要使用文件輔助進行排序,出現其說明SQL性能很差;
    • Using temporary:使用臨時表保存中間結果,常見於 group by ,出現其說明SQL性能很差;
    • Using index: 說明能夠直接在索引樹上就能獲得最終的值,避免了回表,出現其說明SQL性能很好;
    • Using index for group-by:表示使用了 鬆散索引掃描 ,出現其說明SQL性能很好;由於鬆散索引掃描只須要讀取不多量的數據就能夠完成group by操做,因此執行效率很是高;
    • select tables optimized away: 在沒有group by子句的狀況下,基於索引優化 MIN/MAX 聚合函數操做,沒必要等到執行階段在進行計算,查詢執行計劃生成的階段便可完成優化,出現其說明SQL性能達到最優,每每配合 type訪問類型的system 出現;

三、創建索引後再查看執行計劃:

上面經過查看執行計劃得知,由於沒有建立相應的索引,因此走的是全表掃描,性能最差;而後對 createts 字段建立索引;再查看其執行計劃:

經過查看建立索引後的執行計劃發現,這次查詢走的 索引全掃描 ,這次雖然從全表掃描優化到了索引全掃描,可是仍是須要經過掃描了大概 99974 行記錄後才獲得最終的結果集,性能並無提高太多;

而且發現 Extra 信息中仍是存在 Using temporary; Using filesort ,說明沒有使用到 鬆散索引掃描或緊湊索引掃描

而後再次分析下SQL語句:

SELECT
	date_format(createts, '%Y') AS YEAR
FROM
	t_test_log
GROUP BY
	date_format(createts, '%Y')
ORDER BY
	createts DESC

發現SQL中對索引字段 createts 作了 date_format 函數運算,因此才致使沒使用上鬆散索引掃描或緊湊索引掃描;而後須要重寫下SQL 。

四、經過改寫SQL進行優化:

改寫後的SQL以下:

SELECT
	date_format(createts, '%Y') AS years
FROM
	(
		SELECT
			createts
		FROM
			t_test_log
		GROUP BY
			createts
	) t_test_log_1
GROUP BY
	date_format(createts, '%Y')
ORDER BY
	createts DESC

改寫完SQL後從新執行,發現查詢速度快了很是多,性能上有了質的飛躍;

而後又查看了下它的執行計劃以下:

查看上面那個嵌套查詢SQL語句的執行計劃,子查詢部分的經過掃描大概52行記錄就能獲得結果集,相比於一開始須要掃描 99974 行 記錄才能獲得結果集,這個性能快了太多了;而且子查詢的 Extra 信息中出現了 Using index for group-by ,說明使用到了鬆散索引掃描,效率才提高了這麼多;

外查詢對子查詢(52行記錄)的結果集再次進行分組排序,此時採用的是全表(全結果集)的查詢, 若是結果集很大的話,效率不會很高

因此,在使用此優化方案的SQL語句時,須要統計下子查詢的結果集的大小,若是子查詢結果集很大的話,就不建議使用此方案了,能夠嘗試使用下面的這種優化方案;

五、經過 改寫SQL + 改寫代碼 進行優化:

上面優化方案,只需改寫SQL便可,無需對代碼進行修改;本優化方案既要改寫SQL,還要進行代碼的修改;

改寫後的SQL以下: 這個SQL是查詢出表中最小年份和最大年份

(
	SELECT
		date_format(createts, '%Y') AS years
	FROM
		t_test_log
	ORDER BY
		createts
	LIMIT 1
)
UNION ALL
	(
		SELECT
			date_format(createts, '%Y') AS years
		FROM
			t_test_log
		ORDER BY
			createts DESC
		LIMIT 1
	)

查看下上面這個SQL語句的執行計劃:

上面這個SQL是利用索引的默認排序,直接獲取排序後的第一條記錄,只須要掃描一行記錄(rows :1)就能獲取到最終的結果集;因此此SQL的性能是很是好的 。

可是須要記住,這個SQL查詢出的結果集不是最終須要的數據,須要 寫代碼 計算出最終的結果集:

  • 獲得的最大最小年份這兩個值 同樣:說明表中的數據都是屬於一個年份的
  • 獲得的最大最小年份這兩個值不同:
    • 兩個值相減得一:說明年份是挨着的兩個年份,能夠直接將結果集返回;
    • 兩個值相減大於一:說明最小年份和最大年份之間還存在年份,經過計算得出中間年份

可是注意,經過寫代碼計算出最終的年份,這種方式仍是存在一個問題的,那就是確實表中根本沒有中間年份的數據,可是經過計算卻得出了;

舉例說明:假如經過SQL查詢出了最小年份和最大年份是2018和2021,那麼再經過代碼計算出中間年份2019和2020,可是表中數據根本就不存在2019年份的數據,這是就會出現問題了;

因此這種方案也須要根據本身具體的業務場景和實際的數據狀況等分析是否須要採用 。

擴展:

在經過 改寫SQL + 改寫代碼 進行優化時,改寫的SQL不止上面那一種,還有一種查詢效率也比較高的改寫SQL;

就是使用 min、max 聚合函數進行改寫SQL,可是在使用聚合函數時,能夠寫出下面兩種樣式的SQL,到底哪一種改寫SQL效率是比較高呢,留個懸念,你們能夠自行去分析嘗試下喲! 能夠在評論區留下你的答案呀!

第一種改寫SQL方式:

(
	SELECT
		min(date_format(createts, '%Y')) AS years
	FROM
		t_test_log
)
UNION ALL
  (
		SELECT
			max(date_format(createts, '%Y')) AS years
		FROM
			t_test_log
   )

第二種改寫SQL方式:

(
	SELECT
		date_format(minyear, '%Y') AS years
	FROM
		(
			SELECT
				min(createts) AS minyear
			FROM
				t_test_log
		) t_test_log_1
)
UNION ALL
   (
		SELECT
			date_format(maxyear, '%Y') AS years
		FROM
			(
				SELECT
					max(createts) AS maxyear
				FROM
					t_test_log
			) t_test_log_2
   )

♡ 點贊 + 評論 + 轉發 喲

若是本文對您有幫助的話,請揮動下您愛發財的小手點下贊呀,您的支持就是我不斷創做的動力,謝謝啦!

您能夠微信搜索【木子雷】公衆號,大量Java學習乾貨文章,您能夠來瞧一瞧喲!

相關文章
相關標籤/搜索