Azure SQL DB/DW 系列(7)——Query Store案例(4)——查找參數化問題

本文屬於Azure SQL DB/DW系列
上一文:Azure SQL DB/DW 系列(6)——Query Store案例(3)——查看等待信息
本文演示如何用Query Store查看參數化帶來的性能問題,但是跟前面3篇案例不同,這次使用純T-SQL而不是GUI。

前言

  Query Store提供了一個全新的方式用於協助數據庫用戶處理性能問題甚至其他故障,本文演示如何使用Query Store
查找由於參數原因導致的性能問題。我們除了可以使用Query Store查找問題之外,還可以在不修改程序的前提下通過強制計劃來控制查詢的運行。
  前面Azure SQL DB/DW 系列(5)——Query Store案例(2)——計劃迴歸其實已經演示過,如果參數出現問題,性能非常容易出現斷崖式下降。但是使用其他工具或者查詢語句,我們並不是輕易就能發現這類問題。對此我們可以使用從SQL Server 2016引入的Query Store來幫助我們查找這類問題。

什麼叫參數化問題

  當SQL Server接收一個新的查詢,會先編譯,然後在內存的計劃緩存中尋找是否有可用的執行計劃緩存,如果找到了,那往往會選擇重用這個計劃。同時查詢語句的本身也會以字符串的形式緩存。
  後續的每次查詢,如果文本相同,那麼執行計劃會繼續被重用。但是如果文本不變,僅參數變化,但是一旦查詢的文本稍作改動,那麼文本會被認爲不一樣,從而重新編譯和緩存,這種會造成過度編譯和緩存,從而影響性能。
  爲了解決這類問題,SQL Server會嘗試參數化這種查詢文本。這個可以在數據庫屬性中設置,默認值爲“簡單”,SQL Server可以判斷是否要參數化這類查詢,這樣部分但是並非全部查詢都會使用參數化。
  如果SQL Server決定參數化,那麼文本中的一些部分就會被替換成“參數”,然後進行緩存。後續的查詢如果文本相同,參數不同,也可以繼續重用執行計劃,從而減少可能的CPU和編譯時間。
  數據庫屬性中,還可以選擇“強制”,但是就像過去我常說的,可以選擇的選項都有適用場景,某些查詢被強制參數化之後,性能會變得非常差。當然,最好進行參數化的地方實際上是在程序端,使用類似sp_execute_sql等或者各種編程語言自帶的功能。
  當我們對不應參數化的查詢或批處理進行參數化時,問題就發生了。如果謂詞(通常是where)中使用的列的數據分佈特別偏斜,則某些查詢對謂詞的某些值會有一個可怕的執行計劃。
  如果值分佈均勻,則所有值都進展順利,但如果它們不是,則我們很可能有問題,因爲緩存的查詢計劃並不適合。有時,第一次使用查詢時,它使用非典型參數,以便存儲的計劃不適合大多數後續查詢。這可能導致查詢有時異常緩慢地運行。這種現象也叫參數嗅探問題。簡單來說,現象就是可能某個功能今天很快,明天突然很慢,而且一直都很慢。
在這裏插入圖片描述

環境搭建

  這次使用AdventureWorks2017,然後會用下面代碼來清理緩存:

use AdventureWorks2017
go
alter database scoped configuration clear procedure_cache

  然後借用網上一個很出名的建表文件,由於需要某些方式才能訪問,所以我已經下載完並傳到我的CSDN下載中:
make_big_adventure.sql下載地址
  接下來我們先看一下這兩個腳本的“預估執行計劃”,就是你不用真的執行,只需要點一下預估執行計劃/估計執行計劃即可,可以看到一樣的腳本,但是因爲我做了某些手腳,它們的執行計劃是不一樣的:

select * from bigproduct where listprice=245.01
option (recompile)
select * from bigproduct where listprice=0.00
option (recompile)

在這裏插入圖片描述
  爲什麼會這樣呢?因爲數據分佈的不一樣。可以看到相差了200倍:在這裏插入圖片描述
  使用option (recompile)是爲了讓SQL Server針對特定的值進行編譯,可以看出在分佈不均衡的環境下,不同的值是需要不同的執行計劃來支持的。那如果此時數據庫的配置中參數化是“強制”的,會是怎樣的呢?下面來看看:

use AdventureWorks2017
GO
ALTER database AdventureWorks2017 set parameterization forced;
go
alter database scoped configuration clear procedure_cache
go

  這次我們使用實際執行計劃,查看select * from bigproduct where listprice=245.01
在這裏插入圖片描述
  然後檢查:select * from bigproduct where listprice=0.00可以看到執行計劃是一樣的,而且注意執行計劃中的@0,這個意味着被參數化了,而上面的預估執行計劃是沒有的。
在這裏插入圖片描述
  接下來用下面命令清空緩存,然後再次執行select * from bigproduct where listprice=0.00,查看執行計劃,執行計劃又變了:

alter database scoped configuration clear procedure_cache
 go

在這裏插入圖片描述
  同樣查看另外一個查詢的執行計劃,執行計劃被重用了。
在這裏插入圖片描述
  這裏可以看出,SQL Server對參數化的查詢使用了計劃重用,這個計劃是第一個執行的語句的執行計劃,然後後續被不停重用。如果後續的參數導致不應該使用這個緩存的執行計劃,那麼性能將會非常不好。針對這類問題,我們首先得先“找到”他們,然後再進行處理。這次使用Query Store來實現。

Query Store查找參數化語句

  第一步要啓用Query Store:

USE master
GO
--啓用Query Store
ALTER DATABASE AdventureWorks2017 SET QUERY_STORE = ON
GO
--配置Query Store
ALTER DATABASE AdventureWorks2017 SET QUERY_STORE (OPERATION_MODE = READ_WRITE, CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 30),     DATA_FLUSH_INTERVAL_SECONDS = 300, INTERVAL_LENGTH_MINUTES = 10)
GO

  接下來會用這個命令來查詢:

-- 帶有最近一次的Plan ID和文本的參數化查詢

  select qsq.query_id,
  		max(qsqt.query_sql_text) query_sql_text,
  		max(qsp.plan_id) plan_id, 
  		max(qsrs.max_duration) max_duration,
  		max(qsrs.max_cpu_time) max_cpu_time,
  		min(qsrs.min_cpu_time) min_cpu_time,
  		min(qsrs.min_duration) min_duration,
  		max(qsrs.stdev_duration) stdev_duration,
  		max(qsrs.stdev_cpu_time) stdev_cpu_time
  from sys.query_store_query qsq, 
  		sys.query_store_query_text qsqt,
  		sys.query_store_plan qsp,
  		sys.query_store_runtime_stats qsrs
  where qsq.query_text_id= qsqt.query_text_id 
  	and qsp.query_id=qsq.query_id
  	and qsrs.plan_id=qsp.plan_id
  	and (qsq.query_parameterization_type<>0 
         or qsqt.query_sql_text like '%@%')
  	and qsq.is_internal_query=0
         and qsqt.query_sql_text not like '%sys.%' 
  	and qsqt.query_sql_text not like '%sys[ ].%'
         and qsqt.query_sql_text not like '%@[sys@].%'  escape '@'
         and qsqt.query_sql_text not like '%INFORMATION_SCHEMA%'
         and qsqt.query_sql_text not like '%msdb%' 
  	and qsqt.query_sql_text not like '%master%'
  	and qsp.last_execution_time=(select max(last_execution_time)
  					from sys.query_store_plan qsp2
  					where qsp2.query_id= qsp.query_id)
  group by qsq.query_id
  order by stdev_cpu_time desc

  然後我們開始做實驗,參數化問題會出現在adhoc或者存儲過程中,所以這兩部分都會測一下:

create procedure QueryPrice @p money
as
	select * from bigproduct where listprice=@p
go

  然後執行下面的命令,每個50次:

select * from bigproduct where listprice=245.01
go 50
select * from bigproduct where listprice=0.00
go 50
exec QueryPrice 245.01
go 50
exec QueryPrice 0
go 50

  藉助Query Store查看結果:
在這裏插入圖片描述
  我們可以看到通過Query Store可以找到這些參數化查詢語句。但是很遺憾,你不能用Query Store來解決這類問題,雖然可以強制執行計劃,但是因爲由於數據分佈的不同,執行計劃本身就不一樣一樣,所以不能強制所有查詢都使用同一個執行計劃。
  一般而言,對於參數化問題,可以考慮使用option (recompile),不過這種會導致每次執行都要編譯,CPU和時間開銷都比較大。因此最好的方式還是在程序端,並且完善數據庫設計使其儘量少地出現數據嚴重不均衡。

下一文:Azure SQL DB/DW 系列(8)——

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章