相关变量的问题:如何将优化器从敌人变成朋友

本文的作者是OCP的Victor VarlamovvarlamovVp18 )。
原始文章发布于2017年7月7日。
特别感谢翻译的作者-brutaltag

我们的报告系统通常会执行数百个由各种事件触发的冗长的请求。 查询参数是客户端列表和时间间隔(每天,每周,每月)。 由于表中数据的不均匀性,一个查询可以产生一行和一百万行,具体取决于报表参数(不同的客户端在事实表中具有不同的行数)。 每个报告都以具有主要功能的程序包的形式生成,该功能接受输入参数,执行其他转换,然后打开带有关联变量的静态游标,最后返回此打开的游标。 数据库参数CURSOR_SHARING设置为FORCE。
在这种情况下,无论是在优化程序重新使用查询计划的情况下,还是在使用字面量形式的参数完全解析查询的情况下,都必须应对性能低下的问题。 绑定变量可能会导致查询计划不理想。

亚历克斯·戈尔巴乔夫(Alex Gorbachev)在他的《 Oracle专家实践》一书中,讲述了汤姆·基特(Tom Kite)讲的一个有趣的故事。 每个下雨的星期一,用户都必须应对修改后的查询计划。 很难相信,但这是:
“根据最终用户的观察,在星期一下大雨时,数据库性能非常糟糕。 在一周的任何其他日子或星期一,没有雨就没有问题。 通过与DBA的交谈,汤姆·基特(Tom Kite)知道困难一直存在,直到数据库被迫重新启动,然后性能恢复正常。 那是一个解决方法:下雨的星期一-重新启动。”

这是一个真实的例子,并且由于对Oracle工作原理的深入了解,该问题得以完全解决而没有任何魔术。 我将在文章末尾显示解决方案。
这是一个有关相关变量如何工作的小例子。
创建具有不均匀数据的表。

SQL> CREATE TABLE VVP_HARD_PARSE_TEST(C1 NUMBER, C2 NUMBER, C3 VARCHAR2(300)); TABLE created. SQL> INSERT INTO VVP_HARD_PARSE_TEST SELECT ROWNUM C1, CASE WHEN LEVEL < 9 THEN 1 WHEN MOD(ROWNUM, 100)=99 THEN 99 ELSE 1000000 END C2, RPAD('A', 300, 'A') C3 FROM DUAL CONNECT BY LEVEL CREATE INDEX IND_VVP_HARD_PARSE_TEST_C2 ON VVP_HARD_PARSE_TEST(C2); INDEX created. SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(OWNNAME => USER, TABNAME => 'VVP_HARD_PARSE_TEST', CASCADE => TRUE, METHOD_OPT=>'FOR ALL INDEXED COLUMNS SIZE 254'); PL/SQL PROCEDURE successfully completed. SQL> SELECT histogram FROM user_tab_columns WHERE table_name = 'VVP_HARD_PARSE_TEST' AND column_name = 'C2'; HISTOGRAM --------- FREQUENCY SQL> SELECT c2, COUNT(*) FROM VVP_HARD_PARSE_TEST GROUP BY c2 ORDER BY 1; C2 COUNT(*) ----------------------- 1 8 99 10000 1000000 989992 


换句话说,我们有一个具有一百万行的表VVP_HARD_PARSE_TEST,其中在10,000种情况下,该字段为C2 = 99,8个记录的C2 = 1,其余为C2 = 1,000,000.C2字段的直方图指示Oracle优化器关于此数据分布的信息。 这种情况称为数据分布不均 ,直方图可以帮助您根据请求的数据选择正确的查询计划。

我们观察到对该表的简单查询。 显然是为了要求

SELECT * FROM VVP_HARD_PARSE_TEST WHERE c2 = :p

如果p = 1,则INDEX RANGE SCAN是最佳选择,对于p = 1000000的情况,最好使用FULL TABLE SCAN。 Query1和Query1000000查询是相同的,只是注释中的文本不同,这样做是为了获得不同的查询计划标识符。

 DECLARE p NUMBER; v NUMBER; BEGIN V := 0; p := 1000000; FOR rec IN (SELECT /*+query1000000*/ * FROM VVP_HARD_PARSE_TEST WHERE c2 = p) LOOP V := v + 1; END LOOP; dbms_output.put_line(v); v : =0; p := 1; FOR rec IN (SELECT /*+query1000000*/ * FROM VVP_HARD_PARSE_TEST WHERE c2 = p) LOOP V := v + 1; END LOOP; dbms_output.put_line(v); ----------------- V := 0; p := 1; FOR rec IN (SELECT /*+query1*/ * FROM VVP_HARD_PARSE_TEST WHERE c2 = p) LOOP V := v + 1; END LOOP; dbms_output.put_line(v); v := 0; p := 1000000; FOR rec IN (SELECT /*+query1*/ * FROM VVP_HARD_PARSE_TEST WHERE c2 = p) LOOP V := v + 1; END LOOP; dbms_output.put_line(v); END; 

现在让我们看一下查询计划:

 SQL> SELECT sql_id, child_number, executions, plan_hash_value, sql_text FROM v$sql WHERE sql_text LIKE 'SELECT % * FROM VVP_HARD_PARSE_TEST WHERE C2%'; SQL_ID CHILD_NUMBER EXECUTIONS PLAN_HASH_VALUE SQL_TEXT ------------------------------------------------- 7rqnhhp6pahw2 0 2 2782757451 SELECT /*+query1000000*/ * FROM VVP_HARD_PARSE_TEST WHERE C2 = :B1 7xwt28hvw3u9s 0 2 2463783749 SELECT /*+query1*/ * FROM VVP_HARD_PARSE_TEST WHERE C2 = :B1 SQL> SELECT * FROM TABLE(dbms_xplan.display_cursor(sql_id => '7rqnhhp6pahw2', format => 'basic +peeked_binds')); SELECT /*+query1000000*/ * FROM VVP_HARD_PARSE_TEST WHERE C2 = :B1 PLAN hash VALUE: 2782757451 ------------------------------------------------- | Id | Operation | Name | ------------------------------------------------- | 0 | SELECT STATEMENT | | | 1 | TABLE ACCESS FULL| VVP_HARD_PARSE_TEST | ------------------------------------------------- Peeked Binds (IDENTIFIED BY position): -------------------------------------- 1 - :B1 (NUMBER): 1000000 SQl> SELECT * FROM TABLE(dbms_xplan.display_cursor(sql_id => '7xwt28hvw3u9s', format => 'basic +peeked_binds')); SELECT /*+query1*/ * FROM VVP_HARD_PARSE_TEST WHERE C2 = :B1 PLAN hash VALUE: 2463783749 ------------------------------------------------------------------ | Id | Operation | Name | ------------------------------------------------------------------ | 0 | SELECT STATEMENT | | | 1 | TABLE ACCESS BY INDEX ROWID| VVP_HARD_PARSE_TEST | | 2 | INDEX RANGE SCAN | IND_VVP_HARD_PARSE_TEST_C2 | ------------------------------------------------------------------ Peeked Binds (IDENTIFIED BY position): -------------------------------------- 1 - :B1 (NUMBER): 1 

如您所见,在第一次执行时,针对不同请求的计划仅创建一次(每个请求仅存在一个CHILD_NUMBER = 0的子游标)。 每个请求执行两次(EXECUTION = 2)。 在硬解析期间,Oracle检索关联变量的值,并根据这些值选择计划。 但是,即使相关变量在第二次运行中发生了变化,他仍对下一次运行使用相同的计划。 使用非最佳计划-变量C2 = 1的Query1000000使用FULL TABLE SCAN而不是INDEX RANGE SCAN,反之亦然。

显然,修复应用程序并将参数用作查询中的文字是解决问题的最合适方法,但是它导致动态SQL及其已知的缺点。 另一种方法是禁用对相关变量的查询( ALTER SESSION SET "_OPTIM_PEEK_USER_BINDS" = FALSE )或删除直方图( 链接 )。

一种可能的解决方案是数据访问策略的替代使用,也称为虚拟专用数据库 (粒度访问控制,细粒度访问控制,行级别控制)。 这使您可以即时更改请求,因此每次请求使用详细的访问控制时,都可以对请求计划进行完整的分析。 Randalph Geist在一篇文章中详细描述了此技术。 此方法的缺点是完整解析的数量不断增加,并且无法操纵查询计划。

看看我们现在要做什么。 在分析了我们的数据之后,我们决定根据一年中的交易数或交易数将客户分为大,中和小三类(LMS或9-5-1)。 此外,报告中的行数严格取决于以下时间段:月-大,周-中,日-小或9-5-1。 此外,解决方案很简单-我们将使安全策略谓词取决于每个类别和每个期间。 因此,对于每个请求,我们都有9个可能的子游标。 此外,具有不同策略的查询将导致我们获得相同的查询标识符,这使得实现SQL PLAN MANAGEMENT(SQL计划基线)成为可能。

 SQL> CREATE TABLE HARD_PARSE_TABLE AS SELECT * FROM dual; TABLE created. SQL> CREATE TABLE CLIENTS_HP_STATISTICS (client_seqno NUMBER, client_id VARCHAR2(255), cnt_year NUMBER); TABLE created. SQL> INSERT INTO CLIENTS_HP_STATISTICS (client_seqno, client_id, cnt_year) VALUES (1, 'SMALL CLIENT', 8); 1 ROW inserted. SQL> INSERT INTO CLIENTS_HP_STATISTICS (client_seqno, client_id, cnt_year) VALUES (99, 'MIDDLE CLIENT', 50001); 1 ROW inserted. SQL> INSERT INTO CLIENTS_HP_STATISTICS (client_seqno, client_id, cnt_year) VALUES (1000000,'LARGE CLIENT', 989992); 1 ROW inserted. SQL> CREATE OR REPLACE PACKAGE FORCE_HARD_PARSE_PKG IS gc_small CONSTANT NUMBER := 1; gc_middle CONSTANT NUMBER := 5; gc_large CONSTANT NUMBER := 9; gc_client_middle CONSTANT NUMBER := 50000; gc_client_large CONSTANT NUMBER := 500000; gc_daterange_middle CONSTANT NUMBER := 10; gc_daterange_large CONSTANT NUMBER := 50; FUNCTION FORCE_HARD_PARSE(in_schema VARCHAR2, in_object VARCHAR2) RETURN VARCHAR2; PROCEDURE SET_PREDICATE (n NUMBER); PROCEDURE SET_PREDICATES (p_daterange NUMBER DEFAULT NULL, p_clientrange NUMBER DEFAULT NULL); PROCEDURE CALC_PREDICATE; PROCEDURE CALC_PREDICATES(p_date_interval NUMBER DEFAULT 1, p_client_seqno NUMBER DEFAULT NULL, p_client_id VARCHAR2 DEFAULT NULL, p_client_seqno_list VARCHAR2 DEFAULT NULL ); END FORCE_HARD_PARSE_PKG; PACKAGE created. SQL> CREATE OR REPLACE PACKAGE BODY FORCE_HARD_PARSE_PKG IS g_predicate NUMBER; -- g_daterange || 0 || g_clientrange g_daterange NUMBER; -- 1 - small, 5 - middle, 9 - large g_clientrange NUMBER; -- 1 - small, 5 - middle, 9 - large -- FUNCTION FORCE_HARD_PARSE(in_schema VARCHAR2, in_object VARCHAR2) RETURN VARCHAR2 IS BEGIN IF NVL(g_predicate, 0) = 0 THEN RETURN NULL; ELSE RETURN TO_CHAR(g_predicate, 'TM') || ' = ' || TO_CHAR(g_predicate, 'TM'); END IF; END FORCE_HARD_PARSE; -- PROCEDURE SET_PREDICATE (n NUMBER) IS BEGIN g_predicate := n; END; PROCEDURE SET_PREDICATES (p_daterange NUMBER DEFAULT NULL, p_clientrange NUMBER DEFAULT NULL) IS BEGIN IF p_daterange IS NOT NULL THEN g_daterange := p_daterange; CALC_PREDICATE; END IF; IF p_clientrange IS NOT NULL THEN g_clientrange := p_clientrange; CALC_PREDICATE; END IF; END SET_PREDICATES; PROCEDURE CALC_PREDICATE IS BEGIN g_predicate := NVL(g_daterange, 0) * 100 + NVL(g_clientrange, 0); END CALC_PREDICATE; PROCEDURE CALC_PREDICATES (p_date_interval NUMBER DEFAULT 1, p_client_seqno NUMBER DEFAULT NULL, p_client_id VARCHAR2 DEFAULT NULL, p_client_seqno_list VARCHAR2 DEFAULT NULL) IS v_cnt NUMBER; BEGIN IF p_date_interval IS NOT NULL THEN g_daterange := CASE WHEN p_date_interval < gc_daterange_middle THEN gc_small WHEN p_date_interval < gc_daterange_large THEN gc_middle ELSE gc_large END; CALC_PREDICATE; END IF; IF COALESCE(p_client_seqno, p_client_id, p_client_seqno_list) IS NOT NULL THEN SELECT NVL(SUM(cnt_year), 0) AS cnt INTO v_cnt FROM CLIENTS_HP_STATISTICS t WHERE 1=1 AND (p_client_seqno IS NULL OR p_client_seqno = t.client_seqno) AND (p_client_id IS NULL OR p_client_id = t.client_id) AND (p_client_seqno_list IS NULL OR t.client_seqno IN (SELECT SUBSTR(s, CASE WHEN LEVEL > 1 THEN INSTR(s, ',', 1, LEVEL - 1 ) + 1 ELSE 1 END, INSTR(s, ',', 1, LEVEL) – CASE WHEN LEVEL > 1 THEN INSTR(s, ',', 1, LEVEL – 1) + 1 ELSE 1 END) FROM (SELECT p_client_seqno_list||',' AS s FROM DUAL) CONNECT BY INSTR(s, ',', 1, LEVEL) > 0)); g_clientrange := CASE WHEN v_cnt > gc_client_large THEN gc_large WHEN v_cnt > gc_client_middle THEN gc_middle ELSE gc_small END; CALC_PREDICATE; END IF; END CALC_PREDICATES; END FORCE_HARD_PARSE_PKG; PACKAGE BODY created. SQL> EXEC DBMS_RLS.ADD_POLICY (USER, 'HARD_PARSE_TABLE', 'HARD_PARSE_POLICY', USER, 'FORCE_HARD_PARSE_PKG.FORCE_HARD_PARSE', 'select'); PL/SQL PROCEDURE successfully completed. 

现在,如果要在报表中嵌入此类技术,则需要在查询中添加HARD_PARSE_TABLE(这不会破坏它),并在执行主查询之前调用CALC_PREDICATES。

让我们看看这种技术如何改变前面的例子:

 DECLARE p NUMBER; v NUMBER; BEGIN V := 0; p := 1000000; FORCE_HARD_PARSE_PKG.SET_PREDICATE(1000000); FOR rec IN (SELECT /*+query_hp1000000*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE c2 = p) LOOP V := v + 1; END LOOP; dbms_output.put_line(v); v := 0; p := 1; FORCE_HARD_PARSE_PKG.SET_PREDICATE(1); FOR rec IN (SELECT /*+query_hp1000000*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE c2 = p) LOOP V := v + 1; END LOOP; dbms_output.put_line(v); ----------------- V := 0; p := 1; FORCE_HARD_PARSE_PKG.SET_PREDICATE(1); FOR rec IN (SELECT /*+query_hp1*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE c2 = p) LOOP V := v + 1; END LOOP; dbms_output.put_line(v); v := 0; p := 1000000; FORCE_HARD_PARSE_PKG.SET_PREDICATE(1000000); FOR rec IN (SELECT /*+query_hp1*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE c2 = p) LOOP V := v + 1; END LOOP; dbms_output.put_line(v); END; 

让我们看一下执行计划:

 SQL> SELECT sql_id, child_number, executions, plan_hash_value, sql_text, s.* FROM v$sql s WHERE sql_text LIKE 'SELECT % * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE c2%' ORDER BY 1,2; SQL_ID CHILD_NUMBER EXECUTIONS PLAN_HASH_VALUE SQL_TEXT -------------------------------------------------------------------------------- 7wva3uqbgh4qf 0 1 1136240498 SELECT /*+query_hp1000000*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE C2 = :B1 7wva3uqbgh4qf 1 1 3246475190 SELECT /*+query_hp1000000*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE C2 = :B1 8cju3tfjvwm1p 0 1 3246475190 SELECT /*+query_hp1*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE C2 = :B1 8cju3tfjvwm1p 1 1 1136240498 SELECT /*+query_hp1*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE C2 = :B1 -- SQL> SELECT * FROM TABLE(dbms_xplan.display_cursor(sql_id => '7wva3uqbgh4qf', cursor_child_no => 0, format => 'basic +peeked_binds')); SELECT /*+query_hp1000000*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE C2 = :B1 PLAN hash VALUE: 1136240498 ---------------------------------------------------- | Id | Operation | Name | ---------------------------------------------------- | 0 | SELECT STATEMENT | | | 1 | MERGE JOIN CARTESIAN| | | 2 | TABLE ACCESS FULL | HARD_PARSE_TABLE | | 3 | BUFFER SORT | | | 4 | TABLE ACCESS FULL | VVP_HARD_PARSE_TEST | ---------------------------------------------------- Peeked Binds (IDENTIFIED BY position): -------------------------------------- 1 - :B1 (NUMBER): 1000000 -- SQL> SELECT * FROM TABLE(dbms_xplan.display_cursor(sql_id => '7wva3uqbgh4qf', cursor_child_no => 1, format => 'basic +peeked_binds')); SELECT /*+query_hp1000000*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE C2 = :B1 PLAN hash VALUE: 3246475190 -------------------------------------------------------------------- | Id | Operation | Name | -------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | 1 | MERGE JOIN CARTESIAN | | | 2 | TABLE ACCESS FULL | HARD_PARSE_TABLE | | 3 | BUFFER SORT | | | 4 | TABLE ACCESS BY INDEX ROWID| VVP_HARD_PARSE_TEST | | 5 | INDEX RANGE SCAN | IND_VVP_HARD_PARSE_TEST_C2 | -------------------------------------------------------------------- Peeked Binds (IDENTIFIED BY position): -------------------------------------- 1 - :B1 (NUMBER): 1 -- SQl> SELECT * FROM TABLE(dbms_xplan.display_cursor(sql_id => '8cju3tfjvwm1p', cursor_child_no => 0, format => 'basic +peeked_binds')); SELECT /*+query_hp1*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE C2 = :B1 PLAN hash VALUE: 3246475190 -------------------------------------------------------------------- | Id | Operation | Name | -------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | 1 | MERGE JOIN CARTESIAN | | | 2 | TABLE ACCESS FULL | HARD_PARSE_TABLE | | 3 | BUFFER SORT | | | 4 | TABLE ACCESS BY INDEX ROWID| VVP_HARD_PARSE_TEST | | 5 | INDEX RANGE SCAN | IND_VVP_HARD_PARSE_TEST_C2 | -------------------------------------------------------------------- Peeked Binds (IDENTIFIED BY position): -------------------------------------- 1 - :B1 (NUMBER): 1 -- SQL> SELECT * FROM TABLE(dbms_xplan.display_cursor(sql_id => '8cju3tfjvwm1p', cursor_child_no => 1, format => 'basic +peeked_binds')); SELECT /*+query_hp1*/ * FROM VVP_HARD_PARSE_TEST, HARD_PARSE_TABLE WHERE C2 = :B1 PLAN hash VALUE: 1136240498 ---------------------------------------------------- | Id | Operation | Name | ---------------------------------------------------- | 0 | SELECT STATEMENT | | | 1 | MERGE JOIN CARTESIAN| | | 2 | TABLE ACCESS FULL | HARD_PARSE_TABLE | | 3 | BUFFER SORT | | | 4 | TABLE ACCESS FULL | VVP_HARD_PARSE_TEST | ---------------------------------------------------- Peeked Binds (IDENTIFIED BY position): -------------------------------------- 1 - :B1 (NUMBER): 1000000 

看起来很棒! 每个查询执行两次,使用不同的子游标和不同的计划。 对于参数C2 = 1,000,000,我们在两个查询中都看到FULL TABLE SCAN,对于参数C1 = 1,我们总是看到INDEX RANGE SCAN。

最后,我对下雨星期一的情况给出了解决方案:

事实证明,每个星期日的周末都有一个冷备份 ,因此所有查询计划都在星期一早上的第一次执行时重新生成。 其中一名员工通常比其他员工早开始工作,并且他的请求计划在一周内对其他用户的执行情况很好。 但是,如果正在下雨,由于早上路线问题,该用户在工作日开始之前要迟到。 然后,首先开始进行报表的批处理计算,但是由于相关变量的值不合适,因此对于其余情况,查询计划完全不利。”

还有一些有用的系统视图:
dba_tab_histograms, all_tab_histograms, user_tab_histograms
v$vpd_policy
v$sql_bind_capture
dba_hist_sqlbind

Source: https://habr.com/ru/post/zh-CN434444/


All Articles