提高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來得到。