理解性能的奥秘——应用程序中慢,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的嵌套存储过程,外层存储过程和内层存储过程(被嵌套调用的存储过程)中可以存在相同名称的本地临时表吗?如果可以的话,那么有没有什么问题或限制呢? 在嵌套存储过程中,调用的是外层存 ...
随机推荐
- servlet之隐藏域
隐藏域的实现, 商品对象 package app02b;public class Customer { private int id; private String name; ...
- python3进阶之推导式之列表(list)推导式(comprehensions)
1.前言 推导式,英文名字叫comprehensions,注意与comprehension(理解)只有s字母之差.推导式又可以叫解析式,推导式可以从一种数据序列构建新的数据序列的结构体.推导式分为,列 ...
- codevs 1766 装果子
提交地址:http://codevs.cn/problem/1766/ 1766 装果子 时间限制: 1 s 空间限制: 256000 KB 题目等级 : 黄金 Gold 题目描述 De ...
- [Codeforces 940E]Cashback
Description 题库链接 给你两个整数 \(n,c\) ,以及一个数列 \(A\) ,让你将序列分为许多段.对于每一段,他的价值为序列内除了最小的 \(\left\lfloor\frac{le ...
- WISCO信息组NOIP模拟赛-数据结构
传送门 差分+暴力 #include<cstdio> #include<cstdlib> #include<algorithm> #include<cstri ...
- [LCA模版] Distance Queries
题目描述 约翰的奶牛们拒绝跑他的马拉松,因为她们悠闲的生活不能承受他选择的长长的赛道.因此他决心找一条更合理的赛道.此题的输入于第一题相同,紧接着下一行输入一个整数K,以后K行为K个"距离问 ...
- 2015 多校联赛 ——HDU5410(dp)
Sample Input 1 100 2 10 2 1 20 1 1 Sample Output 21 题意:共有m元钱和n种东西,求每种单价p,而且你买x个该种物品可以得到Ax+B个,求m元钱最 ...
- ICM Technex 2017 and Codeforces Round #400 (Div. 1 + Div. 2, combined)
前四题比较水,E我看出是欧拉函数傻逼题,但我傻逼不会,百度了下开始学,最后在加时的时候A掉了 AC:ABCDE Rank:182 Rating:2193+34->2227 终于橙了,不知道能待几 ...
- Gradle学习之构建java与web项目
一.使用Gradle的java插件构建Java项目 1)Gradle插件包含了若干个接口定义和已有的任务项,语法结构:apply plugin:'插件名' ,此处我们定义插件 apply plugin ...
- mysql 合并left join 数据条目
查询两张关联表信息,原sql: SELECT * FROM car_life_info c left JOIN shop_label sl ON c.shop_id = sl.shop_id 出现 ...