咱們平時所寫的SQL語句本質只是獲取數據的邏輯,而不是獲取數據的物理路徑。當咱們寫的SQL語句傳到SQL Server的時候,查詢分析器會將語句依次進行解析(Parse)、綁定(Bind)、查詢優化(Optimization,有時候也被稱爲簡化)、執行(Execution)。除去執行步驟外,前三個步驟以後就生成了執行計劃,也就是SQL Server按照該計劃獲取物理數據方式,最後執行步驟按照執行計劃執行查詢從而得到結果。但查詢優化器不是本篇的重點,本篇文章主要講述查詢優化器在生成執行計劃以後,緩存執行計劃的相關機制以及常見問題。html
從簡介中咱們知道,生成執行計劃的過程步驟所佔的比例衆多,會消耗掉各CPU和內存資源。而實際上,查詢優化器生成執行計劃要作更多的工做,大概分爲3部分:算法
上面三個步驟完成以後,纔會生成多個候選執行計劃。雖然咱們的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
好比說咱們能夠經過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中的執行計劃緩存。