為什么函數影響性能
在SQL語句中,如果不合理的使用函數(Function)就會嚴重影響性能,其實這里想說的是PL/SQL中的自定義函數,反而對於一些內置函數而言,影響性能的可能性較小。那么為什么SQL語句當中,不合理的使用函數會影響性能呢?
在SELECT語句中調用函數時,那么查詢返回的結果集中的每一行都會調用該函數。如果該函數需要執行1秒,返回的結果集是10行,那么此時SQL語句就需要10秒,如果該函數執行時間需要3秒,返回的結果集是10000條記錄,那么這個時間就是30000秒~= 500分鍾。是否很恐怖!因為生產環境中自定義函數有時候會出現復雜的業務邏輯,導致自定義函數性能開銷較高,如果出現不合理調用,那么很容易就會出現性能問題。 下面我們簡單來演示一個例子。
CREATE TABLE TEST
(
ID NUMBER
);
DECLARE RowIndex NUMBER;
BEGIN
RowIndex :=1;
WHILE RowIndex <= 8 LOOP
INSERT INTO TEST
SELECT RowIndex FROM DUAL;
RowIndex := RowIndex +1;
END LOOP;
COMMIT;
END;
/
--創建函數SLOW_FUNCTION,使用DBMS_LOCK.SLEEP休眠2秒,模擬這個函數較慢。
CREATE OR REPLACE FUNCTION SLOW_FUNCTION(p_value IN NUMBER)
RETURN NUMBER
AS
BEGIN
DBMS_LOCK.SLEEP(2);
RETURN p_value+10;
END;
/
SQL> SET TIMING ON;
SQL> SELECT * FROM TEST;
ID
----------
1
2
3
4
5
6
7
8
8 rows selected.
Elapsed: 00:00:00.00
SQL> SELECT SLOW_FUNCTION(ID) FROM TEST;
SLOW_FUNCTION(ID)
-----------------
11
12
13
14
15
16
17
18
8 rows selected.
Elapsed: 00:00:16.01
如果在WHERE當中使用函數,由於有8條記錄,而每次調用函數需要Sleep 2秒, 總共耗費2*8=16秒。所以在WHERE條件中,也要謹慎使用自定義函數。
SQL> SET AUTOTRACE OFF;
SQL>
SQL> SELECT * FROM TEST
2 WHERE SLOW_FUNCTION(ID)>15;
ID
----------
6
7
8
Elapsed: 00:00:16.01
SQL>
什么情況下函數影響性能
其實自定義函數影響性能,主要在於函數(Function)調用的次數或函數(Function)本身的業務邏輯是否復雜,如果SELECT查詢中調用次數很少,影響還是非常小的。如下所示,如果只調用一次的話,這個影響還是非常小的。
SQL> SELECT SLOW_FUNCTION(ID) FROM TEST WHERE ID=2;
SLOW_FUNCTION(ID)
-----------------
12
Elapsed: 00:00:02.01
其次,如果函數實現的業務邏輯簡單,即使調用次數多,對性能影響也很小。我們改寫一下下面函數,通過實驗來驗證測試一下,如下所示:
CREATE OR REPLACE FUNCTION SLOW_FUNCTION(p_value IN NUMBER)
RETURN NUMBER
AS
BEGIN
RETURN p_value+10;
END;
/
然后創建一個存儲過程,來測試一下循環次數對性能的影響。
CREATE OR REPLACE PROCEDURE TEST_SLOW_FUNCTION(ITER IN NUMBER)
AS RESULT VARCHAR2(60);
BEGIN
FOR I IN 1 .. ITER LOOP
SELECT SLOW_FUNCTION(I) INTO RESULT FROM DUAL;
END LOOP;
END TEST_SLOW_FUNCTION;
/
如下所示,當函數業務邏輯簡單,性能開銷很低時,循環次數對性能的影響反而很小。10次循環調用跟1000000次循環調用差別是3秒多。可見如果自定義函數的業務邏輯簡單,循環次數對性能影響較小。
SQL> EXEC TEST_SLOW_FUNCTION(10);
PL/SQL procedure successfully completed.
Elapsed: 00:00:00.01
SQL> EXEC TEST_SLOW_FUNCTION(10000);
PL/SQL procedure successfully completed.
Elapsed: 00:00:00.40
SQL> EXEC TEST_SLOW_FUNCTION(100000);
PL/SQL procedure successfully completed.
Elapsed: 00:00:03.64
如何優化解決問題
對SQL中調用的自定義函數,可以通過等價改寫成多表關聯語句。避免產生大量的遞歸調用,另外就是想法設法減少函數被調用次數。SQL中盡量避免使用自定義函數(不是不能用,而是要看場合,盡量避免使用的原因:因為你寫的函數,可能會被其它人濫用,會偏離當初你寫這個函數的初衷),或者盡量避免函數中實現復雜業務邏輯。
另外,如果實在不能避免的話,就盡量減少調用次數。另外,也有一些技巧可以優化自定義函數性能,下面內容基本參考或翻譯Efficient Function Calls From SQL這篇文章。
首先我們還是准備測試環境,使用最初的那個模擬函數。
CREATE OR REPLACE FUNCTION SLOW_FUNCTION(p_value IN NUMBER)
RETURN NUMBER
AS
BEGIN
DBMS_LOCK.SLEEP(2);
RETURN p_value+10;
END;
/
標量子查詢緩存(scalar subquery caching)
標量子查詢緩存(scalar subquery caching)會通過緩存結果減少SQL對函數(Function)的調用次數, ORACLE會在內存中構建一個哈希表來緩存標量子查詢的結果。當然前提是有重復值的情況下。如果沒有重復值,其實這種方法是沒有任何效果的。如下測試所示
然后我們刪除源數據,構造重復數據,然后我們測試對比看一下實驗結果吧,不同的寫法差別10秒,如果在復雜的實際環境中,這種性能差別還會被放大。
TRUNCATE TABLE TEST;
INSERT INTO TEST
SELECT 1 FROM DUAL UNION ALL
SELECT 1 FROM DUAL UNION ALL
SELECT 1 FROM DUAL UNION ALL
SELECT 2 FROM DUAL UNION ALL
SELECT 2 FROM DUAL UNION ALL
SELECT 2 FROM DUAL UNION ALL
SELECT 3 FROM DUAL UNION ALL
SELECT 3 FROM DUAL;
COMMIT;
通俗來將,當使用標量子查詢的時候,ORACLE會將子查詢結果緩存在哈希表中, 如果后續的記錄出現同樣的值,優化器通過緩存在哈希表中的值,判斷重復值不用重復調用函數,直接使用上次計算結果即可。從而減少調用函數次數,從而達到優化性能的效果。另外在ORACLE 10和11中, 哈希表只包含了255個Buckets,也就是說它能存儲255個不同值,如果超過這個范圍,就會出現散列沖突,那些出現散列沖突的值就會重復調用函數,即便如此,依然能達到大幅改善性能的效果。
DETERMINISTIC關鍵字
ORACLE通過關鍵字DETERMINISTIC來表明一個函數(Function)是確定性的,確定性函數可以用於創建基於函數的索引。這個僅僅能在ORACLE 10g以后的版本中使用。它會影響函數如何緩存在SQL中。如下測試所示:
新建帶有DETERMINISTIC關鍵字的函數
CREATE OR REPLACE FUNCTION SLOW_FUNCTION_DERM(p_value IN NUMBER)
RETURN NUMBER
DETERMINISTIC
AS
BEGIN
DBMS_LOCK.SLEEP(2);
RETURN p_value+10;
END;
/
SQL> SELECT SLOW_FUNCTION(ID) FROM TEST ;
SLOW_FUNCTION(ID)
-----------------
11
11
11
12
12
12
13
13
8 rows selected.
Elapsed: 00:00:16.01
SQL> SELECT SLOW_FUNCTION_DERM(ID) FROM TEST ;
SLOW_FUNCTION_DERM(ID)
----------------------
11
11
11
12
12
12
13
13
8 rows selected.
Elapsed: 00:00:08.00
將函數標記為確定性確實可以提高性能,但DETERMINISTIC緩存受限於每次從服務器fetch多少數據,緩存僅在當前fetch的生命周期內有效(緩存僅適用於獲取的整個生命周期。后續查詢(或提取)無法訪問先前運行的緩存值),而標量子查詢是當前查詢內有效。因此受到數組大小的影響. 數組大小(array size)的差異產生了截然不同的性能。如下測試所示:
The difference in array size produced drastically different performance, showing that caching is only available for the lifetime of the fetch. Subsequent queries (or fetches) have no access to the cached values of previous runs.
SQL> show arraysize;
arraysize 15
SQL> SELECT SLOW_FUNCTION_DERM(ID) FROM TEST ;
SLOW_FUNCTION_DERM(ID)
----------------------
11
11
11
12
12
12
13
13
8 rows selected.
Elapsed: 00:00:08.01
SQL> set arraysize 1;
SQL> SELECT SLOW_FUNCTION_DERM(ID) FROM TEST ;
SLOW_FUNCTION_DERM(ID)
----------------------
11
11
11
12
12
12
13
13
8 rows selected.
Elapsed: 00:00:12.01
關於arraysize摘抄“Oracle arraysize 和 fetch size參數與性能優化說明”博客中這一段資料:
arraysize定義了一次返回到客戶端的行數,當掃描了arraysize行后,停止掃描,返回數據,然后繼續掃描。
這個過程就是統計信息中的SQL*Net round trips to/from client。因為arraysize 默認是15行,那么就有一個問題,因為我們一個block
中的記錄數一般都會超過15行,所以如果按照15行掃描一次,那么每次掃描要多掃描一個數據塊,一個數據塊也可能就會重復掃描多次。
重復的掃描會增加consistent gets 和 physical reads。 增加physical reads,這個很好理解,掃描的越多,物理的可能性就越大。
consistent gets,這個是從undo里讀的數量,ORACLE為了保證數據的一致性,當一個查詢很長,在查詢之后,數據塊被修改,還未提交,
再次查詢時候,ORACLE會根據Undo來構建CR塊,這個CR塊,可以理解成數據塊在之前某個時間的狀態。這樣通過查詢出來的數據就是一致的。
那么如果重復掃描的塊越多,需要構建的CR塊就會越多,這樣讀Undo的機會就會越多,consistent gets 就會越多。
Cross-Session PL/SQL Function Result Cache
ORACLE 11g 中引入了兩個新的緩存機制:
跨會話PL/SQL函數結果緩存:用於緩存函數調用的結果。
Cross-Session PL/SQL Function Result Cache : Used for caching the results of function calls.
查詢結果緩存:用於緩存查詢產生的整個結果集。
Query Result Cache : Used for caching the whole result set produced by a query.
ORACLE 11G提供的PL/SQL函數的緩存機制(對於不同的會話之間可以共用),下面我們使用第一種機制進行查詢結果的緩存,如下所,指定關鍵詞RESULT_CACHE來啟用Cross-Session PL/SQL Function Result Cache
CREATE OR REPLACE FUNCTION SLOW_FUNCTION(p_value IN NUMBER)
RETURN NUMBER
RESULT_CACHE
AS
BEGIN
DBMS_LOCK.SLEEP(2);
RETURN p_value+10;
END;
/
然后開啟一個會話ID為10的會話,測試SQL性能
SQL> SET TIMING ON;
SQL> SELECT * FROM V$MYSTAT WHERE ROWNUM =1;
SID STATISTIC# VALUE
---------- ---------- ----------
10 0 0
Elapsed: 00:00:00.01
SQL> SELECT slow_function(id)
2 FROM TEST;
SLOW_FUNCTION(ID)
-----------------
11
12
13
14
15
16
17
18
8 rows selected.
Elapsed: 00:00:16.02
另外開啟一個會話,對比測試SQL性能
SQL> SET TIMING ON;
SQL> SELECT * FROM V$MYSTAT WHERE ROWNUM =1;
SID STATISTIC# VALUE
---------- ---------- ----------
73 0 0
Elapsed: 00:00:00.01
SQL> SELECT slow_function(id)
2 FROM TEST;
SLOW_FUNCTION(ID)
-----------------
11
12
13
14
15
16
17
18
8 rows selected.
Elapsed: 00:00:00.01
從上述實驗可以看出,使用上述方式的緩存信息可以被其他的會話使用,它依賴的對象是自動被管理的。關於這個技術的細節特性是另外一個比較大的話題,此處不做展開,我們需要知道的就是,數據庫會幫我們自動緩存,從而當其他會話調用時,使用相關緩存結果來減少函數調用次數。從而達到提高性能效果。
使用PL/SQL的集合進行手動的管理緩存信息
在ORACLE 11g之前的版本,我們可以手動將函數調用的值緩存在PL/ SQL集合中,如下所示,我們在對函數調用前,我們構建一個緩存層(caching layer)
CREATE OR REPLACE FUNCTION SLOW_FUNCTION(p_value IN NUMBER)
RETURN NUMBER
AS
BEGIN
DBMS_LOCK.SLEEP(2);
RETURN p_value+10;
END;
/
CREATE OR REPLACE PACKAGE cached_lookup_api AS
FUNCTION get_cached_value (p_id IN NUMBER)
RETURN NUMBER;
PROCEDURE clear_cache;
END cached_lookup_api;
/
CREATE OR REPLACE PACKAGE BODY cached_lookup_api AS
TYPE t_tab IS TABLE OF NUMBER
INDEX BY BINARY_INTEGER;
g_tab t_tab;
g_last_use DATE := SYSDATE;
g_max_cache_age NUMBER := 10/(24*60); -- 10 minutes
-- -----------------------------------------------------------------
FUNCTION get_cached_value (p_id IN NUMBER)
RETURN NUMBER AS
l_value NUMBER;
BEGIN
IF (SYSDATE - g_last_use) > g_max_cache_age THEN
-- Older than 10 minutes. Delete cache.
g_last_use := SYSDATE;
clear_cache;
END IF;
BEGIN
l_value := g_tab(p_id);
EXCEPTION
WHEN NO_DATA_FOUND THEN
-- Call function and cache data.
l_value := slow_function(p_id);
g_tab(p_id) := l_value;
END;
RETURN l_value;
END get_cached_value;
-- -----------------------------------------------------------------
-- -----------------------------------------------------------------
PROCEDURE clear_cache AS
BEGIN
g_tab.delete;
END;
-- -----------------------------------------------------------------
END cached_lookup_api;
/
測試如下所示,不過此方法只能對於當前會話,緩存不在數據庫會話之間共享
這個方法有幾個問題:
緩存中沒有依賴關系管理。只是刪除了超過十分鍾的緩存數據,你可以提高間隔粒度,但是它可能需要額外的工作。
如果會話是連接池的一部分,那么通過多次調用可能會泄漏信息( potential for information bleeding)。這可以通過package reset來解決。
這個方法沒有自動管理緩存大小的機制。
緩存不在數據庫會話之間共享
手動的使用上下文控制緩存(Manual Caching Using Contexts)
創建上下文(contexts)作為ACCESSED GLOBALLY允許在會話之間共享高速緩存,如下腳本所示:
CREATE OR REPLACE CONTEXT cache_context USING cached_lookup_api ACCESSED GLOBALLY;
CREATE OR REPLACE PACKAGE cached_lookup_api AS
FUNCTION get_cached_value (p_id IN NUMBER)
RETURN NUMBER;
PROCEDURE clear_cache;
END cached_lookup_api;
/
CREATE OR REPLACE PACKAGE BODY cached_lookup_api AS
g_last_use DATE := SYSDATE;
g_max_cache_age NUMBER := 10/(24*60); -- 10 minutes
g_context_name VARCHAR2(20) := 'cache_context';
-- -----------------------------------------------------------------
FUNCTION get_cached_value (p_id IN NUMBER)
RETURN NUMBER AS
l_value NUMBER;
BEGIN
IF (SYSDATE - g_last_use) > g_max_cache_age THEN
-- Older than 10 minutes. Delete cache.
g_last_use := SYSDATE;
clear_cache;
END IF;
l_value := SYS_CONTEXT(g_context_name, p_id);
IF l_value IS NULL THEN
l_value := slow_function(p_id);
DBMS_SESSION.set_context(g_context_name, p_id, l_value);
END IF;
RETURN l_value;
END get_cached_value;
-- -----------------------------------------------------------------
-- -----------------------------------------------------------------
PROCEDURE clear_cache AS
BEGIN
DBMS_SESSION.clear_all_context(g_context_name);
END;
-- -----------------------------------------------------------------
END cached_lookup_api;
/
我們在不同會話測試測試可以發現,這個可以實現ORACLE 11g中的Cross-Session PL/SQL Function Result Cache
SQL> SET TIMING ON;
SQL> SELECT * FROM v$mystat WHERE ROWNUM=1;
SID STATISTIC# VALUE
---------- ---------- ----------
10 0 0
Elapsed: 00:00:00.01
SQL> SELECT cached_lookup_api.get_cached_value(id)
2 FROM test;
CACHED_LOOKUP_API.GET_CACHED_VALUE(ID)
--------------------------------------
11
12
13
14
15
16
17
18
8 rows selected.
Elapsed: 00:00:16.03
SQL>
SQL> SET TIMING ON;
SQL> SELECT * FROM v$mystat WHERE ROWNUM=1;
SID STATISTIC# VALUE
---------- ---------- ----------
73 0 0
Elapsed: 00:00:00.01
SQL> SELECT cached_lookup_api.get_cached_value(id)
2 FROM test;
CACHED_LOOKUP_API.GET_CACHED_VALUE(ID)
--------------------------------------
11
12
13
14
15
16
17
18
8 rows selected.
Elapsed: 00:00:00.01
SQL>
Scalar Subquery Caching (Revisited)
除了標量子查詢緩存之外,我們還討論了許多緩存機制,但是這些緩存方法是否可以替代對標量子查詢緩存的需求?答案是否定的,因為標量子查詢緩存是有效減少SQL和PL/SQL之間上下文切換次數的唯一機制。 為了說明這一點,我們將建立一個具有相同值的100,000行的新測試表。
DROP TABLE t2;
CREATE TABLE t2 (
id NUMBER
);
INSERT /*+ APPEND */ INTO t2
SELECT 1
FROM dual
CONNECT BY level <= 100000;
COMMIT;
CREATE OR REPLACE FUNCTION SLOW_FUNCTION(p_value IN NUMBER)
RETURN NUMBER
RESULT_CACHE
AS
BEGIN
DBMS_LOCK.SLEEP(2);
RETURN p_value+10;
END;
/
SET SERVEROUTPUT ON
DECLARE
l_start NUMBER;
BEGIN
l_start := DBMS_UTILITY.get_cpu_time;
FOR cur_rec IN (SELECT slow_function(id)
FROM t2)
LOOP
NULL;
END LOOP;
DBMS_OUTPUT.put_line('Regular Query (SELECT List): ' ||
(DBMS_UTILITY.get_cpu_time - l_start) || ' hsecs CPU Time');
l_start := DBMS_UTILITY.get_cpu_time;
FOR cur_rec IN (SELECT (SELECT slow_function(id) FROM dual)
FROM t2)
LOOP
NULL;
END LOOP;
DBMS_OUTPUT.put_line('Scalar Subquery (SELECT List): ' ||
(DBMS_UTILITY.get_cpu_time - l_start) || ' hsecs CPU Time');
END;
/
在WHERE子句中使用標量子查詢時,這種CPU使用率的差異也是可見的。
造成CPU使用率差異的原因是什么?除了標量子查詢緩存之外,此處討論的所有其他緩存方法仍需要調用PL/SQL函數,這會導致SQL和PL/SQL之間的上下文切換。這些上下文切換需要額外的CPU負載。
因此,即使在使用可選的緩存功能來提高多次執行之間或會話之間的函數調用的性能,仍應該使用標量子查詢緩存來減少上下文切換。
WHERE子句中的函數
前面討論的緩存方法也適用於WHERE子句,特別是用於減少上下文切換的標量子查詢緩存。
在查詢的WHERE子句中的列上應用函數可能會導致性能較差,因為它會阻止優化程序在該列上使用常規索引。假設查詢不能被重寫以消除對函數調用的需要,則一種選擇是使用基於函數的索引(function based index )。
您還應該考慮在Oracle 11g中引入的虛擬列。
函數調用中的讀一致性問題
這個可以直接參考Efficient Function Calls From SQL,此處不做翻譯或解說。
參考資料:
https://oracle-base.com/articles/misc/efficient-function-calls-from-sql