1. 程式人生 > >執行計劃的重用

執行計劃的重用

分析 advance 單詞 流行 sql tostring netd nvarchar gpo

  當查詢被提交時,SQL Server檢查過程緩沖中匹配的執行計劃,如果沒有找到,SQL Server執行查詢編譯和優化以生成新的執行計劃。

  如果執行計劃存在於緩沖中,它在私有的執行上下文中重用,這節約了CPU的編譯和優化周期。

  具有不同過濾條件的相同查詢提交到SQL Server時,如:

SELECT * FROM Person WHERE Id = 1

  當這個查詢被提交時,優化器創建一個執行計劃並將其存儲在過程緩沖中以被將來重用。如果這個查詢使用不同的過濾條件,如:WHERE Id = 2重新提交,重用前面提供的過濾條件值所用的現有執行計劃是有利的。但是為一個過濾條件值創建的執行計劃是否可以重用於另一個過濾條件值,取決於查詢提交到SQL Server的方式。

  提交到SQL Server的查詢分為以下兩類:

  •   即席的(Ad Hoc);
  •   預定義的(Prepared);

一、即席工作負載

  查詢可以在不隔離變量的情況下提交到SQL Server。這種不明確地將查詢可變部分轉換成參數而執行的查詢被稱為即席工作負載(ad hoc workloads)。

  執行如下查詢:

SELECT * FROM Person WHERE Id = 1

  如果查詢按原樣提交,沒有明確地轉換變量為參數,則這是一個即席查詢。

  對於這種情況,除非使用相同的變量,否則不能重用查詢計劃。

二、預定義工作負載

  預定義(Prepared)工作負載明確地參數化查詢的可變部分,這樣查詢計劃不與可變部分的值綁定。在SQL Server中,查詢可以使用以下3種方法作為預定義工作負載提交:

  •   存儲過程:允許保存一個能夠接受並返回用戶提供的參數的SQL語句集合;
  •   sp_executesql:允許執行一個SQL語句或一個SQL批,可以包含用戶提供的參數;
  •   準備/執行模式:允許SQL客戶請求生成能夠在隨後不同參數值的查詢執行期間重用的查詢計劃,不在SQL Server中保存SQL語句;

  對於前面所示的SQL語句可以明確地使用以下的存儲過程參數化:

CREATE PROC InsertPerson1
@Id int
AS
SELECT * FROM Person WHERE PersonId = @Id

  包含在存儲過程中的SELECT語句計劃將嵌入參數@Id不是變量值。

三、即席工作負載的計劃可重用性

  當查詢作為一個即席的工作負載被提交,SQL Server生成一個執行計劃並根據生成這個執行計劃的開銷來決定是否緩沖該計劃。如果生成執行計劃的開銷非常經濟,SQL Server可能根據可用資源不緩沖該計劃以保持過程緩沖的大小。SQL Server在查詢重新提交時重新生成執行計劃,而不用經濟的繼續查詢充滿過程緩沖。

  對於生成執行計劃較高的即席查詢,SQL Server在過程緩沖中保存執行計劃。即席查詢可變部分的值被包含在查詢計劃中而沒有單獨地保存在執行上下文中,這意味著,除非使用所看到的完全相同的變量,否則無法重用這個執行計劃。 

  動手寫個示例:

DBCC FREEPROCCACHE    --清除執行計劃緩存
SELECT * FROM PersonHunderThousand WHERE Id = 1  --此句點擊執行兩次

SELECT * FROM PersonHunderThousand WHERE Id = 2

  下面運行如下語句:

SELECT text,refcounts,usecounts,objtype,cacheobjtype 
FROM sys.dm_exec_cached_plans as p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle)

  看到輸出結果:

  技術分享圖片

  留意到上面的結果:對於兩個不同的Id查詢條件,SQL Server生成了兩個執行計劃。而且根據使用次數usecounts看出,Id=1與Id=2的查詢計劃並不重用。

  這種即席查詢的執行計劃重用效率低下,增加了CPU的負載,因為消耗了附加的CPU周期來重新生成計劃。總而言之,即席計劃緩沖使用語句級的緩沖並且被限制為精確文本匹配。但是對於以上這種簡單查詢(只涉及到一個表),SQL Server有個稱為"簡單參數化"的特性,能夠進行隱式地進行查詢參數化以增進計劃可重用性。但是只限於查詢非常簡單的情況,如只涉及到一個表。

  1、優化即席工作負載

  如果服務器主要用來支持即席查詢,那麽只能得到很低程度的性能改善。有一個服務器選項被稱為optimize for ad hoc workloads(優化即席工作負載),為服務器啟用這個選項改變引擎處理即席查詢的方式。在查詢第一次被調用時不生成完整的編譯計劃,而是創建一個編譯計劃存根(compiled plan stub)。這個存根沒有相關的完整執行計劃,節省了生成執行計劃的事件和所需要的存儲空間。這個選項可以在不重用服務器的情況下使用:


  修改這個選項之後,刷新緩沖,然後重新運行查詢:

技術分享圖片
EXEC sp_configure ‘show advanced options‘,1    --要開啟這個高級選項,才能開啟下面
RECONFIGURE
EXEC sp_configure ‘optimize for ad hoc workloads‘,1  --開啟即席優化負載
RECONFIGURE

--清除執行計劃緩存
DBCC FREEPROCCACHE    
--再次執行這個語句,但是換了個Id為3
SELECT * FROM PersonHunderThousand WHERE Id = 3

SELECT text,refcounts,usecounts,objtype,cacheobjtype,size_in_bytes
FROM sys.dm_exec_cached_plans as p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle)
技術分享圖片

  顯示結果如下:

  技術分享圖片

  從保存到執行計劃緩沖中的大小可以看到非常小,實際上這時存儲在緩沖中的對象是一個編譯計劃的存根,可以為許多查詢創建存根而對服務器的影響小於完整的編譯計劃,但是下次執行即席查詢時,創建一個完整的編譯計劃。

  so,還等什麽,再次執行:

SELECT * FROM PersonHunderThousand WHERE Id = 3

  來看結果:

  技術分享圖片

  留意到存儲進緩沖的到size已經變大了不少,第二次之後之後,這時候存儲進去的已經變成了完整的編譯計劃,執行計劃,也創建了一個新的句柄。

  這說明了在使用許多即席查詢時的節省。

  2、簡單參數化

  提交一個即席查詢時,SQL Server分析查詢以確定輸入文本的哪個部分可能是參數。它查看即席查詢的可變部分,以確定是否可以安全地自動參數化它們,並在查詢中使用這些參數以使查詢計劃獨立於變量值。這種在沒有明確地參數化(使用預定義工作負載技術)時自動轉換查詢的可變部分的特性被稱為簡單參數化。

  在參數化期間,SQL Server去報如果即席查詢轉換為一個參數化模板,參數值的修改不會廣泛地改變計劃的需求。確定簡單參數安全之後,SQL Server為一個即席查詢創建一個參數化模板並將參數化計劃保存在過程緩沖。

  參數計劃不基於用於查詢中的動態值(管你Id=1,Id=2)。因為該計劃為參數化模板(Id=x)所生成,它可以在即席查詢以可變部分的不同值重新執行時被重用。

  示例,按順序點擊一次如下查詢:

技術分享圖片
--清除執行計劃緩存
DBCC FREEPROCCACHE    
--再次執行這個語句,但是換了個Id為3
SELECT * FROM PersonHunderThousand WHERE Id = 4

SELECT text,refcounts,usecounts,objtype,cacheobjtype,size_in_bytes
FROM sys.dm_exec_cached_plans as p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle)
技術分享圖片

  再來看看輸出:

  技術分享圖片

  留意到紅框部分,這個是自動生成的,現在看到該自動生成的執行計劃的使用數量為1,要註意,這一行的自動參數化的可執行計劃的objtype不再是Adhoc(即席的),而是Prepared(預定義的)。

  我們來執行如下查詢:

  技術分享圖片

  看到,Id雖然不同,但是參數計劃的usecounts還是由1變為了2。該參數計劃被重用了。但是依然會生成一個新Id的即席計劃。當Id與現有所有的即席計劃不同時,參數計劃會use一次,當Id與現有計劃匹配時,參數計劃不會使用,匹配的即席計劃會use一次。

  原來的即席查詢盡管不被執行,但是被編譯以創建查詢簡單參數化所需的查詢樹。根據可用的資源,即席查詢的編譯計劃可能會被保存在計劃緩沖中。但是在創建即席查詢的可執行計劃之前,SQL Server判斷自動化參數是安全的,然後自動參數化這個查詢以進行進一步處理。

  另外,當即席查詢可以安全地自動參數化時,SQL Server選擇一種可以代替查詢原樣的文字模板。

  如,執行以下查詢:

SELECT * FROM PersonHunderThousand WHERE Id BETWEEN 5 AND 15

  SQL Server生成的參數計劃如下:

  技術分享圖片

  我們看到上面自動生生的參數計劃,SQL Server選擇了使用>=和<=來代替BETWEEN AND。

  這時候,即使使用》= 《=作為查詢條件也一樣可以重用參數計劃。如:

  技術分享圖片

  從usecounts可以看到,現有計劃被重用了,盡管該查詢和前面執行的語法不一樣,SQL Server生成的自動參數化計劃允許現有計劃不僅在查詢以不同變量值重新提交時重用,而且可以用於使用相同模板形式的查詢。

  3、簡單參數化的局限

  SQL Server在簡單參數化中非常保守,因為不好的計劃的開銷可能遠遠超過生成一個新的計劃的開銷。因而,簡單參數化被限於相當簡單的情況,如只使用一個表的即席查詢。有兩個或更多表之間連接操作的即席查詢不會自動生成參數化計劃。

  4、強制參數化

  如果工作的系統主要由即席查詢組成,你可能希望嘗試增加接收參數的查詢數量。可以修改數據庫來嘗試在一定的限制之內,強制所有查詢像簡單參數化中那樣被參數化。可以使用ALTER DATABASE修改數據庫選項PARAMETERIZATION為FORCED。

  方法如下:

ALTER DATABASE DataExample SET PARAMETERIZATION FORCED/SIMPLE --強制所有查詢像簡單參數化那樣參數化/使用簡單參數化

  測試,我們先執行如下語句(在之前先別執行上面那行語句):

SELECT * FROM PersonHunderThousand as per INNER JOIN Province as pro ON
per.PId = pro.Id WHERE per.Name = ‘幹鷗粕‘

SELECT text,refcounts,usecounts,objtype,cacheobjtype,size_in_bytes
FROM sys.dm_exec_cached_plans as p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle)

  顯示結果如下:

  技術分享圖片

  因為上面的查詢INNER JOIN了一個表,所以並沒有簡單參數化緩沖一個參數計劃。而是直接緩沖了一個即席計劃。

  假如我們清空執行計劃緩沖,然後強制參數化呢?

ALTER DATABASE DataExample SET PARAMETERIZATION FORCED --強制所有查詢像簡單參數化那樣參數化

  再執行以上的語句:

  技術分享圖片

  可見,對於原本並不簡單參數化的查詢,現在也生成了一個參數化計劃。

  強制參數化可以替代字符串參數‘幹鷗粕‘,變量聲明長度為8000的VARCHAR,而不是類似查詢參數的NCHAR(3),這樣可以應用於更多查詢。這樣可能導致阻止索引使用的隱含數據轉換。

  強制參數化實際上只對苦於即席查詢的大量編譯和重編譯的情況有幫助,其他負載將不會從使用強制參數化中獲益。

四、預定義工作負載的計劃可重用性

  將查詢定義為預定義工作負載允許查詢的可變部分明確地參數化。這使SQL Server能生成一個不綁定到查詢可變部分的查詢計劃,使可變部分獨立於執行上下文中。
  SQL Server支持3中技術來提交預定義工作負載:

  • 存儲過程;
  • sp_executesql;
  • 準備/執行(Prepare/execute)模式;

  1、存儲過程;

  使用存儲過程是改進計劃緩沖效率的標準技術。當存儲過程被編譯時,為所有過程中的SQL語句生成一個組合的計劃。為存儲過程生成的執行計劃可以在存儲過程以不同參數值重新執行時重用。

  除了檢查sys.dm_exec_cached_plans外,還可以使用Profiler工具跟蹤存儲過程的執行計劃緩沖,下面的事件用於跟蹤存儲過程的執行計劃。

事件 描述
SP:CacheHit 緩沖中找到的計劃
SP:CacheMiss 緩沖中沒有找到的計劃
SP:ExecContextHit 緩沖中找到的存儲過程執行上下文

  為了使用Profiler跟蹤存儲過程計劃緩沖,可以將這些事件和表9-3所示的其他存儲過程事件和數據列一起使用。

事件 數據列
SP:CacheHit EventClass
SP:CacheMiss TextData
SP:Completed LoginName
SP:ExecContextHit SPID
SP:Starting StartTime
SP:StmtCompleted

  創建一個存儲過程如下:

技術分享圖片
CREATE PROC getPerson
@Id INT
AS
SELECT * FROM PersonTenThousand
INNER JOIN Province
ON PersonTenThousand.PId = Province.Id
WHERE PersonTenThousand.Id = @Id

--並執行一次
EXEC getPerson @Id = 6678
技術分享圖片

  來看sys.dm_exec_cached_plans的輸出:

  技術分享圖片

  該存儲過程創建並緩沖了一個類型為Proc的編譯計劃。執行的usecounts為1,因為存儲過程只執行一次。

  我們來看看執行這個存儲過程SQL Profiler捕捉到的輸出:

技術分享圖片

  從Profiler跟蹤輸出,可以看到存儲過程的計劃在緩沖中沒有找到。當存儲過程第一次執行時,SQL Server查找過程緩沖並且找不到用於gerPerson過程的緩沖條目,導致了一個SP:CacheMiss事件。沒有找到緩沖計劃時,SQL Server安排編譯該存儲過程,隨後SQL Server生成並保存計劃並且繼續執行該存儲過程。

  如果這個存儲過程重新執行以檢索@Id = 9876:

EXEC getPerson @Id = 9876

  現有計劃被重用,如sys.dm_exec_cached_plans所示:

  技術分享圖片
  可以從Profiler跟蹤輸出確認執行計劃的重用:

  技術分享圖片

  從Profiler跟蹤輸出可以看到,現有的計劃在過程緩沖中找到。在搜索緩沖時,SQL Server找到存儲過程p1所用的執行計劃並導致一個SP:CacheHit事件。一旦找到現有的執行計劃,SQL重用該查詢以執行存儲過程。

  存儲過程值得考慮的其他一些方面:

  • 存儲過程在第一次執行時編譯;
  • 存儲過程有其他性能上的好處,如降低網絡流量;
  • 存儲過程有附加的好處,如數據隔離;

  2、存儲過程在第一次執行時編譯

  存儲過程的執行計劃在第一次執行時生成。存儲過程創建時,它只被解析並且保存在數據庫中。在存儲過程創建期間沒有執行任何規範化和優化過程,這使存儲過程可以在其訪問的對象創建之前創建。

  例如,可以創建一個存儲過程如下,即使表Person不存在也能創建:

CREATE PROC getPerson
@Name nvarchar(50)
AS
SELECT * FROM Person WHERE Name = @Name

  因為該引用對象綁定到查詢樹(在存儲過程執行期間由命令解析器生成)的規範化過程在存儲過程創建時沒有執行。存儲過程將在第一次執行時報告錯誤(如果執行時仍然沒有Person表),因為第一次執行時編譯該存儲過程。

  3、存儲過程在性能上的其他好處

  除了通過執行計劃可重用性改進性能之外,存儲過程還提供以下性能上的好處。

  • 業務邏輯靠近數據:執行保存在數據庫中的數據上的廣泛操作的業務邏輯部分應該放在存儲過程中,因為SQL Server的引擎對於關系和集合操作來說是極其強大的。
  • 降低網絡流量:跨越網絡的數據庫應用程序只發送存儲過程的名稱和變量值。只有處理過的結果集被返回到應用程序,中間數據部需要在應用程序和數據庫之間來回傳遞。
  4、sp_executesql

  sp_executesql是一個系統存儲過程,它提供了將一個或多個查詢作為預定義工作負載提交的機制。它允許查詢的可變部分明確地參數化,從而提供和存儲過程同樣有效的執行計劃可重用性。NHibernate生成的SQL語句就是這種。註意,sp_executesql的SQL語句是要求NVARCHAR類型的。

  如:

DECLARE @query NVARCHAR(MAX)
DECLARE @param NVARCHAR(MAX)
SET @query = N‘SELECT * FROM PersonTenThousand AS P1 INNER JOIN Province AS P2 ON P1.PId = p2.Id WHERE p1.Id = @Id‘
SET @param = N‘@Id INT‘
EXEC sp_executesql @query,@param,@Id = 9888

  對於SQL語句,傳遞給sp_executesql存儲過程的字符串被聲明為NVARCAHR並且帶有前綴N。這是因為,sp_executesql使用Unicode字符串作為輸入參數。

  接下來,看看sys.em_exec_cached_plans的輸出:

  技術分享圖片

  從第二行中看到,為通過sp_executesql提交的查詢參數化部分生成的計劃。因為該計劃沒有綁定到查詢的可變部分,所以如果查詢以參數的不同值重新提交,現有的執行計劃可以被重用。

DECLARE @query NVARCHAR(MAX)
DECLARE @param NVARCHAR(MAX)
SET @query = N‘SELECT * FROM PersonTenThousand AS P1 INNER JOIN Province AS P2 ON P1.PId = p2.Id WHERE p1.Id = @Id‘
SET @param = N‘@Id INT‘
EXEC sp_executesql @query,@param,@Id = 777

  sys.em_exec_cached_plans的輸出:

  技術分享圖片

  由usecounts看出,這個參數查詢計劃被重用了。如果這個查詢以可變部分的不同值被重新提交許多次,現有的執行計劃可以被重用而不用重新生成新的執行計劃。

  這裏要註意,所創建的目標查詢語句與通過sp_executesql提交的參數化查詢的文本串匹配。因此,如果相同的查詢從應用程序的不同部分提交,需確保所有地方都使用相同的文本串。

  對於上面的例子,我們將where改成小寫再提交:

DECLARE @query NVARCHAR(MAX)
DECLARE @param NVARCHAR(MAX)
SET @query = N‘SELECT * FROM PersonTenThousand AS P1 INNER JOIN Province AS P2 ON P1.PId = p2.Id where p1.Id = @Id‘
SET @param = N‘@Id INT‘
EXEC sp_executesql @query,@param,@Id = 7777

  輸出如下:

  技術分享圖片

  由以上的計劃我們可以看到,對於改變了一個單詞的查詢語句,現有計劃不被重用,而是創建一個新的計劃。

  一般來說,使用sp_executesql明確地參數化查詢,使其執行計劃在查詢以可變部分的不同值重新提交時可重用。這提供了可重用計劃在性能上的好處,而沒有管理存儲過程所需要的持續性對象的開銷。這個特性分別由ODBC和OLEDB通過SQLExecDirect和ICommandWithParameters發布。Ado.net可以使用ADO的Command命令和Parameters(參數)提交前述的SELECT語句。如果ADO的Command Prepared屬性設置為FALSE並使用ADO Command(‘SELECT * FROM "Order Details" d, ORDER o WHERE d.OrderId = o.OrderID and d.ProductID=?‘)和ADO Parameters,ADO.NET將使用sp_executesql發送SELECT語句。

  簡單的Ado.netDEMO

技術分享圖片
    static void Main(string[] args)
        {
            string str = "server=KISSDODOG-PC;database=DataExample;uid=sa;pwd=123";
            SqlConnection conn = new SqlConnection(str);
            SqlCommand cmd = conn.CreateCommand();
            cmd.CommandText = "SELECT TOP 1 Name FROM PersonTenThousand WHERE Id = @Id";    //設置操作語句
            cmd.Parameters.Add("@Id", SqlDbType.Int);
            cmd.Parameters["@Id"].Value = 1;
            string Name = "";
            conn.Open();
            Name = cmd.ExecuteScalar().ToString();
            conn.Close();
            Console.WriteLine(Name);
            Console.ReadKey();
        }
技術分享圖片

  SQL Server Profiler監控到的輸出如下:

  技術分享圖片

  5、準備/執行(Prepare/execure)模式

  ODBC和OLEDB提供一種準備/執行模式來將查詢作為一個預定義工作負載提交。和sp_executesql類似,這種模式允許查詢的可變部分明確地參數化。準備階段允許SQL Server為查詢生成執行計劃,並返回執行計劃的一個句柄給應用程序。這個執行計劃句柄被執行階段以不同的參數值執行查詢。這種模式只能用於通過ODBC或OLEDB提交查詢,且不能用在SQL Server本身內部-存儲過程中的查詢不能使用這種模式執行。

  SQL ServerODBC驅動程序提供SQLPrepare和SQLExecuteAPI以支持準備/執行模式。SQL Server OLEDB Provider通過ICommandPrepare接口發布這種模式。ADO.NET的OLEDB.NET provider表現與此類似。

  6、參數嗅探

  雖然一個精心定義的工作負載的目標是將被重用的計劃放入緩沖中,但是有可能將不希望重用的計劃放入緩沖中。當過程第一次被SQL Server調用時,所用的值即被作為生成計劃的一部分,如果這些值是數據和統計的代表,那麽你將會獲得一個有利於存儲過程大部分執行的良好計劃。但是,如果這些數據有所變形,它將可能嚴重地影響查詢的性能。

  (這個示例比較難寫,先想想)

  有時,對於變換存儲過程中的參數,緩沖中的執行計劃可能對查詢性能是一種傷害,因為對於不同的參數,實際上使用不同的執行計劃更好。。如果你確認某個計劃有時工作得好,但是有時在變換參數的時候性能不佳,可以用以下方法避免和修復這個問題:

  • 可以在執行過程之前對其運行sp_recompile來在執行時強制重新編譯計劃,或者每當它執行時使用WITH RECOMPILE選項使其重新編譯。
  • 重新將輸入參數賦給本地參數。這種流行的修復方法強制優化器查詢所引用數據的統計來對可能使用的值做出好的猜測,這可能也確實減少了需要考慮的值。這種方案的問題在於減少了所考慮的值。過程執行總體情況可能變得更糟糕。
  • 可以在創建過程時使用查詢提示OPTIMIZE FOR,並且供給它好的參數以生成在大部分查詢中工作良好的計劃。但是,要理解,一定比例的查詢將不能很好地在所提供的參數下工作。
  • 可以使用計劃向導,這是使查詢以某種方式表現而不需修改過程的一個機制。

執行計劃的重用