El problema con las variables relacionadas: cómo convertir el optimizador de enemigo a amigo

El autor del artículo es Victor Varlamov ( varlamovVp18 ), OCP.
El artículo original fue publicado el 07/07/2017.
Un agradecimiento especial al autor de la traducción: brutaltag .

Nuestro sistema de informes generalmente ejecuta cientos de solicitudes largas que son activadas por varios eventos. Los parámetros de consulta son una lista de clientes y un intervalo de tiempo (diario, semanal, mensual). Debido a los datos desiguales en las tablas, una consulta puede producir una fila o un millón de filas, dependiendo de los parámetros del informe (diferentes clientes tienen diferentes números de filas en las tablas de hechos). Cada informe se realiza en forma de paquete con una función principal que toma parámetros de entrada, realiza transformaciones adicionales, luego abre un cursor estático con variables asociadas y finalmente devuelve este cursor abierto. El parámetro DB CURSOR_SHARING está establecido en FORCE.
En tal situación, uno tiene que lidiar con un bajo rendimiento, tanto en el caso de la reutilización del plan de consulta por el optimizador, como cuando la consulta se analiza por completo con parámetros en forma de literales . Las variables enlazadas pueden causar un plan de consulta subóptimo.

En su libro "Oracle Expert Practices", Alex Gorbachev cuenta una interesante historia contada por Tom Kite. Cada lunes lluvioso, los usuarios tenían que lidiar con un plan de consulta modificado. Es difícil de creer, pero fue:
“Según las observaciones del usuario final, cuando llovió mucho el lunes, el rendimiento de la base de datos fue terrible. En cualquier otro día de la semana o el lunes no hubo problemas sin lluvia. De una conversación con el DBA, Tom Kite se enteró de que las dificultades continuaron hasta que la base de datos se vio obligada a reiniciarse, después de lo cual el rendimiento se normalizó. Esa fue una solución: lunes lluvioso, reiniciar ”.

Este es un caso real, y el problema se resolvió completamente sin ningún tipo de magia, solo gracias al excelente conocimiento de cómo funciona Oracle. Mostraré la solución al final del artículo.
Aquí hay un pequeño ejemplo de cómo funcionan las variables relacionadas.
Crea una tabla con datos desiguales.

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 


En otras palabras, tenemos una tabla VVP_HARD_PARSE_TEST con un millón de filas, donde en 10,000 casos el campo es C2 = 99, 8 registros con C2 = 1 y el resto con C2 = 1,000,000. El histograma en el campo C2 indica al optimizador de Oracle sobre esta distribución de datos. Esta situación se conoce como distribución desigual de datos , y un histograma puede ayudarlo a elegir el plan de consulta correcto en función de los datos solicitados.

Observamos consultas simples a esta tabla. Obviamente por la solicitud

SELECT * FROM VVP_HARD_PARSE_TEST WHERE c2 = :p

si p = 1, entonces INDEX RANGE SCAN es la mejor opción, para el caso p = 1000000 es mejor usar FULL TABLE SCAN. Las consultas Query1 y Query1000000 son idénticas, con la excepción del texto en los comentarios, esto se hace para obtener diferentes identificadores del plan de consulta.

 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; 

Ahora veamos los planes de consulta:

 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 

Como puede ver, se crea un plan para diferentes solicitudes solo una vez, en el momento de la primera ejecución (solo existe un cursor secundario con CHILD_NUMBER = 0 para cada solicitud). Cada solicitud se ejecuta dos veces (EJECUCIÓN = 2). Durante el análisis duro, Oracle recupera los valores de las variables asociadas y selecciona un plan de acuerdo con estos valores. Pero usa el mismo plan para la próxima ejecución, a pesar de que las variables relacionadas cambiaron en la segunda ejecución. Se utilizan planes no óptimos: Query1000000 con la variable C2 = 1 usa ESCANEO DE TABLA COMPLETA en lugar de ESCANEO DE RANGO DE ÍNDICE y viceversa.

Está claro que arreglar la aplicación y usar parámetros como literales en la consulta es la forma más adecuada de resolver el problema, pero conduce a un SQL dinámico con sus defectos conocidos. Otra forma es deshabilitar la consulta para variables relacionadas ( ALTER SESSION SET "_OPTIM_PEEK_USER_BINDS" = FALSE ) o eliminar histogramas ( enlace ).

Una de las posibles soluciones es un uso alternativo de políticas de acceso a datos, también conocido como Base de datos privada virtual (control de acceso detallado, control de acceso de grano fino , control de nivel de fila). Esto le permite cambiar las solicitudes sobre la marcha y, por lo tanto, puede hacer un análisis completo del plan de solicitud cada vez que la solicitud utiliza un control de acceso detallado. Esta técnica se describe en detalle en un artículo de Randalph Geist . La desventaja de este método es el creciente número de análisis completos y la incapacidad de manipular los planes de consulta.

Mira lo que haremos ahora. Después de analizar nuestros datos, decidimos dividir a los clientes en tres categorías: grande, mediana y pequeña (LMS o 9-5-1), de acuerdo con el número de transacciones o transacciones durante el año. Además, el número de líneas en el informe depende estrictamente del período: mensual - grande, semanal - medio, diario - pequeño o 9-5-1. Además, la solución es simple: haremos que el predicado de la política de seguridad dependa de cada categoría y de cada período. Entonces, por cada solicitud, obtenemos 9 posibles cursores secundarios. Además, las consultas con diferentes políticas nos llevarán a los mismos identificadores de consulta, esto permite implementar SQL PLAN MANAGEMENT (línea de base del plan 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. 

Ahora, si queremos incorporar dicha tecnología en el informe, debemos agregar HARD_PARSE_TABLE a la consulta (esto no lo arruinará un poco) y llamar a CALC_PREDICATES antes de que se ejecute la consulta principal.

Veamos cómo esta técnica puede transformar el ejemplo anterior:

 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; 

Veamos los planes de ejecución:

 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 

Se ve genial! Cada consulta se ejecuta dos veces, con diferentes cursores secundarios y diferentes planes. Para el parámetro C2 = 1,000,000, vemos ESCANEO DE TABLA COMPLETA en ambas consultas, y para el parámetro C1 = 1 siempre vemos ESCANEO DE RANGO DE ÍNDICE.

Al final doy una solución al caso de los lunes lluviosos:

“Resulta que todos los fines de semana del domingo había una copia de seguridad en frío , por lo que todos los planes de consulta se regeneraron en la primera ejecución el lunes por la mañana. Uno de los empleados generalmente comenzó su trabajo antes que los demás, y su plan de solicitud se ejecutó bien para otros usuarios durante la semana. Sin embargo, si estaba lloviendo, este usuario llegaba tarde al comienzo de la jornada laboral debido a problemas con su ruta matutina. Luego, el cálculo por lotes de los informes fue el primero en comenzar, pero el plan de consulta fue completamente malo para los casos restantes debido a valores inapropiados de las variables asociadas ".

Y algunas vistas útiles del sistema:
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/es434444/


All Articles