談一談SQL Server中的執行計劃緩存(上)

談一談SQL Server中的執行計劃緩存(上)

簡介

    咱們平時所寫的SQL語句本質只是獲取數據的邏輯,而不是獲取數據的物理路徑。當咱們寫的SQL語句傳到SQL Server的時候,查詢分析器會將語句依次進行解析(Parse)、綁定(Bind)、查詢優化(Optimization,有時候也被稱爲簡化)、執行(Execution)。除去執行步驟外,前三個步驟以後就生成了執行計劃,也就是SQL Server按照該計劃獲取物理數據方式,最後執行步驟按照執行計劃執行查詢從而得到結果。但查詢優化器不是本篇的重點,本篇文章主要講述查詢優化器在生成執行計劃以後,緩存執行計劃的相關機制以及常見問題。html

 

爲何須要執行計劃緩存

    從簡介中咱們知道,生成執行計劃的過程步驟所佔的比例衆多,會消耗掉各CPU和內存資源。而實際上,查詢優化器生成執行計劃要作更多的工做,大概分爲3部分:算法

  • 首先,根據傳入的查詢語句文本,解析表名稱、存儲過程名稱、視圖名稱等。而後基於邏輯數據操做生成表明查詢文本的樹。
  • 第二步是優化和簡化,好比說將子查詢轉換成對等的鏈接、優先應用過濾條件、刪除沒必要要的鏈接(好比說有索引,可能不須要引用原表)等。
  • 第三步根據數據庫中的統計信息,進行基於成本(Cost-based)的評估。

 

    上面三個步驟完成以後,纔會生成多個候選執行計劃。雖然咱們的SQL語句邏輯上只有一個,可是符合這個邏輯順序的物理獲取數據的順序卻能夠有多條,打個比方,你但願從北京到上海,便可以作高鐵,也能夠作飛機,但從北京到上海這個描述是邏輯描述,具體怎麼實現路徑有多條。那讓咱們再看一個SQL Server中的舉例,好比代碼清單1中的查詢。sql

1: SELECT *數據庫

2: FROM A INNER JOIN B ON a.a=b.b緩存

3: INNER JOIN C ON c.c=a.a服務器

代碼清單1.架構

 

    對於該查詢來講,不管A先Inner join B仍是B先Inner Join C,結果都是同樣的,所以能夠生成多個執行計劃,但一個基本原則是SQL Server不必定會選擇最好的執行計劃,而是選擇足夠好的計劃,這是因爲評估全部的執行計劃的成本所消耗的成本不該該過大。最終,SQL Server會根據數據的基數和每一步所消耗的CPU和IO的成原本評估執行計劃的成本,因此執行計劃的選擇重度依賴於統計信息,關於統計信息的相關內容,我就不細說了。性能

    對於前面查詢分析器生成執行計劃的過程不難看出,該步驟消耗的資源成本也是驚人的。所以當一樣的查詢執行一次之後,將其緩存起來將會大大減小執行計劃的編譯,從而提升效率,這就是執行計劃緩存存在的初衷。優化

 

執行計劃所緩存的對象

    執行計劃所緩存的對象分爲4類,分別是:spa

  • 編譯後的計劃:編譯的執行計劃和執行計劃的關係就和MSIL和C#的關係同樣。
  • 執行上下文:在執行編譯的計劃時,會有上下文環境。由於編譯的計劃能夠被多個用戶共享,但查詢須要存儲SET信息以及本地變量的值等,所以上下文環境須要對應執行計劃進行關聯。執行上下文也被稱爲Executable Plan。
  • 遊標:存儲的遊標狀態相似於執行上下文和編譯的計劃的關係。遊標自己只能被某個鏈接使用,但遊標關聯的執行計劃能夠被多個用戶共享。
  • 代數樹:代數樹(也被稱爲解析樹)表明着查詢文本。正如咱們以前所說,查詢分析器不會直接引用查詢文本,而是代數樹。這裏或許你會有疑問,代數樹用於生成執行計劃,這裏還緩存代數樹幹毛啊?這是由於視圖、Default、約束可能會被不一樣查詢重複使用,將這些對象的代數樹緩存起來省去了解析的過程。

 

    好比說咱們能夠經過dm_exec_cached_plans這個DMV找到被緩存的執行計劃,如圖1所示。

圖1.被緩存的執行計劃

 

    那究竟這幾類對象緩存所佔用的內存相關信息該怎麼看呢?咱們能夠經過dm_os_memory_cache_counters這個DMV看到,上述幾類被緩存的對象如圖2所示。

圖2.在內存中這幾類對象緩存所佔用的內存

 

    另外,執行計劃緩存是一種緩存。而緩存中的對象會根據算法被替換掉。對於執行計劃緩存來講,被替換的算法主要是基於內存壓力。而內存壓力會被分爲兩種,既內部壓力和外部壓力。外部壓力是因爲Buffer Pool的可用空間降到某一臨界值(該臨界值會根據物理內存的大小而不一樣,若是設置了最大內存則根據最大內存來)。內部壓力是因爲執行計劃緩存中的對象超過某一個閾值,好比說32位的SQL Server該閾值爲40000,而64位中該值被提高到了160000。

這裏重點說一下,緩存的標識符是查詢語句自己,所以select * from SchemaName.TableName和Select * from TableName雖然效果一致,但須要緩存兩份執行計劃,因此一個Best Practice是在引用表名稱和以及其餘對象的名稱時,請帶上架構名稱。

   

基於被緩存的執行計劃對語句進行調優

    被緩存的執行計劃所存儲的內容很是豐富,不只僅包括被緩存的執行計劃、語句,還包括被緩存執行計劃的統計信息,好比說CPU的使用、等待時間等。但這裏值得注意的是,這裏的統計只算執行時間,而不算編譯時間。好比說咱們能夠利用代碼清單2中的代碼根據被緩存的執行計劃找到數據庫中耗時最長的20個查詢語句。

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

SELECT TOP 20

CAST(qs.total_elapsed_time / 1000000.0 AS DECIMAL(28, 2))

AS [Total Duration (s)]

, CAST(qs.total_worker_time * 100.0 / qs.total_elapsed_time

AS DECIMAL(28, 2)) AS [% CPU]

, CAST((qs.total_elapsed_time - qs.total_worker_time)* 100.0 /

qs.total_elapsed_time AS DECIMAL(28, 2)) AS [% Waiting]

, qs.execution_count

, CAST(qs.total_elapsed_time / 1000000.0 / qs.execution_count

AS DECIMAL(28, 2)) AS [Average Duration (s)]

, SUBSTRING (qt.text,(qs.statement_start_offset/2) + 1,

((CASE WHEN qs.statement_end_offset = -1

THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2

ELSE qs.statement_end_offset

END - qs.statement_start_offset)/2) + 1) AS [Individual Query

, qt.text AS [Parent Query]

, DB_NAME(qt.dbid) AS DatabaseName

, qp.query_plan

FROM sys.dm_exec_query_stats qs

CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as qt

CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp

WHERE qs.total_elapsed_time > 0

ORDER BY qs.total_elapsed_time DESC

代碼清單2.經過執行計劃緩存找到數據庫總耗時最長的20個查詢語句

 

    上面的語句您能夠修改Order By來根據不一樣的條件找到你但願找到的語句,這裏就再也不細說了。

    相比較於不管是服務端Trace仍是客戶端的Profiler,該方法有必定優點,若是經過捕捉Trace再分析的話,不只費時費力,還會給服務器帶來額外的開銷,經過該方法找到耗時的查詢語句就會簡單不少。可是該統計僅僅基於上次實例重啓或者沒有運行DBCC FreeProcCache以後。但該方法也有一些弊端,好比說:

  • 相似索引重建、更新統計信息這類語句是不緩存的,而這些語句成本會很是高。
  • 緩存可能隨時會被替換掉,所以該方法沒法看到再也不緩存中的語句。
  • 該統計信息只能看到執行成本,沒法看到編譯成本。
  • 沒有參數化的緩存可能同一個語句呈現不一樣的執行計劃,所以出現不一樣的緩存,在這種狀況下統計信息沒法累計,可能形成不是很準確。

 

執行計劃緩存和查詢優化器的矛盾

    還記得咱們以前所說的嗎,執行計劃的編譯和選擇分爲三步,其中前兩步僅僅根據查詢語句和表等對象的metadata,在執行計劃選擇的階段要重度依賴於統計信息,所以同一個語句僅僅是參數的不一樣,查詢優化器就會產生不一樣的執行計劃,好比說咱們來看一個簡單的例子,如圖3所示。

圖3.僅僅是因爲不一樣的參數,查詢優化器選擇不一樣的執行計劃

 

    你們可能會以爲,這不是挺好的嘛,根據參數產生不一樣的執行計劃。那讓咱們再考慮一個問題,若是將上面的查詢放到一個存儲過程當中,參數不能被直接嗅探到,當第一個執行計劃被緩存後,第二次執行會複用第一次的執行計劃!雖然免去了編譯時間,但很差的執行計劃所消耗的成本會更高!讓咱們來看這個例子,如圖4所示。

圖4.不一樣的參數,倒是徹底同樣的執行計劃!

 

    再讓咱們看同一個例子,把執行順序顛倒後,如圖5所示。

圖5.執行計劃徹底變了

 

    咱們看到,第二次執行的語句,徹底複用了第一次的執行計劃。那總會有一個查詢犧牲。好比說當參數爲4時會有5000多條,此時索引掃描應該最高效,但圖4卻複用了上一個執行計劃,使用了5000屢次查找!!!這無疑是低效率的。並且這種狀況出現會很是讓DBA迷茫,由於在緩存中的執行計劃不可控,緩存中的對象隨時可能被刪除,誰先執行誰後執行產生的性能問題每每也讓DBA頭疼。

   由這個例子咱們看出,查詢優化器但願儘量選擇高效的執行計劃,而執行計劃緩存卻但願儘量的重用緩存,這兩種機制在某些狀況會產生衝突。

    在下篇文章中,咱們將會繼續來看因爲執行計劃緩存和查詢分析器的衝突,以及編譯執行計劃所帶來的常見問題和解決方案。

 

小結

    本篇文章中,咱們簡單講述了查詢優化器生成執行計劃的過程,以及執行計劃緩存的機制。當查詢優化器和執行計劃緩存以某種很差的狀況交匯時,將產生一些問題。在下篇文章中,咱們會繼續探索SQL Server中的執行計劃緩存。

相關文章
相關標籤/搜索