數據庫引擎的工作流程可以歸納為接收請求、執行請求和返回結果。數據庫引擎每接收到一個新的查詢請求(Query Request),查詢優化器就會執行以下工作流程:
- 編譯請求:對TSQL語句進行語法解析,編譯請求,生成TSQL語句表示的邏輯結構。
- 查詢優化:根據TSQL語句的邏輯結構,生成多個預估的執行方案,並根據統計信息,評估每個預估方案的開銷,選擇開銷最低的方案作為最優方案。
- 執行計划:根據最優方案生成執行計划,也就是把TSQL語句中的邏輯操作符轉變為物理操作符,把執行計划傳遞給存儲引擎,並把執行計划緩存到內存中。
- 響應請求:存儲引擎執行查詢計划,記錄每個查詢的執行信息,最后把查詢的結果返回到客戶端。
把執行計划存儲到內存的目的是為了復用執行計划,減少編譯查詢請求的時間消耗和CPU消耗。當數據庫引擎再次接收到相同或相似的查詢請求時,數據庫引擎探測到該請求的執行計划已經被緩存,那么就會跳過編譯請求的過程,直接復用已經緩存的執行計划。
數據庫引擎並不是把查詢計划永久保存在內存中,而是會根據內存的壓力,智能地剔除一些創建時間早、復用頻次少的執行計划。為了實現計划緩存的精准清理,數據庫引擎需要對查詢和查詢計划進行定位和統計,定位通過請求的語句句柄和計划句柄來實現,清理通過查詢統計來實現。
一,語句句柄和計划句柄
數據庫引擎要實現查詢計划的復用,必須能夠識別查詢已經執行過,這就需要對查詢語句進行標記;查詢的執行計划也會被標記,這就需要用到兩個唯一值:
- sql_handle:用以唯一標識一段TSQL文本(Batch或SP),TSQL文本存儲在SQL Manager Cache(SQLMGR)中。
- plan_handle:用於唯一標識一個已編輯的查詢計划,查詢計划存儲在計划緩存(Plan Cache)中。
sql_handle和plan_handle是如何生成的?
- 對於ad hoc查詢,sql_handle是基於整體的SQL Text生成的哈希值;如果一個batch包含多個TSQL語句,那么多個TSQL語句作為一個整體,batch中的查詢字句擁有相同的sql_handle值,但是有不同的偏移量。
- 對於執行的SP、觸發器或函數等數據庫對象,sql_handle是由database ID 和 object ID 派生的哈希值。
- plan_handle是由整體(批處理或SP)生成的已編譯計划派生的哈希值。
sql_handle和plan_handle 之間具有1對多的關系。一個sql_handle 能夠生成多個查詢計划,對應多個plan_handle,但是每個plan_handle只能對應一個sql_handle 。sql_handle對於每一個batch都是唯一的,但是,如果執行batch的條件發生改變,比如set 選項發生變化,那么數據庫引擎在執行同樣的batch時,會生成新的執行計划,產生新的plan_handle,但是sql_handle不變。想要了解更詳細的信息,請閱讀《2.0 Sql_Handle and Plan_Handle Explained》。
1,SQL句柄
sql_handle是一個token,用於唯一標記查詢文本所屬的batch或sp,把sql_handle傳遞給 sys.dm_exec_sql_text()動態管理函數,並結合偏移 statement_start_offset和statement_end_offset,可以抽取出單個查詢的SQL文本。
函數 sys.dm_exec_sql_text(sql_handle | plan_handle)用於獲得整個Batch的TSQL文本,由於TSQL文本都是以nvarchar(max)類型存儲的,一個nvarchar是2個字節,因此,一般情況下,字節偏移量都是2的倍數。
2,計划句柄
plan_handle是一個token,是整個Execution Plan的哈希值,用於唯一標識一個batch或sp的執行計划,把plan_handle傳遞給sys.dm_exec_query_plan(plan_handle)動態管理函數,可以獲取整體(batch或sp)的showplan。
3,查詢計划(query plan)
查詢計划是指查詢語句的顯示計划(showplan),動態管理視圖 sys.dm_exec_query_plan 返回以XML格式表示的showplan,它只能返回整個batch執行的showplan,不能單獨查看某一個子句的執行計划。要想查看單個子句的執行計划,可以通過動態管理視圖 sys.dm_exec_text_query_plan 來實現,該視圖返回以文本格式表示的showplan:
sys.dm_exec_query_plan(plan_handle) sys.dm_exec_text_query_plan ( plan_handle , { statement_start_offset | 0 | DEFAULT } , { statement_end_offset | -1 | DEFAULT } )
對於文本查詢計划,需要指定特定的語句的偏移statement_start_offset 和 statement_end_offset,才能顯示單個子句的showplan。
二,抽取查詢語句
動態管理視圖 sys.dm_exec_query_stats 緩存的是單個查詢語句的執行計划,而sql_handle指向的是整個Batch或SP的句柄值,因此,在該視圖中,可能存在多個相同的sql_handle。
為了獲得單個查詢語句的文本,必須通過偏移量從整體(Batch語句)中抽取,偏移量的單位是字節,字節數量從0開始:
- statement_start_offset:語句開始偏移的字節序號
- statement_end_offset:語句結束偏移的字節序號,-1 表示TSQL文本的末尾
把sql_handle傳遞給 sys.dm_exec_sql_text()動態管理函數,並結合偏移 statement_start_offset和statement_end_offset,可以抽取出單個查詢的SQL文本,抽取查詢語句的腳本是:
select substring(st.text ,qs.statement_start_offset/2+1, ( case when qs.statement_end_offset = -1 then len(convert(nvarchar(max), st.text)) else (qs.statement_end_offset - qs.statement_start_offset)/2 end ) ) as individual_query ,st.text as entire_query from sys.dm_exec_query_stats qs outer apply sys.dm_exec_sql_text(qs.sql_handle) as st
三,查詢的統計數據
數據庫引擎會把每一個查詢請求的執行信息保存起來,例如,查詢的文本,查詢等待的時長,執行的時間,消耗的資源等,並對這些信息進行匯總和統計,這些匯總之后的數據就是查詢統計,存儲到內存結構 DMV:sys.dm_exec_query_stats中。在該視圖中,每一行數據都表示一個查詢語句的統計數據。
請求的執行信息都經過匯總之后,存儲到DMV:sys.dm_exec_query_stats中,從該統計數據中,可以找出對性能影響最大的查詢請求,由於該DMV存儲的是累加值,在使用數據之前,一定要關注記錄的開始時間:
- creation_time:計划編譯的時間
- last_execution_time:最近一次計划開始執行的時間
這兩個時間表示查詢計划的第一次執行和最后一次執行的時間戳。
1,查看語句級別的統計數據
執行計划的重編譯次數,執行查詢的總時間,邏輯讀和物理讀的次數等計數器,是觀察查詢執行情況的重要指標:
- plan_generation_num:表示執行計划產生的數量,表示同一個TSQL文本重新編譯的次數;
- creation_time :計划編譯的時間
- execution_count:計划執行的次數
- total_elapsed_time和max_elapsed_time:查詢計划完成的總時間,單詞elapsed是指單個語句執行的總時間,包括 waiting的時間或 CPU工作(worker)的時間,單位是微秒(us),一微秒是千分之一毫秒(ms)
- total_worker_time 和 max_worker_time:CPU工作的總時間和最大時間,單位是微秒(us)
- total_logical_reads和max_logical_reads:查詢計划執行的邏輯讀的總次數;
- total_logical_writes和max_logical_writes:查詢計划執行的邏輯寫的總次數;
- total_physical_reads和 max_physical_reads:查詢計划執行的物理讀的總次數;
- total_rows和max_rows:查詢返回的數據行的總數量
- total_dop和max_dop:並發執行的並發度的累加和
- total_grant_kb和max_grant_kb:該查詢計划收到的預留授予內存(reserved memory grant)的總量,單位是KB
- total_used_grant_kb和max_used_grant_kb:該查詢計划使用的預留授予內存(reserved memory grant)的總量,單位是KB
- total_ideal_grant_kb:該查詢計划預估的理想授予內存(ideal memory grant)的總量,單位是KB
- total_splils、max_spills和min_spills:查詢計划在完成一次執行時,出現頁溢出的總頁數;
以下腳本用於查看執行計划在單個語句級別上的平均數據,並按照平均執行時間排序,獲取 top 111 的數據:
select top 111 qs.execution_count, qs.total_rows/qs.execution_count as avg_rows, qs.total_worker_time/qs.execution_count/1000 as avg_worker_ms, qs.total_elapsed_time/qs.execution_count/1000 as avg_elapsed_ms, qs.total_physical_reads/qs.execution_count as avg_physical_reads, qs.total_logical_reads/qs.execution_count as avg_logical_reads, qs.total_logical_writes/qs.execution_count as avg_logical_writes, qs.creation_time, qs.plan_generation_num, --st.text as entire_query, substring(st.text, qs.statement_start_offset/2 + 1, ( case when qs.statement_end_offset = -1 then len(convert(nvarchar(max), st.text)) else (qs.statement_end_offset -qs.statement_start_offset)/2 end) ) as individual_query from sys.dm_exec_query_stats qs cross apply sys.dm_exec_sql_text(qs.sql_handle) as st order by avg_elapsed_ms desc
2,查看存儲過程級別的查詢統計
對於緩存的存儲過程,數據庫引擎把SP相關的統計數據緩存在視圖:sys.dm_exec_procedure_stats 中,每一行數據都表示一個SP的統計數據:
select top 111 db_name(ps.database_id) as db_name ,ps.database_id ,object_schema_name(ps.object_id,ps.database_id)+'.'+object_name(ps.object_id,ps.database_id) as proc_name ,ps.type_desc as proc_type ,ps.cached_time ,ps.execution_count ,ps.total_worker_time/ps.execution_count/1000 as avg_worker_ms ,ps.total_elapsed_time/ps.execution_count/1000 as avg_elapsed_ms ,ps.total_physical_reads/ps.execution_count as avg_physical_reads ,ps.total_logical_reads/ps.execution_count as avg_logical_reads ,ps.total_logical_writes/ps.execution_count as avg_logical_writes from sys.dm_exec_procedure_stats ps where ps.database_id<32767 order by avg_elapsed_ms desc
對於database_id 為 32767,這個id是資源數據庫(Resource Database)預留的ID,一般情況下,用戶創建的數據庫ID都會小於該數值。
四,顯示被緩存的計划
函數 sys.dm_exec_query_plan 以XML格式返回指定batch或SP的查詢計划,參數是plan_handle,這意味着,函數返回的是整個語句(Batch或SP)的showplan,XML格式是可視化的,也可以返回文本格式的showplan。
select top 111 qs.execution_count, qs.total_rows/qs.execution_count as avg_rows, qs.total_worker_time/qs.execution_count/1000 as avg_worker_ms, qs.total_elapsed_time/qs.execution_count/1000 as avg_elapsed_ms, qs.total_physical_reads/qs.execution_count as avg_physical_reads, qs.total_logical_reads/qs.execution_count as avg_logical_reads, qs.total_logical_writes/qs.execution_count as avg_logical_writes, qs.creation_time, qs.plan_generation_num, st.text as entire_query, substring(st.text, qs.statement_start_offset/2 + 1, ( case when qs.statement_end_offset = -1 then len(convert(nvarchar(max), st.text)) else (qs.statement_end_offset -qs.statement_start_offset)/2 end) ) as individual_query, qp.query_plan 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 order by avg_elapsed_ms desc
五,計划的統計信息
動態管理視圖:sys.dm_exec_cached_plans 中,每一個行存儲一個查詢計划,通過該視圖,可以查看已緩存的查詢計划、查詢文本、緩存計划占用的內存、緩存計划復用的次數等信息。
select cp.refcounts ,cp.usecounts ,cp.size_in_bytes ,cp.cacheobjtype ,cp.objtype ,st.text as batch_sql --,cp.plan_handle from sys.dm_exec_cached_plans cp outer apply sys.dm_exec_sql_text(cp.plan_handle) st
六,存儲過程的統計信息
對於緩存的存儲過程,sys.dm_exec_procedure_stats 返回聚合的性能統計數據,每一行都代表一個緩存的存儲過程計划,
select db_name(p.database_id) as dbname ,o.name as proc_name ,p.type_desc ,p.cached_time ,p.execution_count ,p.total_worker_time ,p.max_worker_time ,p.total_physical_reads ,p.max_physical_reads ,p.total_logical_writes ,p.max_logical_writes ,p.total_logical_reads ,p.max_logical_reads ,p.total_elapsed_time ,p.max_elapsed_time ,p.total_spills ,p.min_spills ,p.max_spills ,p.plan_handle ,cp.bucketid ,cp.refcounts ,cp.usecounts ,cp.size_in_bytes ,pn.query_plan from sys.dm_exec_procedure_stats p inner join sys.objects o on p.object_id=o.object_id inner join sys.dm_exec_cached_plans as cp on p.plan_handle=cp.plan_handle outer apply sys.dm_exec_query_plan(p.plan_handle) as pn
參考文檔:
Execution Related Dynamic Management Views and Functions (Transact-SQL)
