本文属于《理解性能的奥秘——应用程序中慢,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 paramendFROM sys.dm_exec_query_stats qsCROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) estCROSS APPLY sys.dm_exec_text_query_plan(qs.plan_handle, qs.statement_start_offset, qs.statement_end_offset) qpCROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) epaWHERE 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 = 1THEN '-- ENCRYPTED'WHEN stmt_start >= 0THEN substring(sqltext, stmt_start + 1, CASE stmt_endWHEN 0THEN datalength(sqltext)ELSE stmt_end - stmt_start + 1END)END AS Statement,CASE WHEN paramend > paramstartTHEN CAST(substring(query_plan, paramstart, paramend - paramstart) AS XML)END AS paramsFROM 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
GOCREATE PROCEDURE List_orders_11 @fromdate DATETIME,@custid NCHAR(5)
AS
SELECT @fromdate = dateadd(YEAR, 2, @fromdate)SELECT *
FROM Orders
WHERE OrderDate > @fromdateAND CustomerID = @custidIF @custid = 'ALFKI'CREATE INDEX test ON Orders (ShipVia)SELECT *
FROM Orders
WHERE CustomerID = @custidAND OrderDate > @fromdateIF @custid = 'ALFKI'DROP INDEX testON Orders
GOSET ARITHABORT ONEXEC List_orders_11 '19980101','ALFKI'
GOSET ARITHABORT OFFEXEC 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 iON i.object_id = o.object_id
CROSS APPLY (SELECT ', ' + c.NAME + CASE ic.is_descending_keyWHEN 1THEN ' DESC'ELSE ''ENDFROM sys.index_columns icJOIN sys.columns cON ic.object_id = c.object_idAND ic.column_id = c.column_idWHERE ic.object_id = i.object_idAND ic.index_id = i.index_idAND ic.is_included_column = 0ORDER BY ic.key_ordinalFOR XML PATH('')) AS ikey(cols)
OUTER APPLY (SELECT ', ' + c.NAMEFROM sys.index_columns icJOIN sys.columns cON ic.object_id = c.object_idAND ic.column_id = c.column_idWHERE ic.object_id = i.object_idAND ic.index_id = i.index_idAND ic.is_included_column = 1ORDER BY ic.index_column_idFOR XML PATH('')) AS inc(cols)
WHERE o.NAME = @tblAND 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 sON s.object_id = o.object_id
CROSS APPLY (SELECT ', ' + c.NAMEFROM sys.stats_columns scJOIN sys.columns cON sc.object_id = c.object_idAND sc.column_id = c.column_idWHERE sc.object_id = s.object_idAND sc.stats_id = s.stats_idORDER BY sc.stats_column_idFOR 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)——统计信息基础 

小结:

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

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

  1. 《深入理解计算机系统》之浅析程序性能优化

    此文已由作者余笑天授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 本文主要是基于我之前学习<深入理解计算机系统>(以下简称CSAPP)这本书第五章优化程序性能内容 ...

  2. keil c语言 延迟程序,Keil C51程序设计中几种精确延时方法

    前几天时间在做一个基于51单片机开发板的等精度频率计,用LCD1602液晶显示的,晶振是22.1184MHZ,用得是测频率法,目的是想做到能够测试0--900KHZ的信号. 液晶显示部分花了我好几天才 ...

  3. razor imu 9dof的使用,先锋机器人rosaria 理解,配置STM32-ROS通信等疑难杂症(持续更新中)

    本文由 \color{blue}{\bf本文由} @DavidHan\color{red}{\it @David Han}出品,转载请注明出处\color{blue}{ \bf出品 ,转载请注明出处} ...

  4. c 语言如何处理表格文件中的数据库,C#程序从Excel表格中读取数据并进行处理

    今天做了一个Excel表格数据处理的事情,因为数据量表较大(接近7000条)所以处理起来有点麻烦,于是写了一个程序, 先将程序记下以便将来查找. using System; using System. ...

  5. .NET程序在Linux容器中的演变

    本文讲的是.NET程序在Linux容器中的演变[编者的话]Linux容器技术已被开发人员所熟知,现在.NET程序可以跑在Docker容器中,这为以Windows中心的开发人员带来了好处. [上海站|3 ...

  6. 哪个是python程序中与缩进有关的正确说法_关于Python程序中与“缩进”有关的说法中,以下选项中正确的是()。_学小易找答案...

    [单选题]白内障的主要症状 [多选题]白内障病人手术后护理重点观察是 [单选题]某女,21岁.排球比赛时与队员发生碰撞,后诉鼻梁疼痛来院就诊,检查鼻腔发现鼻中隔有一小血肿.正确处理方法是 [单选题]开 ...

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

    本文是<LoadRunner没有告诉你的>系列文章的第四篇,在这篇短文中,我将尽可能用简洁清晰的文字写下我对"性能"的看法,并澄清几个容易混淆的概念,帮助大家更好的理解 ...

  8. 对linux中多线程编程中pthread_join的理解

    对linux中多线程编程中pthread_join的理解 分类: 程序员面试 linux学习2013-08-04 21:32 234人阅读 评论(0) 收藏 举报 多线程linuxpthread_jo ...

  9. 计算机语言的比较英文论文,科学网—撰写高质量英文科技论文的心态、布局、程序和方法 - 岳中琦的博文...

    撰写高质量英文科技论文的心态.布局.程序和方法 岳中琦 高质量科技论文,是将我们自己的创新研究成果,在现在或将来被全世界人知道和应用.成为人类已知的知识和实践.因此,我们在撰写科技论文时要具备一种极其 ...

最新文章

  1. 《数学之美》第1章 文字和语言 vs 数字和信息
  2. 用一句JQuery代码实现表格的简单筛选
  3. sgu 207 Robbers
  4. leetcode 303. Range Sum Query - Immutable | 303. 区域和检索 - 数组不可变(一维前缀和问题)
  5. 开源项目管理软件禅道使用帮助下载
  6. qt 串口 环形缓存_qt linux串口 缓冲区多大
  7. 作者:杨波,山东农业大学硕士生。
  8. numpy的常用函数 不断更新
  9. 10天确定Python,运行环境(超详细步骤)
  10. 【Java从0到架构师】JS_jQuery_BootStrap
  11. [BUG] CS0234: 命名空间“System.Web.Mvc”中不存在类型或命名空间名称“Ajax”(是否缺少程序集引用?)...
  12. 草料二维码 批量生成二维码
  13. FireFox把Gmail变成大容量网络硬盘
  14. APP自动化测试-3. Appium元素定位与等待
  15. 抽象类和接口的区别(之二)
  16. VS2019番茄助手添加头文件防重复宏定义#ifn
  17. web前端开发主要培训哪些内容
  18. Bluetooth协议学习
  19. Kibana查询语言(KQL)AND、OR匹配,模糊匹配
  20. PTA 数组 7-5 按字母顺序排列出场国家名称

热门文章

  1. 软件接口测试工具篇(一)
  2. python基础“猜单词游戏”代码
  3. 百度联盟作弊诀窍——简单六招申请百度联盟秒过
  4. 云呐|加强实验室固定资产设备在线信息化管理
  5. 了解Swift:15个适合iOS应用开发的初学者友好资源
  6. python假设产品列表如下_Python.习题五 列表与元组(上)
  7. CodeForces - Feng Shui(半平面交)
  8. shui jisfnemskddp psijsjfsif
  9. 房价这么高,为什么租金却高不起来?
  10. PB函数大全(超全,解析很全面)