理解性能的奥秘——应用程序中慢,SSMS中快(2)——SQL Server如何编译存储过程
本文属于《理解性能的奥秘——应用程序中慢,SSMS中快》系列
接上文:理解性能的奥秘——应用程序中慢,SSMS中快(1)——简介
本文介绍SQL Server如何编译存储过程并使用计划缓存。如果你的应用程序完全没有用到存储过程,而是直接使用SQL语句提交请求,那么本文大部分内容也是有效的。但是关于动态SQL的编译会在后面章节介绍,这里重点关注让人头痛的存储过程问题。
什么是存储过程?
- 存储过程。
- 标量用户自定义函数。
- 多步表值函数。
- 触发器
SELECT abc, def FROM myview SELECT a, b, c FROM mytablefunc(9)
CREATE PROCECURE Outer_sp AS ... EXEC Inner_sp ...
CREATE PROCEDURE Some_sp AS
DECLARE @sql nvarchar(MAX),
@params nvarchar(MAX)
SELECT @sql = 'SELECT ...'
...
EXEC sp_executesql @sql, @params, @par1, ...
SQL Server如何生成查询计划:
概述:
参数和变量:
USE Northwind GO CREATE PROCEDURE List_orders_1 AS SELECT * FROM Orders WHERE OrderDate > '20000101' GO CREATE PROCEDURE List_orders_2 @fromdate DATETIME AS SELECT * FROM Orders WHERE OrderDate > @fromdate GO CREATE PROCEDURE List_orders_3 @fromdate DATETIME AS DECLARE @fromdate_copy DATETIME SELECT @fromdate_copy = @fromdate SELECT * FROM Orders WHERE OrderDate > @fromdate_copy GO
EXEC List_orders_1 EXEC List_orders_2 '20000101' EXEC List_orders_3 '20000101'
前两个存储过程的执行计划如上图“查询2”所示,使用了索引查找操作,而第三个存储过程使用的是聚集索引扫描操作,如上图“查询3”所示。此时SQL Server进行了全表扫描(注意,聚集索引的叶节点存储了数据本身,索引聚集索引扫描和表扫描是一样的),为什么会发生这种情况呢?为了明白优化器的决定,一般做法是检查影响的预估行数,如果把鼠标移到查找和扫描两个操作符上,可以看到下面两个结果:
其中值得关注的是“预估影响行数”(Estimated Number of Rows)。对于前两个存储过程,SQL Server预估只会返回1行数据,但是对于第三个存储过程,预估返回249行。这个差异导致了优化器选择不同的执行计划。对于一个表返回少量数据而言,索引查找+键值查找(Key Lookup)是很好的策略,但是对于相对较大规模的返回结果而言,开销就会增加,此时SQL Server底层更愿意使用直接扫描一次全表来降低开销。对于返回表的大部分甚至全部行而言,表扫描/聚集索引扫描会更加高效。因为此时SQL Server只需要访问一次表数据,而查找加键值查找,数据页中的每一行都会触发一次访问。在Northwind数据库中的Orders表,有830行数据,而SQL Server预估要返回249行,所以它觉得扫描是最佳选择。
预估数据的出处?
前面说了为什么会出现不同的执行计划:因为预估数量的不同。但是这又仅仅引出了下一个问题:为什么会不同呢?这也是本系列的关键点之一。对于第一个存储过程,日期是常量,意味着SQL Server只需要考虑这个值的情况。通过查询Orders表的统计信息,即可知道预估行数。但是统计信息毕竟是表的取样数据,所以SQL Server不能确定查询是否有数据返回,所以它认为只返回1行数据。对于第二个存储过程,查询使用了一个变量,更准确地说是一个参数。当优化器进行优化时,SQL Server知道这个存储过程会传入2000-01-01这个值。但是并不知道后续操作,所以它也不确定后续是否真的会用这个参数值。尽管如此,优化器根据输入值计算出对应的预估行数,也和第一个存储过程一样,只有1行,这种策略叫做参数嗅探(parameter sniffing)。对于第三个存储过程,就有所不同了,输入值被复制到一个本地变量,但是在SQL Server产生执行计划时,它真不知道这个值最终会是怎么样子的,所以使用一个标准假设,也就是假设会有30%的命中率,830行的30%就是249行。针对这种情况,有一个变种情景,如下面的第四个存储过程:
USE Northwind GO CREATE PROCEDURE List_orders_4 @fromdate DATETIME = NULL AS IF @fromdate IS NULL SELECT @fromdate = '19900101' SELECT * FROM Orders WHERE OrderDate > @fromdate
在这个存储过程中,参数是可选的,如果不传参数,默认就是null,即返回所有订单数,也就是等价于EXEC List_orders_4,它的执行计划和第一、第二个存储过程一样。使用索引查找+键值查找,尽管返回所有订单数据。如果你查看索引查找操作符的属性,可以看到除了“实际影响行数”(actual number of rows)之外其他都和第二个存储过程一样。在编译这个存储过程时,SQL Server并不知道@fromdate值的改变,假设@fromdate值为NULL。因为在关系数据库中,NULL代表着“未知”,也就是说,如果@fromdate在运行过程中依旧是这个值的话,查询可能返回任意数据。如果SQL Server把这值作为最后的输入,那么它会构造一个只有常量的扫描操作而完全不需要访问表(可以使用SELECT * FROM Orders WHERE OrderDate > NULL 来检验)。但是SQL Server又不得不生成一个满足不管@fromdate在运行时传入什么值都能返回正确结果的执行计划。另外一方面,SQL Server没有义务创建一个对所有值都是最佳的执行计划,所以它假设没有任意值返回,SQL Server因此决定使用索引查找。(但是依旧是返回1行,因为SQL Server永远不会使用0行作为预估行数)这是一个关于参数嗅探的反效果的例子,在实践中,更加合理的方式已更改是改写成下面第五个存储过程的样子:
USE Northwind GO CREATE PROCEDURE List_orders_5 @fromdate DATETIME = NULL AS DECLARE @fromdate_copy DATETIME SELECT @fromdate_copy = coalesce(@fromdate, '19900101') SELECT * FROM Orders WHERE OrderDate > @fromdate_copy
如果你执行这个存储过程,会看到这次使用了聚集索引扫描操作。
关键点:
从中我们得出一个结论:如果你从一个存储过程中提取一个查询,并使用一个常量替换变量和参数,你可能实际上得到了一个不同的查询,这一点在后续小节介绍。
把查询计划放入缓存:
- SQL Server 的缓冲池(buffer cache)已满,SQL Server需要把“过时”的内容清除出去。这个缓存池通常包含了数据和查询计划。
- 执行了ALTER PROC/PROCEDURE命令。
- 执行了对存储过程的sp_recompile命令。
- 执行了DBCC FREEPROCCACHE命令。
- SQL Server重启,因为缓冲池是存储在内存中,重启会清空内存。
- 使用sp_configure或SSMS修改了某些影响查询计划的配置参数。
- 语句中涉及的表定义被修改。
- 语句中涉及的表上的索引变更,包含使用ALTER INDEX或DBCC DBREINDEX重建索引。
- 语句涉及的表的统计信息更新或新建统计信息。这些统计信息可能被SQL Server自动创建和更新,也可能被DBA操作。
- 语句涉及的表被执行了sp_recompile。
EXEC List_orders_2 '19900101'
不同设置的查询计划:
USE Northwind GO CREATE PROCEDURE List_orders_6 AS SELECT * FROM Orders WHERE OrderDate > '12/01/1998' GO SET DATEFORMAT dmy GO EXEC List_orders_6 GO SET DATEFORMAT mdy GO EXEC List_orders_6 GO
SELECT qs.plan_handle, a.attrlist
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) est
CROSS APPLY (SELECT epa.attribute + '=' + convert(nvarchar(127), epa.value) + ' '
FROM sys.dm_exec_plan_attributes(qs.plan_handle) epa
WHERE epa.is_cache_key = 1
ORDER BY epa.attribute
FOR XML PATH('')) AS a(attrlist)
WHERE est.objectid = object_id ('dbo.List_orders_6')
AND est.dbid = db_id('Northwind')
plan_handle attrlist ------------------------------------------------------------------------------------------- -------------------------------------------------- 0x05000D009D13BF5E30DF8F550300000001000000000000000000000000000000000000000000000000000000 compat_level=120 date_first=7 date_format=2 0x05000D009D13BF5ED0678F590300000001000000000000000000000000000000000000000000000000000000 compat_level=120 date_first=7 date_format=1
USE Northwind GO EXEC sp_recompile List_orders_2 GO SET DATEFORMAT dmy GO EXEC List_orders_2 '12/01/1998' GO SET DATEFORMAT mdy GO EXEC List_orders_2 '12/01/1998' GO
第一句sp_recompile是为了刷掉旧的计划缓存确保对演示没有影响。这一次,参数值是一样的,但是由于日期格式不一样,所以第一个查询实际上是使用了1998-01-12而第二个查询实际上使用了1998-12-01。
SELECT convert(binary(4), 4347)
默认设置:
| 使用ADO.NET/ODBC/OLE DB的应用程序 | SSMS/查询分析器 | SQLCMD/OSQL/BCP/SQL Agent | ISQL/DB-Library | |
| ANSI_NULL_DFLT_ON | ON | ON | ON | OFF |
| ANSI_NULLS | ON | ON | ON | OFF |
| ANSI_PADDING | ON | ON | ON | OFF |
| ANSI_WARNING | ON | ON | ON | OFF |
| CONCATE_NULLS_YIELD_NULL | ON | ON | ON | OFF |
| QUOTED_IDENTIFIER | ON | ON | OFF | OFF |
| ARITHABORT | OFF | ON | OFF | OFF |
上面可以看到,如果你用应用程序连接SQL Server,那么ARITHABORT是OFF的,但是使用SSMS连接,却是ON,所以应用程序和SSMS执行的同一个存储过程也使用不同的缓存条目,但是SQL Server也会编译存储过程,嗅探当前参数值,然后得到可能和应用调用的存储过程不同的查询计划。那么现在差不多可以回答一开始的问题了。后续章节会做更加深入的探讨,但是最常见的关于“在应用程序中很慢,在SSMS中很快”的原因是参数嗅探和不同的ARITHABORT默认值。
SET ANSI_NULLS, QUOTED_IDENTIFIER OFF go CREATE PROCEDURE stupid @x int AS IF @x = NULL PRINT "@x is NULL" go SET ANSI_NULLS, QUOTED_IDENTIFIER ON go EXEC stupid NULL
结果将是: @x is NULL
- 记得上面表格中的前六个是仅仅为了向后兼容,所以建议设为OFF。虽然不是强制,但是通常而言,设为OFF很少会有问题。
- 对于ARITHABORT,在SQL 2005及其后续版本中,ANSI_WARNING是否为ON也不对其有任何影响。在SSMS中,可以通过设置来修改。但是这回在使用SSMS连接SQL Server时改变ARITHABORT的默认设置,并且不能让你的应用跑得更快,但是最起码你不会因为在SSMS中得到不同的性能而感到困惑。
语句重编译的效果:
USE Northwind GO CREATE PROCEDURE List_orders_7 @fromdate DATETIME, @ix BIT AS SELECT @fromdate = dateadd(YEAR, 2, @fromdate) SELECT * FROM Orders WHERE OrderDate > @fromdate IF @ix = 1 CREATE INDEX test ON Orders (ShipVia) SELECT * FROM Orders WHERE OrderDate > @fromdate GO EXEC List_orders_7 '19980101', 1
实际执行计划如下:
EXEC List_orders_7 '19960101', 0
USE Northwind GO DROP INDEX test ON Orders DROP PROCEDURE List_orders_7
USE Northwind GO CREATE PROCEDURE List_orders_8 AS DECLARE @fromdate DATETIME SELECT @fromdate = '20000101' SELECT * FROM Orders WHERE OrderDate > @fromdate CREATE INDEX test ON Orders (ShipVia) SELECT * FROM Orders WHERE OrderDate > @fromdate DROP INDEX test ON Orders GO EXEC List_orders_8 GO DROP PROCEDURE List_orders_8
USE Northwind GO CREATE PROCEDURE List_orders_9 AS DECLARE @ids TABLE (a INT NOT NULL PRIMARY KEY) INSERT @ids (a) SELECT OrderID FROM Orders SELECT COUNT(*) FROM Orders O WHERE EXISTS ( SELECT * FROM @ids i WHERE O.OrderID = i.a ) CREATE INDEX test ON Orders (ShipVia) SELECT COUNT(*) FROM Orders O WHERE EXISTS ( SELECT * FROM @ids i WHERE O.OrderID = i.a ) DROP INDEX test ON Orders GO EXEC List_orders_9 GO DROP PROCEDURE List_orders_9
USE Northwind GO CREATE TYPE temptype AS TABLE (a INT NOT NULL PRIMARY KEY) GO CREATE PROCEDURE List_orders_10 @ids temptype READONLY AS SELECT COUNT(*) FROM Orders O WHERE EXISTS ( SELECT * FROM @ids i WHERE O.OrderID = i.a ) GO DECLARE @ids temptype INSERT @ids (a) SELECT OrderID FROM Orders EXEC List_orders_10 @ids GO DROP PROCEDURE List_orders_10 DROP TYPE temptype
小结:
- 不总是参数嗅探的错
理解性能的奥秘——应用程序中慢,SSMS中快(2)——SQL Server如何编译存储过程的更多相关文章
- 理解性能的奥秘——应用程序中慢,SSMS中快(6)——SQL Server如何编译动态SQL
本文属于<理解性能的奥秘--应用程序中慢,SSMS中快>系列 接上文:理解性能的奥秘--应用程序中慢,SSMS中快(5)--案例:如何应对参数嗅探 我们抛开参数嗅探的话题,回到了本系列的最 ...
- 理解性能的奥秘——应用程序中慢,SSMS中快(5)——案例:如何应对参数嗅探
本文属于<理解性能的奥秘--应用程序中慢,SSMS中快>系列 接上文:理解性能的奥秘--应用程序中慢,SSMS中快(4)--收集解决参数嗅探问题的信息 首先我们需要明白,参数嗅探本身不是问 ...
- 理解性能的奥秘——应用程序中慢,SSMS中快(4)——收集解决参数嗅探问题的信息
本文属于<理解性能的奥秘--应用程序中慢,SSMS中快>系列 接上文:理解性能的奥秘--应用程序中慢,SSMS中快(3)--不总是参数嗅探的错 前面已经提到过关于存储过程在SSMS中运行很 ...
- 理解性能的奥秘——应用程序中慢,SSMS中快(3)——不总是参数嗅探的错
本文属于<理解性能的奥秘--应用程序中慢,SSMS中快>系列 接上文:理解性能的奥秘--应用程序中慢,SSMS中快(2)--SQL Server如何编译存储过程 在我们开始深入研究如何处理 ...
- 理解性能的奥秘——应用程序中慢,SSMS中快(1)——简介
本文属于<理解性能的奥秘--应用程序中慢,SSMS中快>系列 在工作中发现有不少类似的现象,有幸看到国外大牛写的一篇文章,由于已经完善得不能再添油加醋,所以决定直接翻译,原文出处:http ...
- 理解性能的奥秘——应用程序中慢,SSMS中快(4)收集解决参数嗅探问题的信息
---从计划缓存中直接获取查询计划和参数: ), ) SELECT @dbname = 'hydee_连锁', @procname = 'dbo.p_select_ware'; WITH baseda ...
- 在论坛中出现的比较难的sql问题:14(存储过程问题 存储过程参数、存储过程内的参数)
原文:在论坛中出现的比较难的sql问题:14(存储过程问题 存储过程参数.存储过程内的参数) 最近,在论坛中,遇到了不少比较难的sql问题,虽然自己都能解决,但发现过几天后,就记不起来了,也忘记解决的 ...
- SQL Azure (14) 将云端SQL Azure中的数据库备份到本地SQL Server
<Windows Azure Platform 系列文章目录> 注意: 1.只有SQL Server 2012 CU4及以上版本才支持本章内容 2.当你的数据库文件很大时,建议优化以下内容 ...
- SQL Server的嵌套存储过程中使用同名的临时表怪像浅析
SQL Server的嵌套存储过程,外层存储过程和内层存储过程(被嵌套调用的存储过程)中可以存在相同名称的本地临时表吗?如果可以的话,那么有没有什么问题或限制呢? 在嵌套存储过程中,调用的是外层存 ...
随机推荐
- [LeetCode] Student Attendance Record I 学生出勤记录之一
You are given a string representing an attendance record for a student. The record only contains the ...
- 页面中引入mui 地址选择,点击页面中其他input时页面回到顶部
问题:在页面中引入mui地址选择时,点击页面中的input页面会滚到顶部(谷歌浏览器中出现的bug),在手机上点击input会出现跳动.开始的时候是想修改mui.min.js里的滚动事件,但是后来找到 ...
- 浅谈CSRF漏洞
前言: 看完小迪老师的CSRF漏洞讲解.感觉不行 就自己百度学习.这是总结出来的. 歌曲: 正文: CSRF与xss和像,但是两个是完全不一样的东西. xss攻击(跨站脚本攻击)储存型的XSS ...
- [CQOI 2015]选数
Description 我们知道,从区间[L,H](L和H为整数)中选取N个整数,总共有(H-L+1)^N种方案.小z很好奇这样选出的数的最大公约数的规律,他决定对每种方案选出的N个整数都求一次最大公 ...
- [HNOI2002]跳蚤
题目描述 Z城市居住着很多只跳蚤.在Z城市周六生活频道有一个娱乐节目.一只跳蚤将被请上一个高空钢丝的正中央.钢丝很长,可以看作是无限长.节目主持人会给该跳蚤发一张卡片.卡片上写有N+1个自然数.其中最 ...
- ●HDU 6021 MG loves string
题链: http://acm.hdu.edu.cn/showproblem.php?pid=6021 题解: 题意:对于一个长度为 N的由小写英文字母构成的随机字符串,当它进行一次变换,所有字符 i ...
- uva 10118(DP)
UVA 10118 题意: 有4堆糖果,每堆有n(最多40)个,有一个篮子,最多装5个糖果,我们每次只能从某一堆糖果里拿出一个糖果, 如果篮子里有两个相同的糖果,那么就可以把这两个(一对)糖果放进自己 ...
- bzoj1094[ZJOI2007]粒子运动 计算几何
1094: [ZJOI2007]粒子运动 Time Limit: 10 Sec Memory Limit: 162 MBSubmit: 658 Solved: 164[Submit][Status ...
- FJOI2017 RP++
嗯如果算得没错大概十二小时之后就是省选二试了 这次考试貌似就在我们学校 虽然机子挺旧的基本没用过 平时训练都是在专门的机房 其实貌似压力不是很大 因为一试跪了TAT 那时候还是图样 T3按照惯例是 ...
- ubuntu14.0464位 Ros环境 安装halcon13.01
至于ROS的系统,之前就是安装好的,如果有疑问的可以参考官网的安装教程,按照指令一步一步的操作,http://wiki.ros.org/cn/indigo/Installation/Ubuntu (1 ...