提高SQL Server最具性能的一个方面就是存储过程,SQL Server具有执行计划的缓存功能,以便计划重用。SQL Server2000加强了ad-hoc执行计划的缓存功能,就处理存储过程上性能最佳,其缘由因为存储过程是做为数据库对象来使用;不过,存储过程的使用不当也必然致使缓存执行计划在初始查询时丢失,固然也会致使存储过程的重编译,于是带来没必要要的性能损失。本文主要介绍如下几点:php
过程缓存
node
过程缓存占据着SQL Server内存池的主要部分,早在SqL Server 7.0时,内存池中的对象由SqL Server动态分配,DBA没法为过程缓存指定内存配置,SQL Server为根据系统使用状况自我调节来确保缓存的命中率。很明显,SQL Server可用的内存越多,其内存池相应也越大,其缓存组件也就越大,下图列出了SQL Server内存池的各部分占用状况:
[img=511 border=0,225 src=]http://www.itpub.net/[/img]
图 1: SQL Server 内存池组件
sql
若是SQL Server使用动态内存配置,则它会同操做系统进行交互以请求所须要的内存,相应也会释放一些内存供其它进程使用。这必将会影响不一样内存组件的缓存使用。例如,在一台SQL Server上运行一些存储过程,而后检查缓存的执行计划和是否重用。启动占用内存比较大的程序,回事后来再来检查缓存时会发现,以前缓存的计划已经从内存中删除。再次执行存储过程时,在Profiler中能够看到Cache Miss事件,其缘由主要因为先前缓存的执行计划已经再也不可用。
数据库
DBCC DROPCLEANBUFFERS 与 DBCC FREEPROCCACHE
缓存
在生产环境中,一般不建议对buffers和缓存进行修改,缘由会致使性能的极大损失。然而,微软提供了两个DBCC命令,方便咱们无需中止和重启SQL Server服务就能够删除缓存的数据和计划缓存,固然这两个命令也存在性能的差别,下面咱们来逐一介绍:
安全
DBCC DROPCLEANBUFFERS
性能
此命令用于清除数据缓存,该命令须要有sysadmin组权限。
测试
DBCC FREEPROCCACHE
ui
此命令用于清空过程缓存。经过SQL Profiler能够看到Cache Remove事件的存在。当使用此命令回到未缓存状态。在存储过程第一次执行和以后执行时性能上存在着极大差别,因为在执行以前会执行编译过程。
编码
系统表syscacheobjtects(SQL Server 2005中为sys.dm_exec_cached_plans)存储了缓存执行计划的信息,这里我主要介绍如下几列:
列名
|
描述
|
cacheobjtype
|
这个是缓存对象的类型,这里主要介绍如下两个:
|
objtype
|
这个是对象类型,因为本文介绍存储过程,此类型为Proc
|
objid
|
这个与sysobjects表中的id字段相对应(ad-hoc或prepared查询除外)即存储过程的名称
|
dbid
|
因为objid参照sysobjects的id,dbid即对应数据库标识。
|
uid
|
Uid对应用户标识
|
sql
|
Sql执行语句或存储过程名称(无参数)
|
经过查询该系统表能够校验计划是否被缓存。缓存中存储的执行计划只能经过该表来查询,它一方面使咱们找出缓存了哪些计划,另外一方面使咱们对缓存的工做模式有了更多了解,对于更多的信息,须要使用Profiler来获取执行计划的信息。
Profiler模板配置
在SQL Server 2000中,Profiler初始为咱们提供了关于存储过程的缓存和编译的事件,启动默认的模板,咱们只需添加SP:Cache事件、SP:ExecContextHit事件和SP:Recopile事件。因为是在本地上运行SQL Server,这里移除了RPC:Completed事件,添加SP:Completed事件,若要查看当缓存丢失和重编译事件,也须要添加SP:Starting和SP:Stmt事件。
编译与执行计划
当存储过程首次被调用时,则会生成其执行计划,这也就是编译执行计划的含义,不过这与VB语言或即时编译(just-in-time)及其余语言如Java不一样,SQL Server将存储过程构建一个中间阶段-执行计划。从执行计划中能够知道哪些索引可使用,以并行执行时须要分哪些步骤作等等。
存储过程第一次调用时,SQL Server须要在过程缓存中(更高层次上)查看是否已存在该执行计划。因为存储过程是首次调用,并无找到相应的执行计划。SQL Server的编译进程则处于准备阶段,而后对该存储过程发出一个[COMPILE]锁,固然也会在过程缓存中进行搜索,以找到与该对象对应的执行计划。正由于是首次执行,SQL Server也会因为找不到匹配的执行计划,而编译生成新的执行计划,并将其放入过程缓存中执行。
那么第二次执行会怎样?当存储过程再次调用时,首先会检查过程缓存中是否有对应的执行计划,这与先前第一次调用时所作的同样,如果对于指定了引用存储过程的数据库和拥有者则会在缓存中很快找到相匹配的执行计划,而无需重编译或从新生成新的执行计划。如果找不到匹配的执行计划,SQL Server会再次对调用的存储过程执行[COMPILE]锁,接着是一系列的缓存搜索等操做。Lazywriter会负责执行计划在内存中停留的时间量,确保了常用的执行计划在过程缓存中的时间会更长。
使用 sp_带来的问题
SQL Server中以sp­_打头的存储过程默认为系统存储过程,这些存储过程默认应存储在master数据库中,不过也有一些开发人员选择sp_做为存储过程的命名前缀,而这些存储过程位于用户建立的数据库中。使用存储在非master数据库以sp_命名的存储过程所引起的问题是会产生一个缓存丢失事件,即每次调用存储过程时会执行[COMPILE]锁,随后则是一系列的缓存搜索,其缘由是因为SQL Server处理以sp_打头的存储过程方式不一样,并不关心其在过程缓存。
如下是SQL Server处理sp_存储过程的步骤:
既使sp_存储过程的owner符合要求,SQL Server首先仍在master中查找,当在检查过程缓存时,在master数据库上会扫描执行计划,于是产生一个SP:CacheMiss事件,如下是调用第二次sp_存储过程时的SQL Profiler(这里添加了SP:StmtStarting和SP:StmtCompleted用来讲明当存储过程首次执行时SP:CacheMiss事件的产生)
注意最开始的SP:Cache Miss事件,此事件的产生是因为存储过程sp_CacheMiss不在master数据库,从而,SQL Server执行[COMPILE]锁以及一系列的缓存搜索,在二次的缓存搜索时,SQL Server能够找到其对应的执行计划(即SP:ExecContextHit事件的出现),此时不须要额外的时间和资源来查找执行计划,不过应当注意的是[COMPILE]锁是排它锁。在重编译时,存储过程则是以有序进行,必然形成系统性能的下降。
正是因为SQL Server查询sp_命名的存储过程的方式,因此建议选择其余命名方法,例如usp_、proc_等。
SQL Server为咱们提供了许多灵活性,可是须要咱们合理地使用来确保产生没必要要的性能问题,其中一个方面就是命名数据库对象。当调用一个存储过程时,SQL Server会查看是否指定了该对象的owner,若未指定,则会执行过程缓存初始化搜索,以查找知足与该调用者匹配的存储过程。所以,假如咱们使用非dbo用户,如SQLUser,而存储过程属于dbo,咱们会发现仍获得一个SP:CacheMiss事件,状况和在用户数据库中调用sp_存储过程同样。下面是未指定owner时产生SP:Cache Miss事件的一个Profiler例子:
从图中注意到该存储过程的执行方式并未指定owner,若以普通用户身份登陆到SQL Server,则会扫描过程缓存并查找属于该owner的存储过程usp_CacheHit,SP:CacheMiss事件由此而来。若咱们使用两部分命名方式显式指定owner,则直接获得SP:ExecContextHit事件,这代表未执行[COMPILE]锁和过程缓存的二次扫描操做。如下是指定owner的跟踪:
在调用存储过程以前的一点不一样就是添加”dbo.”,不过咱们经常匆略了这一点,建议养成添加“dbo.”习惯,通常地,咱们以dbo的身份建立存储过程,可是若以非dbo身份建立,则须要指定owner,不然会产生SP:Cache Miss事件,由此产生来的性能前面已经讨论。
重编译问题
一般看到存储过程发生重编译有几种缘由,少数的重编译未必是坏事。例如,某表中数据的更改,因为数据的实时性,先前的执行计划效率将会下降,必然须要重编译;另一种状况是手动执行sp_recompile存储过程来强制存储过程的重编译,或者以WITH RECOMPILE选项来执行存储过程。
DML和DDL混合
存储过程内部将DDL(数据定义语言)和DML(数据操做语言)混合交错执行,也将致使重编译,甚至在执行过程当中也会发生重编译,例如:让咱们经过如下存储过程示例来讲明:
CREATE PROC usp_Build_Interleaved
AS
-- DDL 定义
CREATE TABLE A (
CustomerID nchar(5) NOT NULL CONSTRAINT PK_A PRIMARY KEY CLUSTERED,
CompanyName nvarchar(40) NOT NULL,
City nvarchar(15) NULL,
Country nvarchar(15) NULL)
-- DML 定义
INSERT A
SELECT CustomerID, CompanyName, City, Country
FROM Customers
-- DDL 定义
CREATE TABLE B (
OrderID int NOT NULL CONSTRAINT PK_B PRIMARY KEY NONCLUSTERED,
CustomerID nchar(5) NOT NULL,
Total money NOT NULL)
-- DML 定义
INSERT B
SELECT O.OrderID, O.CustomerID, SUM((OD.UnitPrice * OD.Quantity) * (1 - OD.Discount))
FROM Orders O JOIN [Order Details] OD ON O.OrderID = OD.OrderID
GROUP BY O.OrderID, O.CustomerID
CREATE CLUSTERED INDEX IDX_B_CustomerID ON B (CustomerID)
如上所示的,DML和DDL语句交错执行,要看其实际的效果,下面是使用Profiler捕获的结果:
如上图所示的SP:ExecContextHit事件,已经产生了一个缓存执行计划。不过,因为DDL与DML的交错执行,在存储过程执行过程当中产生了两个SP:Recompile事件。因为示例中的存储过程同时含有DML和DDL语句,必然不可以一块儿移除重编译,可是,能够经过将全部的DDL语句放在存储过程的顶部的方法能够将2次重编译减小到1次重编译。
CREATE PROC usp_Build_NoInterleave
AS
-- DDL
CREATE TABLE A (
CustomerID nchar(5) NOT NULL CONSTRAINT PK_A PRIMARY KEY CLUSTERED,
CompanyName nvarchar(40) NOT NULL,
City nvarchar(15) NULL,
Country nvarchar(15) NULL)
CREATE TABLE B (
OrderID int NOT NULL CONSTRAINT PK_B PRIMARY KEY NONCLUSTERED,
CustomerID nchar(5) NOT NULL,
Total money NOT NULL)
-- DML
INSERT A
SELECT CustomerID, CompanyName, City, Country
FROM Customers
INSERT B
SELECT O.OrderID, O.CustomerID, SUM((OD.UnitPrice * OD.Quantity) * (1 - OD.Discount))
FROM Orders O JOIN [Order Details] OD ON O.OrderID = OD.OrderID
GROUP BY O.OrderID, O.CustomerID
CREATE CLUSTERED INDEX IDX_B_CustomerID ON B (CustomerID)
经过重写存储过程,咱们看到两个CREATE TABLE语句都位于前面,仅在这两个表建立后执行DML语句来填充数据和建立汇集索引,运行跟踪,咱们只看到一个SP:Recompile事件:
咱们并不能彻底移除SP:Recompile事件,可是能够重写存储过程的方式来减小重编译的次数。
使用 sp_executesql
一般在存储过程内部不使用sp_executesql系统存储过程,不过,此方法也能够解决一些重编译问题。使用sp_executesql和EXECUTE语句的主要问题是基于安全上的考虑,若使用存储过程来限制数据库的访问,使用sp_executesql会带来一些麻烦。
可是,若使用sp_executesql或EXECUTE语句传递SQL串来执行,SQL Server则自动检查其安全性,若是使用sp_executesql来从表中获取数据,须要受权调用者在该表的SELECT权限。那么为何要采用sp_executesql呢?下面经过一个例子:
CREATE PROC usp_Display_Recompile
AS
SELECT A.CompanyName, A.City, A.Country, B.OrderID, B.Total
FROM A JOIN B ON A.CustomerID = B.CustomerID
DROP TABLE A
DROP TABLE B
以上提到的一点:若是给定的表数据发生了更改,存储过程显然很容易产生重编译。因为usp_Display_Recompile的数据依赖于先前建立的存储过程,usp_Display_Recompile每次执行时,数据因表删除、重建和重填充而改变,查看Profiler确认重编译事件:
注意到执行SELECT语句,SQL Server执行了一次重编译。如果以sp_executesql执行,能够避免重编译,下面来重写存储过程:
CREATE PROC usp_Display_NoRecompile
AS
EXEC sp_executesql N'SELECT A.CompanyName, A.City, A.Country, B.OrderID, B.Total
FROM A JOIN B ON A.CustomerID = B.CustomerID'
DROP TABLE A
DROP TABLE B
此时的SELECT语句已经在sp_executesql的上下文内,若在两个表上具备SELECT权限,则能够避免存储过程的重编译,经过Profile的跟踪结果能够发现避免了重编译:
与SELECT语句对应是的,产生了一个SP:CacheInsert事件,可是并没有SP:Recompile事件的产生,所以使用sp_executesql,能够彻底避免重编译。
备注
上面咱们简要地介绍了存储过程和缓存,过程缓存是内存缓冲池的主要部件,由SQL Serve动态分配,截至到SQL Server 7.0,仍没有可用的方法来控制缓存大小,不过,SQL Server的内部缓存结构仍保留最常用和开销比较低的执行计划驻留内存。能够经过syscacheobjects系统表查看内存中缓存计划,对于更详细的能够经过SQL Profiler来得到。