本文属于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)——