理解性能的奥秘——应用程序中慢,SSMS中快(4)——收集解决参数嗅探问题的信息

本文属于《理解性能的奥秘——应用程序中慢,SSMS中快》系列


接上文:理解性能的奥秘——应用程序中慢,SSMS中快(3)——不总是参数嗅探的错

前面已经提到过关于存储过程在SSMS中运行很快,但在应用程序中运行很慢的可能原因:因为ARITHABORT的不同选项会导致不同的缓存词目,另外由于SQL Server使用了参数嗅探导致获得了不同的执行计划。

虽然已经说明了这个现象的原因,但是还没解释:如何定位和解决这个问题?到目前为止,大家都知道了如何快速处理,如果这个问题很紧急,可以直接使用:

EXEC sp_recompile 存储过程名

前面提到过,这个操作会刷新计划缓存。下次存储过程被调用时,会产生新的查询计划。如果通过这种方式可以解决,那么认为这个已经不是问题了。

但是如果问题依旧,那么就需要做更深入的研究,并且不适合再用sp_recompile或者类似的方式去修改存储过程。另外请一直打开显示执行计划的选项以便用于对比和检查,最起码可以获取参数的值(可以通过右键执行计划→【显示执行计划XML】的方式在XML格式的执行计划中搜索ParameterList的值)。这是本节的主题。

在开始本节之前,给个小建议:修改SSMS的默认值,以便因为ARITHABORT的默认值带来困惑。建议把这个值设为OFF。但是与应用程序相同设置确实会有一些小缺点:你可能观察不到与参数嗅探有关的性能问题。但是如果你已经养成了都校验ARITHABORT ON和OFF的结果,那么这个问题就不成问题了。

获取必要的事实:

所有的性能侦测都需要事实。如果没有事实,那么就想一首歌里面说的:

We‘ve been running round in our present state
Hoping help will come from above
But even angels there make the same mistakes

重点在:即使天使也会犯错。如果没有充足的理由,即使天使也帮不了你。下面五个为处理关于参数嗅探性能问题的基础事实:

  1. 哪个语句慢?
  2. 不同的查询计划是怎样的?
  3. SQL Server嗅探了哪些参数?
  4. 表和索引的定义是怎样的?
  5. 统计信息的分布情况如何?实时更新吗?

这些方面也几乎可以覆盖所有查询优化的方面。只有第三点是特别针对参数嗅探问题的。另外,注意“复数”,你需要看两个执行计划:好的和坏的。下面章节将会逐个说明。

哪个语句慢?

首先我们得找到慢查询,大部分情况下,问题代码属于单语句级别。如果存储过程仅有一个语句,那么这步可以忽略。否则,可以使用Profiler找出来:其中Duration列可以显示出来。

另外一个可选方案就是使用由Lee Tudor编写的存储过程:sqltrace (http://www.sommarskog.se/sqlutil/sqltrace.html),sqltrace使用SQL批语句作为参数,开始一个服务器级别的跟踪,运行这个批,然后停止跟踪,就可以得到所需的结果。里面也提供了很多选项可供输出。

在SSMS中获取查询计划和参数:

大部分情况下,在SSMS中运行存储过程可以很容易地得到查询计划,只要启用“包含实际执行计划”按钮即可。在不包含多个语句的情况下,这种方式很好用,但是在多语句的情况下,这种方式会显得很复杂。后面会演示替代方案。

一般情况下,你可能用下面这种方式运行:

SET ARITHABORT ON
go
EXEC that_very_sp 4711, 123, 1
go
SET ARITHABORT OFF
go
EXEC that_very_sp 4711, 123, 1

这里假设存储过程运行在默认配置上面。第一个执行可以得到好的查询计划——因为嗅探了参数并生成了执行计划,而第二次调用因为已经存在了执行计划在计划缓存中,所以依旧使用原有执行计划(这个计划不适合第二次调用)。关于获取缓存键,可以查看上一篇文章的相关内容:不同设置的查询计划  。

一旦你获得了执行计划,那么获得嗅探的参数值就容易了,右键查询计划最左边的操作符(一般是SELECT/INSERT等),然后选择【属性】,可以看到如下图的内容:

第一个【Parameter Compiled Values】是嗅探值,也就是引起问题的值。如果你了解你的应用程序和使用方式,可能马上从中知道问题。如果不是,最起码你可以知道程序调用存储过程时使用了这个可能有问题的值。

另外可以看到在缓存键中的不同的SET选项。但是注意在SQL 2005中的bug,但是这个跟SSMS的版本没关系。

虽然SSMS提供了很好的研究查询计划的接口,但是我还是建议使用一个更好的插件:SQL Sentry Plan Explorer     ,这个工具允许你从不同的方式查看查询计划,并且很容易地在大量批语句中定位。

当你研究一个查询计划时,不仅仅要研究开销的百分比,还要研究箭头的粗细。箭头越粗,传入到下一个操作符的数据量越大。实际上,我(作者)很少完全关注在操作符的开销百分比上。因为他们总是预估的,可能会偏离很远。

从计划缓存中直接获取查询计划和参数:

从实践来说,从SSMS中不会总是那么轻易就能获取到查询计划和嗅探值。特别是次优执行计划可能由于会运行很久导致你接受不了,或者存储过程由于包含了太多语句导致在SSMS中看起来很乱。更麻烦的是存储过程以循环方式执行,使得在SSMS中查看查询计划变得几乎不可能。

对此,其中一个方法是从计划 缓存中直接获取查询计划及其参数。这种方式很方便,但是也有一个明显的限制:你能获取的仅仅是预估执行计划。而实际影响行数和实际执行次数这两个用于分析为什么执行计划很差的重要因素却被丢失了。

查询语句:

下面这个语句用于返回计划缓存中存储过程的语句、嗅探值和查询计划:

DECLARE @dbname NVARCHAR(256),
	@procname NVARCHAR(256)

SELECT @dbname = ‘Northwind‘,
	@procname = ‘dbo.List_orders_11‘;

WITH basedata
AS (
	SELECT qs.statement_start_offset / 2 AS stmt_start,
		qs.statement_end_offset / 2 AS stmt_end,
		est.encrypted AS isencrypted,
		est.TEXT AS sqltext,
		epa.value AS set_options,
		qp.query_plan,
		charindex(‘<ParameterList>‘, qp.query_plan) + len(‘<ParameterList>‘) AS paramstart,
		charindex(‘</ParameterList>‘, qp.query_plan) AS paramend
	FROM sys.dm_exec_query_stats qs
	CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) est
	CROSS APPLY sys.dm_exec_text_query_plan(qs.plan_handle, qs.statement_start_offset, qs.statement_end_offset) qp
	CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) epa
	WHERE est.objectid = object_id(@procname)
		AND est.dbid = db_id(@dbname)
		AND epa.attribute = ‘set_options‘
	),
next_level
AS (
	SELECT stmt_start,
		set_options,
		query_plan,
		CASE
			WHEN isencrypted = 1
				THEN ‘-- ENCRYPTED‘
			WHEN stmt_start >= 0
				THEN substring(sqltext, stmt_start + 1, CASE stmt_end
							WHEN 0
								THEN datalength(sqltext)
							ELSE stmt_end - stmt_start + 1
							END)
			END AS Statement,
		CASE
			WHEN paramend > paramstart
				THEN CAST(substring(query_plan, paramstart, paramend - paramstart) AS XML)
			END AS params
	FROM basedata
	)
SELECT set_options AS [SET],
	n.stmt_start AS Pos,
	n.Statement,
	CR.c.value(‘@Column‘, ‘nvarchar(128)‘) AS Parameter,
	CR.c.value(‘@ParameterCompiledValue‘, ‘nvarchar(128)‘) AS [Sniffed Value],
	CAST(query_plan AS XML) AS [Query plan]
FROM next_level n
CROSS APPLY n.params.nodes(‘ColumnReference‘) AS CR(c)
ORDER BY n.set_options,
	n.stmt_start,
	Parameter

如果再次之前你从来没用过DMVs,估计对你而言会有点难懂。但是为了把注意力集中在我们的主题上。在这里需要提醒的是语句中需要制定数据库和存储过程名。

结果输出:

为了检查上面语句的输出情况,可以使用下面这个定制的存储过程:

USE Northwind
GO

CREATE PROCEDURE List_orders_11 @fromdate DATETIME,
	@custid NCHAR(5)
AS
SELECT @fromdate = dateadd(YEAR, 2, @fromdate)

SELECT *
FROM Orders
WHERE OrderDate > @fromdate
	AND CustomerID = @custid

IF @custid = ‘ALFKI‘
	CREATE INDEX test ON Orders (ShipVia)

SELECT *
FROM Orders
WHERE CustomerID = @custid
	AND OrderDate > @fromdate

IF @custid = ‘ALFKI‘
	DROP INDEX test
		ON Orders
GO

SET ARITHABORT ON

EXEC List_orders_11 ‘19980101‘,
	‘ALFKI‘
GO

SET ARITHABORT OFF

EXEC List_orders_11 ‘19970101‘,
	‘BERGS‘

然后运行上面的获取查询计划的语句,可以得到类似下面的效果:

其中里面有几列:

  • SET——表示查询计划中set_options属性。正如前面提到的,这是一个位掩码。上图中可以看到两个值:251 为默认设置,4347为默认设置+ARITHABORT ON。如果看到其他值,可以用作者编写的一个函数反编译:setoptions 。
  • Pos——表示该语句在存储过程的起始位置,从存储过程创建语句起算,包括任何CREATE PROCEDURE之前的注释。虽然大部分情况下作用不大,但是可以看到语句在存储过程中的出现顺序。
  • Statement——SQL语句,但是注意对于每个参数,都会重复一次。
  • Parameter——参数名,仅列出语句中出现的参数。也就是说,如果该查询语句没有使用到的参数,是不会出现在这里的。
  • Sniffed Value——在编译时的参数值,也就是在嗅探优化并产生查询计划的参数值。因为这是从计划缓存中获取的信息,所以这里的值并不是实际参数值,从上图中可以看到,对于不同的语句可能会出现不同的参数值。
  • Query Plan——对应的预估执行计划。

从Trace文件中获取查询计划和参数:

除了上面提到的内容之外,还可以从Trace文件中获取应用程序或SSMS中的查询计划。在Trace中,可以有几个Showplan事件可供选择。其中最通用的是Showplan XML Statistics Profile,可以提供在SSMS中获取实际执行计划的结果。

但是由于某些原因,使用跟踪几乎不是好的方式。因为这种跟踪会触发服务器的负载增加。因为trace的原理决定了,即使你指定了单独的spid,也需要触发所有进程产生所需的事件,然后再过滤,所以负载还是很高。

从SQL Server 2008 开始,引入了Extended Events,可以用于获取查询计划,这部分本人(译者)会在后续添加。原文中作者并未做介绍。

获取表和索引的定义:

这里假设读者已经知道如何获取表的定义,可以使用脚本或者sp_help获取,所以这里只介绍索引定义。可以使用脚本或者sp_helpindex获取,但是脚本很笨重,而sp_helpindex有不支持SQL 2005及后续版本加入的 新功能,那么下面这个语句可以很大程度帮到忙:

DECLARE @tbl NVARCHAR(265)

SELECT @tbl = ‘Orders‘

SELECT o.NAME,
	i.index_id,
	i.NAME,
	i.type_desc,
	substring(ikey.cols, 3, len(ikey.cols)) AS key_cols,
	substring(inc.cols, 3, len(inc.cols)) AS included_cols,
	stats_date(o.object_id, i.index_id) AS stats_date,
	i.filter_definition
FROM sys.objects o
JOIN sys.indexes i
	ON i.object_id = o.object_id
CROSS APPLY (
	SELECT ‘, ‘ + c.NAME + CASE ic.is_descending_key
			WHEN 1
				THEN ‘ DESC‘
			ELSE ‘‘
			END
	FROM sys.index_columns ic
	JOIN sys.columns c
		ON ic.object_id = c.object_id
			AND ic.column_id = c.column_id
	WHERE ic.object_id = i.object_id
		AND ic.index_id = i.index_id
		AND ic.is_included_column = 0
	ORDER BY ic.key_ordinal
	FOR XML PATH(‘‘)
	) AS ikey(cols)
OUTER APPLY (
	SELECT ‘, ‘ + c.NAME
	FROM sys.index_columns ic
	JOIN sys.columns c
		ON ic.object_id = c.object_id
			AND ic.column_id = c.column_id
	WHERE ic.object_id = i.object_id
		AND ic.index_id = i.index_id
		AND ic.is_included_column = 1
	ORDER BY ic.index_column_id
	FOR XML PATH(‘‘)
	) AS inc(cols)
WHERE o.NAME = @tbl
	AND i.type IN (
		1,
		2
		)
ORDER BY o.NAME,
	i.index_id

这个语句仅用于常规关系型索引,不适合XML索引和空间索引。

获取统计信息相关信息:

可以使用下面语句获取表上所有统计信息的情况:

DECLARE @tbl NVARCHAR(265)

SELECT @tbl = ‘Orders‘

SELECT o.NAME,
	s.stats_id,
	s.NAME,
	s.auto_created,
	s.user_created,
	substring(scols.cols, 3, len(scols.cols)) AS stat_cols,
	stats_date(o.object_id, s.stats_id) AS stats_date,
	s.filter_definition
FROM sys.objects o
JOIN sys.stats s
	ON s.object_id = o.object_id
CROSS APPLY (
	SELECT ‘, ‘ + c.NAME
	FROM sys.stats_columns sc
	JOIN sys.columns c
		ON sc.object_id = c.object_id
			AND sc.column_id = c.column_id
	WHERE sc.object_id = s.object_id
		AND sc.stats_id = s.stats_id
	ORDER BY sc.stats_column_id
	FOR XML PATH(‘‘)
	) AS scols(cols)
WHERE o.NAME = @tbl
ORDER BY o.NAME,
	s.stats_id

其中列stats_date返回统计信息最近更新时间。如果这个时间已经过去很久,那么统计信息可能已经过时。参数嗅探问题的根源通常不是统计信息过时,但是也应该检查一下。需要记住的是,统计信息的列如果是单调递增——如ID、date列,那么会很快过时,因为语句通常获取最近插入的数据,在统计信息的直方图中,记录的却通常是旧的数据。关于直方图会在后续介绍。

如果你认为统计信息过时,可以执行下面语句:

UPDATE STATISTICS 表名 WITH FULLSCAN, INDEX

这个语句通过完整扫描,更新表上所有的索引的统计信息。FULLSCAN并非必须,但是简单取样(默认值)的统计信息更新却经常会出现问题。限制统计信息在索引上的更新可以大幅度降低执行时间,因为SQL Server会对每个非聚集索引都扫描一次。

除了更新表上的全部索引,也可以用下面语句更新某个索引:

UPDATE STATISTICS tbl indexname WITH FULLSCAN

注意:在表名和索引名之间没有句号,只有空格。
如果是统计信息过时导致的性能问题,通常在更新统计信息之后,就可以发现应用程序的性能马上就得到提升。因为统计信息的更新会触发重编译,使得存储过程会对参数重新嗅探并产生更好的执行计划。

为了检查索引的统计信息情况,可以使用DBCC SHOW_STATITICS命令。这个命令需要两个参数,第一个是表名,第二个参数是索引或统计信息的名称,但是第二个参数也可以是列名,比如:

DBCC SHOW_STATISTICS (Orders, OrderDate)

下面是结果中第三个结果集的截图,这个是统计信息直方图的内容,直方图反映了表上数据的数据分布情况。

图中说明,表里面有一行数据,OrderDate等于1996-07-04。并且有一行数据属于1996-07-05到1996-07-07,有两行数据的OrderDate为1996-07-08。(因为RANGE_ROWS是1 而EQ_ROWS是2。)然后在Northwind库中的这个表的对应列的统计信息中,有188步长。直方图的步长永远不会超过200步。关于统计信息,可以查阅本人(译者)的文章:全废话SQL Server统计信息(2)——统计信息基础

小结:

本节主要演示了如何获取侦测性能问题的必要信息,下一节会介绍一些修复参数嗅探问题的例子。

时间: 2024-10-10 11:08:27

理解性能的奥秘——应用程序中慢,SSMS中快(4)——收集解决参数嗅探问题的信息的相关文章

理解性能的奥秘——应用程序中慢,SSMS中快(5)——案例:如何应对参数嗅探

本文属于<理解性能的奥秘--应用程序中慢,SSMS中快>系列 接上文:理解性能的奥秘--应用程序中慢,SSMS中快(4)--收集解决参数嗅探问题的信息 首先我们需要明白,参数嗅探本身不是问题,而是一个特性,避免SQL Server做出盲目的假设,从而产生次优查询计划.但是有些情况下,参数嗅探却会带来负面影响.通常有下面三种典型的情况: 查询使用的参数嗅探完全不合适.也就是说,查询计划对于这次执行是合适的,但是对于下一次执行就可能不合适. 应用程序中存在特定的调用模式,而且与其他大部分调用模式差

理解性能的奥秘——应用程序中慢,SSMS中快(3)——不总是参数嗅探的错

本文属于<理解性能的奥秘--应用程序中慢,SSMS中快>系列 接上文:理解性能的奥秘--应用程序中慢,SSMS中快(2)--SQL Server如何编译存储过程 在我们开始深入研究如何处理参数嗅探相关的性能问题之前,由于这个课题过于广泛,所以首先先介绍一些跟参数嗅探没有直接关系的内容,但是又会导致语句在SSMS和应用程序中存在性能差异的情况. 替换变量和参数: 前面已经接触过,但是在这里对其进行扩展.有时会看到论坛上有人说,某个存储过程很慢,但是把相同的语句提取出来单独执行就很快.真相就是:语

理解性能的奥秘——应用程序中慢,SSMS中快(6)——SQL Server如何编译动态SQL

本文属于<理解性能的奥秘--应用程序中慢,SSMS中快>系列 接上文:理解性能的奥秘--应用程序中慢,SSMS中快(5)--案例:如何应对参数嗅探 我们抛开参数嗅探的话题,回到了本系列的最初关注点中:为什么语句在应用程序中慢,但是在SSMS中快?到目前为止,都是在说存储过程的情况.而存储过程的问题通常是因为SET ARITHABORT的不同设置项的原因.如果你的应用不使用存储过程,而是通过中间层提交客户端的查询,那么也有几个原因可能让你的查询因为不同的缓存条目从而使得在SSMS和应用程序中的运

理解性能的奥秘——应用程序中慢,SSMS中快(2)——SQL Server如何编译存储过程

接上文:理解性能的奥秘--应用程序中慢,SSMS中快(1)--简介 本文介绍SQL Server如何编译存储过程并使用计划缓存.如果你的应用程序完全没有用到存储过程,而是直接使用SQL语句提交请求,那么本文大部分内容也是有效的.但是关于动态SQL的编译会在后面章节介绍,这里重点关注让人头痛的存储过程问题. 什么是存储过程? 虽然这个问题有点愚蠢,但是实际的问题是:什么对象有自己的查询计划?SQL Server为下面四类对象创建查询计划: 存储过程. 标量用户自定义函数. 多步表值函数. 触发器

理解性能提升By阿姆达尔定律(Amdahl‘s law)

我们在进行系统优化完成后,怎么评估优化的效果呢?最简单的方式是测量系统优化后耗时和优化前耗时的比例,这也叫加速比S(Speed Up).阿姆达尔定律在理解性能优化具有重要指导意义.优化前系统总耗时To(old),优化后系统总耗时Tn(new),加速比S=To/Tn.通过下面这张图理解:(α为待提速部分原来耗时比例) S = To/Tn = 1 / (1-α)+α/k当待优化部分提速无穷倍(k接近无穷大,不耗时间)时候,S = 1 / (1-α) , 也就是说,比如α=60%,如果系统中60%的部

《LoadRunner 没有告诉你的》之四——理解性能

本文是<LoadRunner没有告诉你的>系列文章的第四篇,在这篇短文中,我将尽可能用简洁清晰的文字写下我对“性能”的看法,并澄清几个容易混淆的概念,帮助大家更好的理解“性能”的含义. 如何评价性能的优劣: 用户视角 vs. 系统视角 对于最终用户(End-User)来说,评价系统的性能好坏只有一个字——“快”.最终用户并不需要关心系统当前的状态——即使系统这时正在处理着成千上万的请求,对于用户来说,由他所发出的这个请求是他唯一需要关心的,系统对用户请求的响应速度决定了用户对系统性能的评价.

YARN环境中应用程序JAR包冲突问题的分析及解决

Hadoop框架自身集成了很多第三方的JAR包库.Hadoop框架自身启动或者在运行用户的MapReduce等应用程序时,会优先查找Hadoop预置的JAR包.这样的话,当用户的应用程序使用的第三方库已经存在于Hadoop框架的预置目录,但是两者的版本不同时,Hadoop会优先为应用程序加载Hadoop自身预置的JAR包,这种情况的结果是往往会导致应用程序无法正常运行. 下面从我们在实践中遇到的一个实际问题出发,剖析Hadoop on YARN 环境下,MapReduce程序运行时JAR包查找的

cv::namedWindow, GLFWwindow以及其他程序嵌入到MFC中的教程

cv::namedWindow, GLFWwindow以及其他程序嵌入到MFC中的教程 MFC虽然很老, 不美观, 不跨平台, 但是在Windows系统中, 利用MFC做功能验证的界面, 还是很快很方便的. 因为它老, 所以有很多解决方案可以利用, 因为它是MS提供的界面库, 所以在Windows上很容易实现, 并且和Windows系统结合很紧密. 比如说, 窗口消息等, 在MFC中是很方便实现的. 基于上面的种种原因, 利用MFC作为功能验证的一个"壳" 是很好的工具. 当然, 难免

【SQL server初级】数据库性能优化三:程序操作优化

数据库优化包含以下三部分,数据库自身的优化,数据库表优化,程序操作优化.此文为第三部分 数据库性能优化三:程序操作优化 概述:程序访问优化也可以认为是访问SQL语句的优化,一个好的SQL语句是可以减少非常多的程序性能的,下面列出常用错误习惯,并且提出相应的解决方案 一.操作符优化 1. IN.NOT IN 操作符 IN和EXISTS 性能有外表和内表区分的,但是在大数据量的表中推荐用EXISTS 代替IN . Not IN 不走索引的是绝对不能用的,可以用NOT EXISTS 代替 2. IS