簡(jiǎn)介
我們平時(shí)所寫(xiě)的SQL語(yǔ)句本質(zhì)只是獲取數(shù)據(jù)的邏輯,而不是獲取數(shù)據(jù)的物理路徑。當(dāng)我們寫(xiě)的SQL語(yǔ)句傳到SQL Server的時(shí)候,查詢(xún)分析器會(huì)將語(yǔ)句依次進(jìn)行解析(Parse)、綁定(Bind)、查詢(xún)優(yōu)化(Optimization,有時(shí)候也被稱(chēng)為簡(jiǎn)化)、執(zhí)行(Execution)。除去執(zhí)行步驟外,前三個(gè)步驟之后就生成了執(zhí)行計(jì)劃,也就是SQL Server按照該計(jì)劃獲取物理數(shù)據(jù)方式,最后執(zhí)行步驟按照?qǐng)?zhí)行計(jì)劃執(zhí)行查詢(xún)從而獲得結(jié)果。但查詢(xún)優(yōu)化器不是本篇的重點(diǎn),本篇文章主要講述查詢(xún)優(yōu)化器在生成執(zhí)行計(jì)劃之后,緩存執(zhí)行計(jì)劃的相關(guān)機(jī)制以及常見(jiàn)問(wèn)題。
為什么需要執(zhí)行計(jì)劃緩存
從簡(jiǎn)介中我們知道,生成執(zhí)行計(jì)劃的過(guò)程步驟所占的比例眾多,會(huì)消耗掉各CPU和內(nèi)存資源。而實(shí)際上,查詢(xún)優(yōu)化器生成執(zhí)行計(jì)劃要做更多的工作,大概分為3部分:
首先,根據(jù)傳入的查詢(xún)語(yǔ)句文本,解析表名稱(chēng)、存儲(chǔ)過(guò)程名稱(chēng)、視圖名稱(chēng)等。然后基于邏輯數(shù)據(jù)操作生成代表查詢(xún)文本的樹(shù)。
第二步是優(yōu)化和簡(jiǎn)化,比如說(shuō)將子查詢(xún)轉(zhuǎn)換成對(duì)等的連接、優(yōu)先應(yīng)用過(guò)濾條件、刪除不必要的連接(比如說(shuō)有索引,可能不需要引用原表)等。
第三步根據(jù)數(shù)據(jù)庫(kù)中的統(tǒng)計(jì)信息,進(jìn)行基于成本(Cost-based)的評(píng)估。
上面三個(gè)步驟完成之后,才會(huì)生成多個(gè)候選執(zhí)行計(jì)劃。雖然我們的SQL語(yǔ)句邏輯上只有一個(gè),但是符合這個(gè)邏輯順序的物理獲取數(shù)據(jù)的順序卻可以有多條,打個(gè)比方,你希望從北京到上海,即可以做高鐵,也可以做飛機(jī),但從北京到上海這個(gè)描述是邏輯描述,具體怎么實(shí)現(xiàn)路徑有多條。那讓我們?cè)倏匆粋€(gè)SQL Server中的舉例,比如代碼清單1中的查詢(xún)。
SELECT *
FROM A INNER JOIN B ON a.a=b.b
INNER JOIN C ON c.c=a.a
代碼清單1.
對(duì)于該查詢(xún)來(lái)說(shuō),無(wú)論A先Inner join B還是B先Inner Join C,結(jié)果都是一樣的,因此可以生成多個(gè)執(zhí)行計(jì)劃,但一個(gè)基本原則是SQL Server不一定會(huì)選擇最好的執(zhí)行計(jì)劃,而是選擇足夠好的計(jì)劃,這是由于評(píng)估所有的執(zhí)行計(jì)劃的成本所消耗的成本不應(yīng)該過(guò)大。最終,SQL Server會(huì)根據(jù)數(shù)據(jù)的基數(shù)和每一步所消耗的CPU和IO的成本來(lái)評(píng)估執(zhí)行計(jì)劃的成本,所以執(zhí)行計(jì)劃的選擇重度依賴(lài)于統(tǒng)計(jì)信息,關(guān)于統(tǒng)計(jì)信息的相關(guān)內(nèi)容,我就不細(xì)說(shuō)了。
對(duì)于前面查詢(xún)分析器生成執(zhí)行計(jì)劃的過(guò)程不難看出,該步驟消耗的資源成本也是驚人的。因此當(dāng)同樣的查詢(xún)執(zhí)行一次以后,將其緩存起來(lái)將會(huì)大大減少執(zhí)行計(jì)劃的編譯,從而提高效率,這就是執(zhí)行計(jì)劃緩存存在的初衷。
執(zhí)行計(jì)劃所緩存的對(duì)象
執(zhí)行計(jì)劃所緩存的對(duì)象分為4類(lèi),分別是:
編譯后的計(jì)劃:編譯的執(zhí)行計(jì)劃和執(zhí)行計(jì)劃的關(guān)系就和MSIL和C#的關(guān)系一樣。
執(zhí)行上下文:在執(zhí)行編譯的計(jì)劃時(shí),會(huì)有上下文環(huán)境。因?yàn)榫幾g的計(jì)劃可以被多個(gè)用戶共享,但查詢(xún)需要存儲(chǔ)SET信息以及本地變量的值等,因此上下文環(huán)境需要對(duì)應(yīng)執(zhí)行計(jì)劃進(jìn)行關(guān)聯(lián)。執(zhí)行上下文也被稱(chēng)為Executable Plan。
游標(biāo):存儲(chǔ)的游標(biāo)狀態(tài)類(lèi)似于執(zhí)行上下文和編譯的計(jì)劃的關(guān)系。游標(biāo)本身只能被某個(gè)連接使用,但游標(biāo)關(guān)聯(lián)的執(zhí)行計(jì)劃可以被多個(gè)用戶共享。
代數(shù)樹(shù):代數(shù)樹(shù)(也被稱(chēng)為解析樹(shù))代表著查詢(xún)文本。正如我們之前所說(shuō),查詢(xún)分析器不會(huì)直接引用查詢(xún)文本,而是代數(shù)樹(shù)。這里或許你會(huì)有疑問(wèn),代數(shù)樹(shù)用于生成執(zhí)行計(jì)劃,這里還緩存代數(shù)樹(shù)干毛???這是因?yàn)橐晥D、Default、約束可能會(huì)被不同查詢(xún)重復(fù)使用,將這些對(duì)象的代數(shù)樹(shù)緩存起來(lái)省去了解析的過(guò)程。
比如說(shuō)我們可以通過(guò)dm_exec_cached_plans這個(gè)DMV找到被緩存的執(zhí)行計(jì)劃,如圖1所示。
圖1.被緩存的執(zhí)行計(jì)劃
那究竟這幾類(lèi)對(duì)象緩存所占用的內(nèi)存相關(guān)信息該怎么看呢?我們可以通過(guò)dm_os_memory_cache_counters這個(gè)DMV看到,上述幾類(lèi)被緩存的對(duì)象如圖2所示。
圖2.在內(nèi)存中這幾類(lèi)對(duì)象緩存所占用的內(nèi)存
另外,執(zhí)行計(jì)劃緩存是一種緩存。而緩存中的對(duì)象會(huì)根據(jù)算法被替換掉。對(duì)于執(zhí)行計(jì)劃緩存來(lái)說(shuō),被替換的算法主要是基于內(nèi)存壓力。而內(nèi)存壓力會(huì)被分為兩種,既內(nèi)部壓力和外部壓力。外部壓力是由于Buffer Pool的可用空間降到某一臨界值(該臨界值會(huì)根據(jù)物理內(nèi)存的大小而不同,如果設(shè)置了最大內(nèi)存則根據(jù)最大內(nèi)存來(lái))。內(nèi)部壓力是由于執(zhí)行計(jì)劃緩存中的對(duì)象超過(guò)某一個(gè)閾值,比如說(shuō)32位的SQL Server該閾值為40000,而64位中該值被提升到了160000。
這里重點(diǎn)說(shuō)一下,緩存的標(biāo)識(shí)符是查詢(xún)語(yǔ)句本身,因此select * from SchemaName.TableName和Select * from TableName雖然效果一致,但需要緩存兩份執(zhí)行計(jì)劃,所以一個(gè)Best Practice是在引用表名稱(chēng)和以及其他對(duì)象的名稱(chēng)時(shí),請(qǐng)帶上架構(gòu)名稱(chēng)。
基于被緩存的執(zhí)行計(jì)劃對(duì)語(yǔ)句進(jìn)行調(diào)優(yōu)
被緩存的執(zhí)行計(jì)劃所存儲(chǔ)的內(nèi)容非常豐富,不僅僅包括被緩存的執(zhí)行計(jì)劃、語(yǔ)句,還包括被緩存執(zhí)行計(jì)劃的統(tǒng)計(jì)信息,比如說(shuō)CPU的使用、等待時(shí)間等。但這里值得注意的是,這里的統(tǒng)計(jì)只算執(zhí)行時(shí)間,而不算編譯時(shí)間。比如說(shuō)我們可以利用代碼清單2中的代碼根據(jù)被緩存的執(zhí)行計(jì)劃找到數(shù)據(jù)庫(kù)中耗時(shí)最長(zhǎng)的20個(gè)查詢(xún)語(yǔ)句。
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.通過(guò)執(zhí)行計(jì)劃緩存找到數(shù)據(jù)庫(kù)總耗時(shí)最長(zhǎng)的20個(gè)查詢(xún)語(yǔ)句
上面的語(yǔ)句您可以修改Order By來(lái)根據(jù)不同的條件找到你希望找到的語(yǔ)句,這里就不再細(xì)說(shuō)了。
相比較于無(wú)論是服務(wù)端Trace還是客戶端的Profiler,該方法有一定優(yōu)勢(shì),如果通過(guò)捕捉Trace再分析的話,不僅費(fèi)時(shí)費(fèi)力,還會(huì)給服務(wù)器帶來(lái)額外的開(kāi)銷(xiāo),通過(guò)該方法找到耗時(shí)的查詢(xún)語(yǔ)句就會(huì)簡(jiǎn)單很多。但是該統(tǒng)計(jì)僅僅基于上次實(shí)例重啟或者沒(méi)有運(yùn)行DBCC FreeProcCache之后。但該方法也有一些弊端,比如說(shuō):
類(lèi)似索引重建、更新統(tǒng)計(jì)信息這類(lèi)語(yǔ)句是不緩存的,而這些語(yǔ)句成本會(huì)非常高。
緩存可能隨時(shí)會(huì)被替換掉,因此該方法無(wú)法看到不再緩存中的語(yǔ)句。
該統(tǒng)計(jì)信息只能看到執(zhí)行成本,無(wú)法看到編譯成本。
沒(méi)有參數(shù)化的緩存可能同一個(gè)語(yǔ)句呈現(xiàn)不同的執(zhí)行計(jì)劃,因此出現(xiàn)不同的緩存,在這種情況下統(tǒng)計(jì)信息無(wú)法累計(jì),可能造成不是很準(zhǔn)確。
執(zhí)行計(jì)劃緩存和查詢(xún)優(yōu)化器的矛盾
還記得我們之前所說(shuō)的嗎,執(zhí)行計(jì)劃的編譯和選擇分為三步,其中前兩步僅僅根據(jù)查詢(xún)語(yǔ)句和表等對(duì)象的metadata,在執(zhí)行計(jì)劃選擇的階段要重度依賴(lài)于統(tǒng)計(jì)信息,因此同一個(gè)語(yǔ)句僅僅是參數(shù)的不同,查詢(xún)優(yōu)化器就會(huì)產(chǎn)生不同的執(zhí)行計(jì)劃,比如說(shuō)我們來(lái)看一個(gè)簡(jiǎn)單的例子,如圖3所示。
圖3.僅僅是由于不同的參數(shù),查詢(xún)優(yōu)化器選擇不同的執(zhí)行計(jì)劃
大家可能會(huì)覺(jué)得,這不是挺好的嘛,根據(jù)參數(shù)產(chǎn)生不同的執(zhí)行計(jì)劃。那讓我們?cè)倏紤]一個(gè)問(wèn)題,如果將上面的查詢(xún)放到一個(gè)存儲(chǔ)過(guò)程中,參數(shù)不能被直接嗅探到,當(dāng)?shù)谝粋€(gè)執(zhí)行計(jì)劃被緩存后,第二次執(zhí)行會(huì)復(fù)用第一次的執(zhí)行計(jì)劃!雖然免去了編譯時(shí)間,但不好的執(zhí)行計(jì)劃所消耗的成本會(huì)更高!讓我們來(lái)看這個(gè)例子,如圖4所示。
圖4.不同的參數(shù),卻是完全一樣的執(zhí)行計(jì)劃!
再讓我們看同一個(gè)例子,把執(zhí)行順序顛倒后,如圖5所示。
圖5.執(zhí)行計(jì)劃完全變了
我們看到,第二次執(zhí)行的語(yǔ)句,完全復(fù)用了第一次的執(zhí)行計(jì)劃。那總會(huì)有一個(gè)查詢(xún)犧牲。比如說(shuō)當(dāng)參數(shù)為4時(shí)會(huì)有5000多條,此時(shí)索引掃描應(yīng)該最高效,但圖4卻復(fù)用了上一個(gè)執(zhí)行計(jì)劃,使用了5000多次查找?。?!這無(wú)疑是低效率的。而且這種情況出現(xiàn)會(huì)非常讓DBA迷茫,因?yàn)樵诰彺嬷械膱?zhí)行計(jì)劃不可控,緩存中的對(duì)象隨時(shí)可能被刪除,誰(shuí)先執(zhí)行誰(shuí)后執(zhí)行產(chǎn)生的性能問(wèn)題往往也讓DBA頭疼。
由這個(gè)例子我們看出,查詢(xún)優(yōu)化器希望盡可能選擇高效的執(zhí)行計(jì)劃,而執(zhí)行計(jì)劃緩存卻希望盡可能的重用緩存,這兩種機(jī)制在某些情況會(huì)產(chǎn)生沖突。
在下篇文章中,我們將會(huì)繼續(xù)來(lái)看由于執(zhí)行計(jì)劃緩存和查詢(xún)分析器的沖突,以及編譯執(zhí)行計(jì)劃所帶來(lái)的常見(jiàn)問(wèn)題和解決方案。
小結(jié)
本篇文章中,我們簡(jiǎn)單講述了查詢(xún)優(yōu)化器生成執(zhí)行計(jì)劃的過(guò)程,以及執(zhí)行計(jì)劃緩存的機(jī)制。當(dāng)查詢(xún)優(yōu)化器和執(zhí)行計(jì)劃緩存以某種不好的情況交匯時(shí),將產(chǎn)生一些問(wèn)題。在下篇文章中,我們會(huì)繼續(xù)探索SQL Server中的執(zhí)行計(jì)劃緩存。
以上內(nèi)容是小編給大家介紹的SQL Server中的執(zhí)行計(jì)劃緩存(上)的全部敘述,希望大家喜歡。
您可能感興趣的文章:- 淺析SQL Server 聚焦索引對(duì)非聚集索引的影響
- MySQL中主鍵索引與聚焦索引之概念的學(xué)習(xí)教程
- SQLSERVER中得到執(zhí)行計(jì)劃的兩種方式
- SqlServer 執(zhí)行計(jì)劃及Sql查詢(xún)優(yōu)化初探
- SQL Server中參數(shù)化SQL寫(xiě)法遇到parameter sniff ,導(dǎo)致不合理執(zhí)行計(jì)劃重用的快速解決方法
- 淺析SQL Server中的執(zhí)行計(jì)劃緩存(下)
- 強(qiáng)制SQL Server執(zhí)行計(jì)劃使用并行提升在復(fù)雜查詢(xún)語(yǔ)句下的性能
- 淺析SQL Server的聚焦使用索引和查詢(xún)執(zhí)行計(jì)劃