理解性能的奥秘——应用程序中慢,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的嵌套存储过程,外层存储过程和内层存储过程(被嵌套调用的存储过程)中可以存在相同名称的本地临时表吗?如果可以的话,那么有没有什么问题或限制呢? 在嵌套存储过程中,调用的是外层存 ...
随机推荐
- Oracle12c功能增强新特性之维护&升级&恢复&数据泵等
1. 内容提要 1) 表分区维护的增强. 2) 数据库升级改善. 3) 跨网络还原/恢复数据文件. 4) 数据泵的增强. 5) 实时ADDM. 6) 并发统计信息收集. 2 ...
- Ubuntu安装及配置virtualenv,virtualenvwrapeer
安装virtualenv pip install virtualenv 如果下载较慢,可以添加豆瓣源: pip install -i https://pypi.douban.com/simple/ v ...
- 深入探索.NET框架内部了解CLR如何创建运行时对象
原文地址:http://msdn.microsoft.com/en-us/magazine/cc163791.aspx 原文发布日期: 9/19/2005 原文已经被 Microsoft 删除了,收集 ...
- [USACO 04OPEN]MooFest
Description 约翰的N 头奶牛每年都会参加“哞哞大会”.哞哞大会是奶牛界的盛事.集会上的活动很多,比如堆干草,跨栅栏,摸牛仔的屁股等等.它们参加活动时会聚在一起,第i 头奶牛的坐标为Xi,没 ...
- [BZOJ 3332]旧试题
Description 圣诞节将至.一年一度的难题又摆在wyx面前——如何给妹纸送礼物. wyx的后宫有n人,这n人之间有着复杂的关系网,相互认识的人有m对.wyx想要量化后宫之间的亲密度,于是准备给 ...
- [POI2009]KAM-Pebbles
题目描述 Johny and Margaret are playing "pebbles". Initially there is a certain number of pebb ...
- BZOJ 4372 烁烁的游戏
Description 背景:烁烁很喜欢爬树,这吓坏了树上的皮皮鼠.题意:给定一颗n个节点的树,边权均为1,初始树上没有皮皮鼠.烁烁他每次会跳到一个节点u,把周围与他距离不超过d的节点各吸引出w只皮皮 ...
- [SCOI2016]背单词
题目描述 Lweb 面对如山的英语单词,陷入了深深的沉思,”我怎么样才能快点学完,然后去玩三国杀呢?“.这时候睿智的凤老师从远处飘来,他送给了 Lweb 一本计划册和一大缸泡椒,他的计划册是长这样的: ...
- permu(变态考试题)
题目描述 给定一个严格递增的序列T,求有多少个T的排列S满足:∑min(T[i],S[i])=k 输入输出格式 输入格式: 第一行两个数n,k 第二行n个数,表示T 输出格式: 一个正整数表示答案,答 ...
- 「LibreOJ NOIP Round #1」旅游路线
Description T 城是一个旅游城市,具有 nnn 个景点和 mmm 条道路,所有景点编号为 1,2,...,n1,2,...,n1,2,...,n.每条道路连接这 nnn 个景区中的某两个景 ...