1. 程式人生 > 其它 >oracle 並非所有變數都已繫結_PostgreSQL繫結變數窺視

oracle 並非所有變數都已繫結_PostgreSQL繫結變數窺視

技術標籤:oracle 並非所有變數都已繫結

作者介紹

唐成: 網名 osdba,《PostgreSQL修煉之道:從小工到專家》的作者,中啟乘數科技公司聯合創始人,從業20年,從事PostgreSQL資料庫超過10年,擁有十幾年資料庫、作業系統、儲存領域的工作經驗,歷任過網易研究院技術專家、阿里巴巴高階資料庫專家,從事過阿里巴巴PostgreSQL、Greenplum資料庫的架構設計和運維。做過數個百TB以上的Greenplum叢集的維護和擴容工作,解決過很多PostgreSQL、Greenplum方面的疑難雜症。

1. 繫結變數窺視的原理說明

Oracle DBA都知道,繫結變數窺視功能是Oracle資料庫的一個特性,自ORACLE9i版本開始引入,是可以通過引數數“_optim_peek_user_binds”來控制是否開啟,預設是開啟,即為TRUE。這就意味著,第一次以變數的方式執行某類SQL時,會生成第一個執行計劃,後續執行該類SQL語句,即使變數的傳入值不同,但因變數窺視的作用,依然會沿用第一次SQL語句執行時生成的執行計劃,這種特性非常適用於業務表資料分佈比較均勻的場景,執行計劃比較穩定。但對於資料分佈嚴重傾斜的業務表,可能會出現錯誤的執行計劃,在極端情況下,會引發嚴重的效能問題。 當”_optim_peek_user_binds”引數設定為FALSE,即將繫結變數窺視引數特性禁用。那麼已經執行過的某類值的執行計劃將不會發生變化,一旦傳入某個新值時,優化器會自動根據被訪問物件的統計、直方圖等資訊,產生它認為效率最高、成本最低的執行計劃。也就是說,在特性關閉的情況下,該類SQL語句可能會產生更優的執行計劃。 所以為了讓系統的效能不至於大起大落,在很多使用者那裡會關閉繫結變數窺視的功能。 那麼PostgreSQL資料庫在繫結變數的執行計劃這一塊的行為是什麼呢? PostgreSQL資料庫的行為有一些複雜: ·當前5次執行時,每次都會根據實際傳入的實際繫結變數新生成執行計劃進行執行,即每次都是硬解析,同時會記錄這5次的執行計劃; ·當第6次開始執行時,會生成一個通用的執行計劃(generic plan),同時與前5次的執行計劃進行比較,如果比較的結果是通用執行計劃不比前5次的執行計劃差,以後就會固定把這個通用的執行計劃固定下來,這之後即使傳入的值發生變化後,執行計劃也不再變化。這就相當於Oracle打開了繫結變數窺視的功能。 ·當然,當第6次開始執行時,如果通用的執行計劃(generic plan)比前5次的某一個執行計劃差,則以後則每次都重新生成執行計劃,即以後永遠都是硬解析了。 從上面原理可以看出,PostgreSQL資料庫能否不走硬解析,與前5次執行時傳入的實際值有很大的關係,可以想象如果前5次執行時都是一個固定的值,第6次執行時的通用執行計劃與前5次又一樣,這時執行計劃就會固定,如果以後傳進來的值可以生成更好的執行計劃,也不會生成了,這時可能會導致比較大的問題,這與Oracle打開了繫結變數窺視產生了一樣的問題。只是因為PostgreSQL因為有先執行5次,然後第6次比較的機制,讓這個問題出現的概率低了很多,但實際上還是會出現的。 當然,如果每次都是重新生成執行計劃,對於高併發,會降低一些效能。實際上,對於一些重要的系統,每次重新生成執行計劃,會更好一些,因為這種方式防止了系統的效能大起大落。 目前,網上很少有文章介紹這個原理,即使有也是把這個原理介紹的不清楚。

2. 實際測試

2.1 造測試表和資料

下面我們實際測試,來更深的理解這個原理。
1.create table test01(id serial, t text);2.insert into test01(t) select 'tang' from generate_series(1, 1000000);3.insert into test01(t) select 'osdba' from generate_series(1, 2);4.CREATE INDEX idx_test01_t ON test01(t);5.analyze test01;
上面的例子中我們建立了不均勻的資料,即為“tang”的資料是100萬,而為“osdba”的資料是2條。 如果我們按常量來查,執行計劃會走正確:
1.osdba=# explain SELECT count(*) FROM test01 WHERE t = 'tang';
2. QUERY PLAN3.------------------------------------------------------------------------------------------4. Finalize Aggregate (cost=12656.23..12656.24 rows=1 width=8)5. -> Gather (cost=12656.01..12656.22 rows=2 width=8)6. Workers Planned: 27. -> Partial Aggregate (cost=11656.01..11656.02 rows=1 width=8)
8. -> Parallel Seq Scan on test01 (cost=0.00..10614.34 rows=416668 width=0)9. Filter: (t = 'tang'::text)10.(6 rows)11.12.Time: 1.532 ms13.14.osdba=# explain SELECT count(*) FROM test01 WHERE t = 'osdba';15. QUERY PLAN16.--------------------------------------------------------------------------------------17. Aggregate (cost=4.45..4.46 rows=1 width=8)18. -> Index Only Scan using idx_test01_t on test01 (cost=0.42..4.44 rows=1 width=0)19. Index Cond: (t = 'osdba'::text)20.(3 rows)21.22.Time: 1.484 ms
上面可以看到,當按“tang”來查是,走的是全表掃描,而按“osdba”查詢時走的是索引,說明執行計劃都是正確的。

2.2 按繫結變數的第一次測試

下面我們按繫結變數的方式執行:
1.PREPARE myplan(text) AS SELECT count(*) FROM test01 WHERE t = $1;
下面具體看:
1.osdba-mac:~ osdba$ psql2.psql (10.5)3.Type "help" for help.4.5.osdba=# PREPARE myplan(text) AS SELECT count(*) FROM test01 WHERE t = $1;6.PREPARE7.osdba=# explain EXECUTE myplan('tang');8.                                        QUERY PLAN9.------------------------------------------------------------------------------------------10. Finalize Aggregate  (cost=12656.23..12656.24 rows=1 width=8)11.   ->  Gather  (cost=12656.01..12656.22 rows=2 width=8)12.         Workers Planned: 213.         ->  Partial Aggregate  (cost=11656.01..11656.02 rows=1 width=8)14.               ->  Parallel Seq Scan on test01  (cost=0.00..10614.34 rows=416668 width=0)15.                     Filter: (t = 'tang'::text)16.(6 rows)17.18.osdba=# explain EXECUTE myplan('tang');19.                                        QUERY PLAN20.------------------------------------------------------------------------------------------21. Finalize Aggregate  (cost=12656.23..12656.24 rows=1 width=8)22.   ->  Gather  (cost=12656.01..12656.22 rows=2 width=8)23.         Workers Planned: 224.         ->  Partial Aggregate  (cost=11656.01..11656.02 rows=1 width=8)25.               ->  Parallel Seq Scan on test01  (cost=0.00..10614.34 rows=416668 width=0)26.                     Filter: (t = 'tang'::text)27.(6 rows)28.29.osdba=# explain EXECUTE myplan('tang');30.                                        QUERY PLAN31.------------------------------------------------------------------------------------------32. Finalize Aggregate  (cost=12656.23..12656.24 rows=1 width=8)33.   ->  Gather  (cost=12656.01..12656.22 rows=2 width=8)34.         Workers Planned: 235.         ->  Partial Aggregate  (cost=11656.01..11656.02 rows=1 width=8)36.               ->  Parallel Seq Scan on test01  (cost=0.00..10614.34 rows=416668 width=0)37.                     Filter: (t = 'tang'::text)38.(6 rows)39.40.osdba=# explain EXECUTE myplan('tang');41.                                        QUERY PLAN42.------------------------------------------------------------------------------------------43. Finalize Aggregate  (cost=12656.23..12656.24 rows=1 width=8)44.   ->  Gather  (cost=12656.01..12656.22 rows=2 width=8)45.         Workers Planned: 246.         ->  Partial Aggregate  (cost=11656.01..11656.02 rows=1 width=8)47.               ->  Parallel Seq Scan on test01  (cost=0.00..10614.34 rows=416668 width=0)48.                     Filter: (t = 'tang'::text)49.(6 rows)50.51.osdba=# explain EXECUTE myplan('tang');52.                                        QUERY PLAN53.------------------------------------------------------------------------------------------54. Finalize Aggregate  (cost=12656.23..12656.24 rows=1 width=8)55.   ->  Gather  (cost=12656.01..12656.22 rows=2 width=8)56.         Workers Planned: 257.         ->  Partial Aggregate  (cost=11656.01..11656.02 rows=1 width=8)58.               ->  Parallel Seq Scan on test01  (cost=0.00..10614.34 rows=416668 width=0)59.                     Filter: (t = 'tang'::text)60.(6 rows)61.62.osdba=# explain EXECUTE myplan('tang');63.                                        QUERY PLAN64.------------------------------------------------------------------------------------------65. Finalize Aggregate  (cost=12656.23..12656.24 rows=1 width=8)66.   ->  Gather  (cost=12656.01..12656.22 rows=2 width=8)67.         Workers Planned: 268.         ->  Partial Aggregate  (cost=11656.01..11656.02 rows=1 width=8)69.               ->  Parallel Seq Scan on test01  (cost=0.00..10614.34 rows=416668 width=0)70.                     Filter: (t = $1)71.(6 rows)
注意上面執行中的第6次的執行計劃和第5次的執行計劃發生了變化,前5次都是“Filter: (t = ‘tang’::text)”,而第6次變成了“Filter: (t = $1)”,這說明執行計劃變成了通用執行計劃,這時我們把傳進去的值改成“osdba”,發現也會是走全表掃描了,不會走索引了,這時的執行計劃就錯了:
1.osdba=# explain analyze EXECUTE myplan('osdba');2.                                                               QUERY PLAN3.----------------------------------------------------------------------------------------------------------------------------------------4. Finalize Aggregate  (cost=12656.23..12656.24 rows=1 width=8) (actual time=114.069..114.069 rows=1 loops=1)5.   ->  Gather  (cost=12656.01..12656.22 rows=2 width=8) (actual time=113.957..114.865 rows=3 loops=1)6.         Workers Planned: 27.         Workers Launched: 28.         ->  Partial Aggregate  (cost=11656.01..11656.02 rows=1 width=8) (actual time=106.088..106.088 rows=1 loops=3)9.               ->  Parallel Seq Scan on test01  (cost=0.00..10614.34 rows=416668 width=0) (actual time=106.072..106.072 rows=1 loops=3)10.                     Filter: (t = $1)11.                     Rows Removed by Filter: 33333312. Planning time: 0.035 ms13. Execution time: 115.044 ms14.(10 rows)

2.3 按繫結變數的第二次測試

前面的測試時,我們前5次執行時傳進去的值都是“tang”,我們這一次讓前5次中四次傳進去的值是“tang”,有一次是“osdba”:
1.osdba-mac:~ osdba$ psql2.psql (10.5)3.Type "help" for help.4.5.osdba=# PREPARE myplan(text) AS SELECT count(*) FROM test01 WHERE t = $1;6.PREPARE7.osdba=# explain EXECUTE myplan('tang');8.                                        QUERY PLAN9.------------------------------------------------------------------------------------------10. Finalize Aggregate  (cost=12656.23..12656.24 rows=1 width=8)11.   ->  Gather  (cost=12656.01..12656.22 rows=2 width=8)12.         Workers Planned: 213.         ->  Partial Aggregate  (cost=11656.01..11656.02 rows=1 width=8)14.               ->  Parallel Seq Scan on test01  (cost=0.00..10614.34 rows=416668 width=0)15.                     Filter: (t = 'tang'::text)16.(6 rows)17.18.osdba=# explain EXECUTE myplan('tang');19.                                        QUERY PLAN20.------------------------------------------------------------------------------------------21. Finalize Aggregate  (cost=12656.23..12656.24 rows=1 width=8)22.   ->  Gather  (cost=12656.01..12656.22 rows=2 width=8)23.         Workers Planned: 224.         ->  Partial Aggregate  (cost=11656.01..11656.02 rows=1 width=8)25.               ->  Parallel Seq Scan on test01  (cost=0.00..10614.34 rows=416668 width=0)26.                     Filter: (t = 'tang'::text)27.(6 rows)28.29.osdba=# explain EXECUTE myplan('tang');30.                                        QUERY PLAN31.------------------------------------------------------------------------------------------32. Finalize Aggregate  (cost=12656.23..12656.24 rows=1 width=8)33.   ->  Gather  (cost=12656.01..12656.22 rows=2 width=8)34.         Workers Planned: 235.         ->  Partial Aggregate  (cost=11656.01..11656.02 rows=1 width=8)36.               ->  Parallel Seq Scan on test01  (cost=0.00..10614.34 rows=416668 width=0)37.                     Filter: (t = 'tang'::text)38.(6 rows)39.40.osdba=# explain EXECUTE myplan('tang');41.                                        QUERY PLAN42.------------------------------------------------------------------------------------------43. Finalize Aggregate  (cost=12656.23..12656.24 rows=1 width=8)44.   ->  Gather  (cost=12656.01..12656.22 rows=2 width=8)45.         Workers Planned: 246.         ->  Partial Aggregate  (cost=11656.01..11656.02 rows=1 width=8)47.               ->  Parallel Seq Scan on test01  (cost=0.00..10614.34 rows=416668 width=0)48.                     Filter: (t = 'tang'::text)49.(6 rows)50.51.osdba=# explain EXECUTE myplan('osdba');52.                                      QUERY PLAN53.--------------------------------------------------------------------------------------54. Aggregate  (cost=4.45..4.46 rows=1 width=8)55.   ->  Index Only Scan using idx_test01_t on test01  (cost=0.42..4.44 rows=1 width=0)56.         Index Cond: (t = 'osdba'::text)57.(3 rows)
後面我們無論再怎麼執行固定的值,發現每次都是生成新的執行計劃了:
1.osdba=# explain EXECUTE myplan('tang');2.                                        QUERY PLAN3.------------------------------------------------------------------------------------------4. Finalize Aggregate  (cost=12656.23..12656.24 rows=1 width=8)5.   ->  Gather  (cost=12656.01..12656.22 rows=2 width=8)6.         Workers Planned: 27.         ->  Partial Aggregate  (cost=11656.01..11656.02 rows=1 width=8)8.               ->  Parallel Seq Scan on test01  (cost=0.00..10614.34 rows=416668 width=0)9.                     Filter: (t = 'tang'::text)10.(6 rows)11.12.osdba=# explain EXECUTE myplan('tang');13.                                        QUERY PLAN14.------------------------------------------------------------------------------------------15. Finalize Aggregate  (cost=12656.23..12656.24 rows=1 width=8)16.   ->  Gather  (cost=12656.01..12656.22 rows=2 width=8)17.         Workers Planned: 218.         ->  Partial Aggregate  (cost=11656.01..11656.02 rows=1 width=8)19.               ->  Parallel Seq Scan on test01  (cost=0.00..10614.34 rows=416668 width=0)20.                     Filter: (t = 'tang'::text)21.(6 rows)22....23....24....25.26.osdba=# explain EXECUTE myplan('osdba');27.                                      QUERY PLAN28.--------------------------------------------------------------------------------------29. Aggregate  (cost=4.45..4.46 rows=1 width=8)30.   ->  Index Only Scan using idx_test01_t on test01  (cost=0.42..4.44 rows=1 width=0)31.         Index Cond: (t = 'osdba'::text)32.(3 rows)33....34....35.36.osdba=# explain EXECUTE myplan('tang');37.                                        QUERY PLAN38.------------------------------------------------------------------------------------------39. Finalize Aggregate  (cost=12656.23..12656.24 rows=1 width=8)40.   ->  Gather  (cost=12656.01..12656.22 rows=2 width=8)41.         Workers Planned: 242.         ->  Partial Aggregate  (cost=11656.01..11656.02 rows=1 width=8)43.               ->  Parallel Seq Scan on test01  (cost=0.00..10614.34 rows=416668 width=0)44.                     Filter: (t = 'tang'::text)45.(6 rows)
上面的演示是前5次中前四次傳進去的值是“tang”,最後一次是“osdba”,實際上只要前五次中,只要任意有1次或多次傳進去的是“osdba”,不一定要求最後一次是“osdba”時,都不會走通用的執行計劃,這個結果大家可以測試。 這就驗證了我們前面的理論。

3. PostgreSQL 12的plan_cache_mode配置引數

在PostgreSQL11及一下的版本中,因為繫結變數窺視,雖然比Oracle出現的概率低,但還是有一定的概率導致執行計劃走錯。那麼在PostgreSQL中是否也有類似Oracle的隱含引數把繫結變數窺視關掉的功能?答案是PostgreSQL12提供了這個功能。 在PostgreSQL 12提供了plan_cache_mode配置引數,可以取以下三個值:

· auto: 這時預設值,即預設情況下與PostgreSQL11及以下版本相同的行為。

· force_custom_plan: 相當於關閉繫結變數窺視,永遠進行硬解析。

·force_generic_plan: 走通用的固定執行計劃(generic plan)

所以對於一些非常重要的系統,可以把“plan_cache_mode”配置成“force_custom_plan”,避免執行計劃的錯誤,如下所示:
1.osdba-mac:pgdata12 osdba$ psql2.psql (12.1)3.Type "help" for help.4.5.osdba=# PREPARE myplan(text) AS SELECT count(*) FROM test01 WHERE t = $1;6.PREPARE7.osdba=# set plan_cache_mode to force_custom_plan;8.SET9.osdba=# explain EXECUTE myplan('osdba');10.                                      QUERY PLAN11.--------------------------------------------------------------------------------------12. Aggregate  (cost=4.45..4.46 rows=1 width=8)13.   ->  Index Only Scan using idx_test01_t on test01  (cost=0.42..4.44 rows=1 width=0)14.         Index Cond: (t = 'osdba'::text)15.(3 rows)16.17.osdba=# explain EXECUTE myplan('tang');18.                                        QUERY PLAN19.------------------------------------------------------------------------------------------20. Finalize Aggregate  (cost=12656.23..12656.24 rows=1 width=8)21.   ->  Gather  (cost=12656.01..12656.22 rows=2 width=8)22.         Workers Planned: 223.         ->  Partial Aggregate  (cost=11656.01..11656.02 rows=1 width=8)24.               ->  Parallel Seq Scan on test01  (cost=0.00..10614.34 rows=416668 width=0)25.                     Filter: (t = 'tang'::text)26.(6 rows)
79c468604d7b0666df788bd935a75862.png