天天看點

一個執行計劃異常變更的案例 - 外傳之rolling invalidation

剛做完一次網絡切換支援,得空寫一篇,其實今兒取了巧,這篇文章是之前寫過的,碰巧又是這次“執行計劃異常變更”案例涉及的一個知識點,是以再次翻出來。

之前的幾篇文章:

《一個執行計劃異常變更的案例 - 前傳》

《一個執行計劃異常變更的案例 - 外傳之綁定變量窺探》

《一個執行計劃異常變更的案例 - 外傳之檢視綁定變量值的幾種方法》

做性能測試,有一條SQL,使用了綁定變量,檢視V$SQLAREA發現version_count是2,

一個執行計劃異常變更的案例 - 外傳之rolling invalidation

檢視V$SQL,發現有兩條記錄,分别對應了0和1兩個child cursor:

一個執行計劃異常變更的案例 - 外傳之rolling invalidation

再檢視這兩個child cursor對應的執行計劃:

child cursor:0

一個執行計劃異常變更的案例 - 外傳之rolling invalidation

child cursor:1

一個執行計劃異常變更的案例 - 外傳之rolling invalidation

發現除了成本代價略有不同,其他通路路徑完全一緻。應用保證使用的相同使用者執行這條SQL語句,綁定變量窺探關閉。問題就來了,為何同一條SQL有兩個child cursor,且執行計劃一緻?

再抛一下,通過V$SQL_SHARED_CURSOR視圖可以檢視遊标失效的原因,對比這兩個cursor,不同之一就是這個ROLL_INVALID_MISMATCH字段的值,0号cursor值為N,1号cursor值為Y,

一個執行計劃異常變更的案例 - 外傳之rolling invalidation

另外,REASON字段,0号cursor顯示了内容,1号cursor該字段值為空。

這個問題通過Rolling Cursor Invalidations with DBMS_STATS.AUTO_INVALIDATE (文檔 ID 557661.1)這篇文章能夠很好地解釋。

大體意思是在10g之前,使用dbms_stats采集對象統計資訊,除非no_invalidate設為TRUE,否則所有緩存在Library Cache中的遊标都會失效,下次執行時需要做硬解析。隐患就是對于一個OLTP系統,會産生一次硬解析風暴,消耗大量的CPU、庫緩存以及共享池latch的争用,進而影響應用系統的響應時間。如果設定no_invalidate為FALSE,則現有存儲的遊标不會使用更新的對象統計資訊,仍使用舊有執行計劃,直到下次硬解析,要麼因為時間太久,導緻cursor被刷出,要麼手工執行flush重新整理了共享池,這兩種情況下會重新執行硬解析,根據更新的對象統計資訊,生成更新的執行計劃。這麼做其實還是有可能出現硬解析風暴,特别是OLTP系統,高并發時候,有SQL語句頻繁通路。

使用dbms_stats.gather_XXX_stats的時候,有個參數no_invalidate,

一個執行計劃異常變更的案例 - 外傳之rolling invalidation

預設是AUTO_INVALIDATE,這表示是由Oracle來決定什麼時候讓依賴的遊标失效。

10g之後,如果采集對象統計資訊使用的no_invalidate參數是auto_invalidate,則Oracle會采用如下操作,來緩解可能的硬解析風暴。

1.執行dbms_stats,所有依賴于這個已分析對象的緩存cursor遊标會被标記為rolling invalidation,并且記錄此時刻是T0。

2.下次某個session需要解析這個标記為rolling invalidation的cursor遊标時,會設定一個時間戳,其取值為_optimizer_invalidation_period定義的最大值範圍内的一個随機數。之是以是随機數,就是為了分散這些 invalidation的遊标,防止出現硬解析風暴。參數_optimizer_invalidation_period預設值是18000秒,5小時。記錄這次解析時間為T1,時間戳值為Tmax。但此時,仍是重用了已有遊标,不會做硬解析,不會使用更新的統計資訊來生成一個新的執行計劃。

3.接下來這個遊标(标記了rolling invalidation和時間戳)的每次使用時,都會判斷目前時刻T2是否超過了時間戳Tmax。如果未超過,則仍使用已存在的cursor。如果Tmax已經超過了,則會讓此遊标失效,建立一個新的版本(一個新的child cursor子遊标),使用更新的執行計劃,并且新的子遊标會标記V$SQL_SHARED_CURSOR中ROLL_INVALID_MISMATCH的值。

這些和我上面碰見的情況基本一緻。

MOS是附帶了一個實驗,可以根據實驗來體會下這種情況。

1.為了容易觀察,設定_optimizer_invalidation_period為1分鐘,

一個執行計劃異常變更的案例 - 外傳之rolling invalidation

2.建立測試表,并采集統計資訊,

一個執行計劃異常變更的案例 - 外傳之rolling invalidation

3.執行一次目标SQL,并檢視V$SQL_SHARED_CURSOR資訊,

一個執行計劃異常變更的案例 - 外傳之rolling invalidation

此時檢視這條SQL的解析和執行次數都是1,

一個執行計劃異常變更的案例 - 外傳之rolling invalidation

4.再執行一次目标SQL,

檢視這條SQL的解析和執行次數是2,

一個執行計劃異常變更的案例 - 外傳之rolling invalidation

有人曾說過,11g中未必會按照_optimizer_invalidation_period參數定義的時間産生新的子遊标,我上面用的環境是11g,确實如此,等了2分鐘,執行目标SQL,仍隻有一個子遊标。這樣的好處有人也說了,就是更加的随機,因為如果嚴格按照參數設定的時間失效,則有可能頻繁使用的遊标會在逾時後某一時刻集中做硬解析,還是會有資源的影響,隻是時間推遲了,是以如果是在逾時值基礎上又有随機分布,則可能會将硬解析的影響降到最低。

又等了一段時間,再查詢V$SQL,

一個執行計劃異常變更的案例 - 外傳之rolling invalidation

确實産生了兩個子遊标,這裡需要注意FIRST_LOAD_TIME的時間是一樣的,因為他是parent父遊标的建立時間,顯然這兩個子遊标肯定是對應同一個父遊标,不同的就是LAST_LOAD_TIME,這是子遊标的使用時間。

一個執行計劃異常變更的案例 - 外傳之rolling invalidation

再看看V$SQL_SHARED_CURSOR,

一個執行計劃異常變更的案例 - 外傳之rolling invalidation

兩個子遊标資訊,隻有一個R項值有差别,R是ROLL_INVALID_MISMATCH,0号子遊标是N,1号子遊标是Y,看看官方文檔對這個字段的說明,

一個執行計劃異常變更的案例 - 外傳之rolling invalidation

表示的就是标記為rolling invalidation的遊标,已經是超過了時間視窗,此時0号子遊标已經過期,1号子遊标使用最新的統計資訊,來生成最新的執行計劃。

這就解釋了為何同一條SQL,執行計劃一緻,但卻有兩個子遊标的情況。

MOS中還描述了一些遊标使用的場景:

1.如果一個遊标被标記為rolling invalidation,但是再不會做解析,則這個遊标不會失效,最終還是可能根據LRU被刷出共享池。

2.如果一個遊标被标記為rolling invalidation,後面隻會解析一次,那麼這個遊标依然不會失效(僅僅使用時間戳标記),最終還是可能根據LRU被刷出共享池。

3.頻繁使用的遊标,在超過時間戳Tmax值後,下次解析時就會被置為失效。

很明顯,上面的這些方法是有效的,因為失效标記僅僅适用于這些頻繁重用的遊标,對于其他場景的遊标可以忽略,未有影響。

總結:

1.凡事有因果,同一條SQL,執行計劃相同,但産生了兩個子遊标,總會有其的原因,上面遊标失效标記可能是一個原因,當然還有可能是其他原因。

2.對于FIRST_LOAD_TIME這些字段的了解,還是要準确些,不能斷章取義。