1. 程式人生 > >SQLServer中的執行計劃緩存由於長時間緩存對性能造成的幹擾

SQLServer中的執行計劃緩存由於長時間緩存對性能造成的幹擾

找到 產品 ans ads 實戰 alt 開發 實時計算 query

本文出處:http://www.cnblogs.com/wy123/p/7190785.html

(保留出處並非什麽原創作品權利,本人拙作還遠遠達不到,僅僅是為了鏈接到原文,因為後續對可能存在的一些錯誤進行修正或補充,無他)


先拋出一個性能問題,前幾天遇到一個生產環境性能極其低下的存儲過程,開發人員根據具體的業務邏輯和返回的數據量,猜測到這個存儲過程的執行應該不會有這麽慢。
當時意識到可能是執行計劃緩存的問題,因為當前這個存儲過程的寫法還是比較遵守參數化SQL的規範的(如果是動態即席查詢SQL就不會有問題了)
有意思的是,開發人員提供的相關參數,跟編譯時候的參數竟然是一模一樣的,這也是本文重點要表達的重點。

於是去查詢當前服務器上緩存的執行計劃的到底是怎麽樣的,緩存這個執行計劃的時間,以及當前緩存的這個執行計劃編譯時候的參數。
在查詢到對應存儲過程緩存的執行計劃之後,發現其執行計劃,確實跟當前直接帶入參數執行SQL的執行計劃有很大的差異。
當然該問題不完全與Parameter Sniffing完全一致,因此暫不討論Parameter Sniffing問題。
執行計劃由於已經被緩存了起來,當前查詢也命中了先前執行計劃的緩存,但緩存的這個執行計劃對目前的查詢來說並可能不是最(相對)優化的,
畢竟該執行計劃已經緩存了超過1天了。
那麽,緩存的執行計劃到底適不適合當前語句的執行?如果不適合於當前語句的執行,又該怎麽處理,類似問題從長遠看,該怎麽避免?

特定語句的執行計劃緩存信息

參考如下sql,查詢出來某些指定語句的執行計劃緩存信息。

  SELECT  st.Text,
        SUBSTRING(
            st.Text, (qs.statement_start_offset/2)+1,
            ((CASE qs.statement_end_offset
            WHEN -1 THEN DATALENGTH(st.Text)
            ELSE qs.statement_end_offset
            END - qs.statement_start_offset)/
2)+1 ), qp.query_plan, qs.plan_handle, qs.sql_handle, DB_NAME(st.dbid) as dbname, qs.creation_time, qs.last_execution_time, getdate() as currenttime, qs.execution_count, qs.last_worker_time, qs.last_physical_reads, qs.last_logical_reads, qs.last_elapsed_time FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as st OUTER APPLY sys.dm_exec_query_plan(qs.plan_handle) as qp WHERE 1=1 and st.text like %模糊匹配存儲過程中具體標記性的sql文本% order by creation_time desc

對於類似如下的查詢結果,從查詢到的結果中可以看出,直接打開query_plan的xml,可以看到這個存儲過程中的語句的執行計劃情況,當前我這裏僅僅是一個示例

技術分享

對於當前這個緩存的執行計劃的編譯參數,可以將query_plan的xml信息復制出來,用notepad++之類的文本編輯器格式化之後顯示,查詢器編譯時候的參數,
參考截圖,在xml信息的最後面一部分,或者可以直接按照關鍵字搜索ParameterList,就可以找到編譯執行計劃時候的具體的參數值了。
在ParameterList的子節點中column就是參數,ParameterCompiledValue的值就是編譯執行計劃的值。
這樣一來,就可以確定,緩存的執行計劃在編譯的時候的參數與當前運行的參數是否存在較大的差異,以及緩存的執行計劃是在什麽生成的,緩存了多久。

技術分享

對於一開始提到的問題,結果就是當前執行的存儲過程中的語句,其執行計劃已經被緩存超過了一天,因為尚未帶到觸發“重編譯”的條件,該緩存繼續保留在內存中。
鑒於數據庫中的數據是不斷變化的,當時編譯的執行計劃,在當前時間來看,即便是參數完全一致,也不一定適用於當前的查詢,性能問題也因此產生。
為什麽會編譯出來一個與當前完全不一致的執行計劃,並且緩存到目前為止還沒有被清理?

個人猜測有兩個原因,不過也不完全確定,
一是基於當時的數據分布情況(統計信息)得到的一個執行計劃,可能當時本身的統計信息就不是準確的,但是有沒有外界因素促使執行計劃重編譯
二是當時編譯的執行計劃本身就是不合理的,執行計劃的編譯與多種因素相關,甚至是內存壓力也會導致無法編譯出來一個相對較優的執行計劃,
SQL Server執行計劃的生成,並不一定總是“最高效”的,只是相對高效的,在內存壓力小的時候編譯出來的執行計劃,可能與內存壓力大的時候編譯出來的執行計劃存在差異
其實第一條猜測的原因存在一定的自相矛盾,
後者的可能性更大,因為如果導致重編譯的因素沒有發生變化,同樣的參數,當前執行也會跟緩存的執行計劃一樣,如果存在導致重編譯的因素,那麽緩存的執行計劃本身也要被清理掉。

更何況如果緩存了某一個較少概率出現的參數,或者類似於產生parameter sniff的參數問題(當然parameter sniff問題就另當別論),影響甚至就更大。

如下截圖是某生產環境服務器上的,可以看到,在業務存儲過程或者SQL語句的執行計劃緩存,多的可以緩存了超過20天,另外還有緩存的超過了3天,4天的。

技術分享

  據觀察,SQL Server在對待執行計劃緩存的內存占用,是輕易不會去清理的,可以認為這部分內存(相對於數據緩存)的要求優先級是比較高的,
  也就是說在data cache面臨較大壓力的時候(PLE可以低到一分鐘之內),部分執行計劃依然被緩存的好好的。
  很可惜,在某些情況下,緩存的執行計劃,非但沒有提供更好的性能,反倒是因為不適用於當前的查詢,拖慢了性能。

執行計劃緩存的基礎知識

1,什麽是執行計劃緩存

  SQL Server查詢引擎接收到sql語句之後,首先會對其進行語法,語義的解析,然後進行編譯,之後生成執行計劃,對於可滿足緩存的要求的sql語句,SQLServer會對其進行緩存。

2,執行計劃緩存的作用

  減少SQL的編譯,SQLServer接收到存在執行計劃緩存的SQL語句的時候,可以直接使用緩存的執行計劃進行執行,避免重新編譯造成的資源上和時間上的消耗。

3,當前執行計劃緩存占用的空間

  參考如下SQL,可以使用sys.dm_os_memory_clerks或者sys.dm_exec_cached_plans 來查詢執行計劃緩存已經占用的空間。

--CACHESTORE_SQLCP,動態sql,預編譯sql執行計劃
select * from sys.dm_os_memory_clerks
where type = CACHESTORE_SQLCP

--proc:存儲過程
--prepared:預定義語句
--Adhoc:動態查詢
select sum(size_in_kb)/1024.0/1024.0 as size_in_GB FROM
(
    SELECT  objtype ,  
            size_in_bytes/1024.0 AS  size_in_kb
    FROM    sys.dm_exec_cached_plans  
    WHERE objtype IN ( Proc,Prepared,Adhoc)
)t

如下是某服務器上的執行計劃緩存信息,兩者查詢出來的結果基本上一致。

技術分享

  4,執行計劃緩存可最大占用的空間

    執行計劃占用的內存空間是一種稱之為Stolen Memory的內存類型,Stolen Memory包括執行計劃緩存,以及Session實時計算所需要的內存,比如排序,聚合計算,hash join等等。
    SQL Server中Stolen Memory的分配有一個公式,在SQL Server 2005 SP1之後,大概如下
    如果Target Mermory(也即SQL Server可用的最大內存空間)

    1)不超過8GB的情況下:Stolen Memory = Target Mermory * 75%
    2)如果Target Mermory在8GB~64GB之間:Stolen Memory = 8 * 75% + (Target Mermory - 8)*50%
    3)如果Target Mermory大於64GB時:Stolen Memory = 8 * 75% + (64 - 8)*50% + (Target Mermory - 64)*25%

    可見,隨著最大可用內存的增加,Stolen Memory並不是線性遞增的,而是增加程度趨於減少(增加程度的導數是減少的)
    另外在SQL Server之後的版本中也在控制Stolen Memory的最大可用內容量,具體參考《SQL Server 2012 實施與管理實戰指南》+ 這裏。

    需要註意的是,執行計劃緩存可最大占用的空間僅僅是Stolen Memory的一部分,並不是Stolen Memory的全部。

    那麽問題就來了,這裏可以認為,執行計劃緩存可最大占用的空間是無法直接控制的,
    如果SQL Server的Target Mermory越大,Stolen Memory也就越大,Stolen Memory越大,執行計劃緩存可用的內存空間就越大,
    雖然執行計劃有老化清理機制,單個人感覺還不夠靈活,或者說可控不強(現在才明白,其他關系型數據庫,開放出來很多可配置的參數的作用了,當然不是說可以隨意配置,想改就改)
    執行計劃緩存占用的內存空間越大,真的就越好嗎?
    肯定不是,一開始提到的問題就歸因於此,正是因為Stolen Memory尚未得到內存壓力,執行計劃緩存就一直存儲在內存中,但是存儲的相對較久的執行計劃,並非適合於當前的查詢。

  5,關於即席查詢(ad hoc)SQL的配置

    這一點根本文關系不大,簡單提一下,對於ad hoc,也即動態sql,因為其參數很有可能每次都發生變化,類似SQL就沒有必要緩存了,
    第一次執行的時候僅存儲一個執行計劃的“存根”而不是存儲整個執行計劃,只有相同參數的SQL執行超過一次只有才緩存下來整個完整的執行計劃。

--開啟 optimize for ad hoc workloads
sp_CONFIGURE show advanced options,1
reconfigure
go
sp_CONFIGURE optimize for ad hoc workloads,1
reconfigure
go

技術分享

這一點配置的意義何在?內存對數據庫來說是很重要的資源,如果Stolen Memory各項內存使用都可以配置,在盡可能滿足Stolen Memory的基礎上,更多更久地緩存數據才是王道。

如果解決潛在可能過期的執行計劃

    上述分析說明,在Stolen Memory尚未感受到內存壓力,或者是執行計劃清理機制尚未清理老化的執行計劃緩存的時候,執行計劃緩存是依舊保存在內存中的。
    但是這種緩存的執行計劃,因為是語句編譯的時候的數據進行生成的計劃,是不一定適合於當前查詢的,或者是對於當前查詢不是最優化的。
    這就需要,在某些情況下,需要人為地清理執行計劃緩存。
    SQL Server提供了一個系統功能DBCC FREEPROCCACHE去清理執行計劃緩存,參數是上文提到的具體的執行計劃句柄(plan_handle),不加參數就全清了,註意一下即可。

-- Remove the specific plan from the cache.清楚特定的執行計劃
DBCC FREEPROCCACHE (0x060006001ECA270EC0215D05000000000000000000000000);
GO

    對於DBCC FREEPROCCACHE去清理執行特定計劃緩存,此種方式僅可以用來應急使用。
    如果經常發生類似問題,於情於理,我個人覺得是說不過去的,絕對是DBA該負得起的責任。

更多的時候,已經緩存過一段時間的執行計劃,哪些是本應該被清理的,那些是可以繼續緩存使用的?這一點很難主觀地來界定。
甚至也沒有有效的參考信息,因為你不知道3天之前編譯的那個執行計劃,緩存了三天之後,對於當前的查詢,是不是依舊是最(較)優化的。
這一點可以在實踐中根據具體的情況進行探索,比如可以根據生成實踐,清理超過1天的執行計劃緩存,或者把實踐這個閾值設置的大一點,但是不建議太長,比如三天五天的
個人認為,既然執行計劃能夠緩存,那麽被清理之後,依然能夠緩存,但是清楚之後重編譯的執行計劃,之準確性上會有更大的提高,一次重編譯的代價也是值得的。
理想情況下,一個執行計劃編譯之後,能夠緩存(重用執行計劃)一天,相對來說也是比較理想的了。

當然,這裏是不包括“采用with recompile語句存儲過程的重編譯或者option(recompile)基於語句的重編”來避免執行計劃緩存的。
執行計劃緩存本是處於節約資源與提高性能方面的考慮,只不過是凡事都具體兩面性,有利必有弊。
正常情況下,也不建議采用with recompile或者option(recompile)來強制編譯存儲過程或者SQL語句,大多數時候遵循好規範即可。

大概想了一下,使用類似如下SQL,采用動態執行DBCC FREEPROCCACHE的方式,可以達到預期的目的。
當然,執行方式,執行時間,時間閾值可以根據具體情況進行調整。

  
if object_id(tempdb..#t_remove_expired_plan) is not null
    drop table #t_remove_expired_plan
GO

create table #t_remove_expired_plan
(
    id int identity(1,1),
    plan_handle varbinary(500)
)
GO


insert into #t_remove_expired_plan (plan_handle) 
select  qs.plan_handle
from sys.dm_exec_query_stats qs 
where creation_time< dateadd(hh,-24,getdate())
GO


declare @exists_data bit = 1
declare @v_plan_handle varbinary(500)
declare @str_sql varchar(1000)
while @exists_data = 1
begin
    select top 1 @v_plan_handle = plan_handle from #t_remove_expired_plan
    if(@v_plan_handle is not null)
    begin
        execute sp_executesql NDBCC FREEPROCCACHE(@plan_handle) ,N@plan_handle varbinary(500),@plan_handle = @v_plan_handle
    end
    delete top (1) from #t_remove_expired_plan

    if exists(select 1 from #t_remove_expired_plan)
    begin
        set @exists_data = 1
    end
    else 
    begin
        set @exists_data = 0
    end
end

技術分享

總結:

   執行計劃緩存原本是為了減少編譯SQL帶來的資源以及時間上的消耗,在可存在可重用的執行計劃緩存的情況下提高SQL的執行效率
   對於老化的緩存計劃的處理方式,SQL Server有自己的清理機制,但是仍舊缺乏一定的靈活性。
   但是在某些特殊情況下,對於性能問題,緩存的執行計劃可能會幹擾到當前SQL的執行效率,
   可能會存外部環境沒有問題(服務器資源,CPU,IO,內存,網絡等),SQL本身寫法也沒有問題,也不會出現缺少索引等情況,但是執行效率打不到預期的情況,
   這時就好考慮是不是緩存的執行計劃導致當前SQL采用了不合理的執行方式。
 

   微軟的平臺,一直以來本著簡單易容,快速上手的特點,對用戶做了大量的封裝(屏蔽),在個性化的可配置化上,與其他數據庫尤其是開源產品相比顯得有些不足。
   不過可以通過間接的方式,來達到類似於“可配置化”的目的。

SQLServer中的執行計劃緩存由於長時間緩存對性能造成的幹擾