Introdução

Quando precisamos analisar um plano de execução de uma consulta SQL no Oracle, podemos precisar da versão completa do plano incluindo as estatísticas reais das últimas execuções (ao invés de confiar puramente nas estimativas que aparecem no plano de execução convencional).

Por exemplo, a query com SQL_ID “90wxxwzxgpxvs” demonstrada abaixo apresenta a seguinte estrutura:

Plan hash value: 4040251747                                                                                                                        
                                                                                                                                                   
-----------------------------------------------------------------------------------------------------------------------------------------------    
| Id  | Operation                          | Name         | E-Rows |E-Bytes| Cost (%CPU)| E-Time   | Pstart| Pstop |  OMem |  1Mem | Used-Mem |    
-----------------------------------------------------------------------------------------------------------------------------------------------    
|   0 | SELECT STATEMENT                   |              |        |       | 32617 (100)|          |       |       |       |       |          |    
|   1 |  HASH GROUP BY                     |              |      1 |    56 | 32617   (1)| 00:00:02 |       |       |  1034K|  1034K|  554K (0)|    
|*  2 |   HASH JOIN                        |              |    328 | 18368 | 32616   (1)| 00:00:02 |       |       |  1572K|  1572K|  747K (0)|    
|*  3 |    TABLE ACCESS FULL               | COUNTRIES    |      1 |    18 |     3   (0)| 00:00:01 |       |       |       |       |          |    
|   4 |    NESTED LOOPS                    |              |   7225 |   268K| 32613   (1)| 00:00:02 |       |       |       |       |          |    
|   5 |     NESTED LOOPS                   |              |   7225 |   268K| 32613   (1)| 00:00:02 |       |       |       |       |          |    
|   6 |      VIEW                          | VW_GBC_16    |   7225 |   190K| 25366   (1)| 00:00:01 |       |       |       |       |          |    
|   7 |       HASH GROUP BY                |              |   7225 |   232K| 25366   (1)| 00:00:01 |       |       |  1417K|  1417K| 1866K (0)|    
|*  8 |        HASH JOIN                   |              |   7225 |   232K| 25365   (1)| 00:00:01 |       |       |  1506K|  1506K| 1279K (0)|    
|   9 |         PART JOIN FILTER CREATE    | :BF0000      |      7 |   112 |    27   (0)| 00:00:01 |       |       |       |       |          |    
|* 10 |          TABLE ACCESS FULL         | TIMES        |      7 |   112 |    27   (0)| 00:00:01 |       |       |       |       |          |    
|  11 |         PARTITION RANGE JOIN-FILTER|              |     11M|   180M| 25308   (1)| 00:00:01 |:BF0000|:BF0000|       |       |          |    
|  12 |          TABLE ACCESS FULL         | SALES        |     11M|   180M| 25308   (1)| 00:00:01 |:BF0000|:BF0000|       |       |          |    
|* 13 |      INDEX UNIQUE SCAN             | CUSTOMERS_PK |      1 |       |     1   (0)| 00:00:01 |       |       |       |       |          |    
|  14 |     TABLE ACCESS BY INDEX ROWID    | CUSTOMERS    |      1 |    11 |     2   (0)| 00:00:01 |       |       |       |       |          |    
-----------------------------------------------------------------------------------------------------------------------------------------------    

Neste exemplo temos apenas as colunas com o prefixo “E-” , de “Estimated”. Então você decide visualizar o plano com as métricas reais, que são apresentadas naquelas colunas com o prefixo “A-” de “Actual”, e tenta obter isso incluindo a opção “allstats” na chamada da DBMS_XPLAN.DISPLAY_CURSOR:

SELECT * FROM table(DBMS_XPLAN.DISPLAY_CURSOR(
 SQL_ID=>'90wxxwzxgpxvs', 
 CURSOR_CHILD_NO=>0, 
 FORMAT=>'ALL allstats last'
));

Mas o plano resultante é exatamente igual ao anterior, com a única diferença que ele inclui essa Nota no rodapé:

Note                                                                                                                                               
-----                                                                                                                                              
   - Warning: basic plan statistics not available. These are only collected when:                                                                  
       * hint 'gather_plan_statistics' is used for the statement or                                                                                
       * parameter 'statistics_level' is set to 'ALL', at session or system level                                                                  
                                                                                              

Aqui você se depara com algumas alternativas como: Alterar o parâmetro statistics_level para ALL a nível de sistema, o que é impraticável se você está fazendo isso em um banco de dados importante (“e se ele não é importante, por que você precisa dele ?” — Ricardo Portilho).

Outra opção mais usual seria alterar esse parâmetro a nível de sessão, sendo possível criar uma Trigger de Logon para executar o “ALTER SESSION” quando chegar uma nova conexão da aplicação ou módulo específico. Mas essa abordagem também pode ser impraticável em ambientes altamente críticos em que a criação de uma Trigger de Logon poderia causar um impacto muito maior na aplicação, além de possivelmente precisar passar por um fluxo de aprovação que só falta pedir a benção do papa.

Agora a gente cai na terceira opção que é adicionar esse HINT “gather_plan_statistics” na Query, algo que você pode simplesmente pegar o texto da consulta e fazer isso executando manualmente no SQL Developer. Mas eventualmente essa abordagem também pode adicionar mais variáveis ao teste, fazendo com que o otimizador gere um plano de execução diferente daquele que está sendo usado pela a aplicação.

A dica

A dica deste post é se você quer ou precisa adicionar esse HINT a query original pra avaliar o comportamento gerado pela própria aplicação, sem alterar a aplicação para adicionar o HINT no texto da query.

Solução: Criar um SQL Patch !

Se você ainda não sabe o que é ou nunca usou esse recurso, de forma bem resumida: Ele permite anexar HINTs em um SQL_ID de forma dinâmica. Se você pesquisar por “Oracle SQL Patch” no Google, irá encontrar vários posts de boas referências explicando sobre como usá-lo. Eu já falei um pouco sobre SQL Patch no post “3 Abordagens Diferentes Para Fixar Planos de Execução de Comandos SQL no Oracle Database“.

Voltando ao foco deste post, o objetivo aqui é apenas incluir o HINT “gather_plan_statistics” para o SQL_ID que queremos analisar. Neste caso, não iremos usar o SQL Patch para influenciar as decisões do otimizador, apenas para forçar a coleta das estatísticas de execução da query.

Exemplo para versão 12cR2 ou superior:

SET SERVEROUTPUT ON;
DECLARE
l_patch_name VARCHAR2(100);
l_SQLID  VARCHAR2(32) := '90wxxwzxgpxvs';
vName VARCHAR2(32) := 'patch_'|| l_SQLID || '_plan_stats';
BEGIN

l_patch_name := sys.dbms_sqldiag.create_sql_patch(
 sql_id=> l_SQLID
,hint_text => 'gather_plan_statistics' 
,name=> vName
,category=>'DEFAULT'
);

dbms_output.put_line('SQL Patch: ' || l_patch_name);
END;
/

Para as versões 11g e 12cR1:

SET SERVEROUTPUT ON;
DECLARE
 l_sql_text CLOB;
 vSQL_ID varchar2(32) := '90wxxwzxgpxvs';
 vCount number;
 vName VARCHAR2(32) := 'patch_'|| l_SQLID || '_plan_stats';
BEGIN

 SELECT sql_fulltext INTO l_sql_text FROM gv$sqlarea WHERE sql_id = vSQL_ID AND rownum=1;
 
 SYS.DBMS_SQLDIAG_INTERNAL.I_CREATE_PATCH (
   name => vName,
   sql_text => l_sql_text,
   hint_text => 'gather_plan_statistics' 
 );

 DBMS_OUTPUT.PUT_LINE('Patch criado: ' || vName);
END;
/

Tecnicamente existe a possibilidade de o código acima falhar se o SQL_ID não estiver mais disponível na memória, mas considerando que o contexto aqui possivelmente envolve a análise de algo que está executando ou executou recentemente, esse script deve funcionar na maioria dos casos.

No entanto, se você eventualmente precisar criar um SQL_PATCH para uma query que não está disponível na memória no momento, e assumindo que pode usar o AWR, basta alterar a linha 9, trocando o nome da view GV$SQLAREA para DBA_HIST_SQLTEXT que funciona da mesma forma.

Demo

SYSTEM@DBSOL1; SET SERVEROUTPUT ON;
SYSTEM@DBSOL1; DECLARE
  2  l_patch_name VARCHAR2(100);
  3  l_SQLID  VARCHAR2(32) := '90wxxwzxgpxvs';
  4  vName VARCHAR2(32) := 'patch_'|| l_SQLID || '_plan_stats';
  5  BEGIN
  6  l_patch_name := sys.dbms_sqldiag.create_sql_patch(
  7   sql_id=>; l_SQLID
  8  ,hint_text =>; 'gather_plan_statistics' 
  9  ,name=>; vName
 10  ,category=>;'DEFAULT'
 11  );
 12  dbms_output.put_line('SQL Patch: ' || l_patch_name);
 13  END;
 14* /
SQL Patch: patch_90wxxwzxgpxvs_plan_stats


PL/SQL procedure successfully completed.

Ao consultar a GV$SQL, podemos confirmar que o cursor foi gerando usando o SQL Patch que criamos:

SYSTEM@DBSOL1; select inst_id, sql_id, plan_hash_value, child_number, last_load_time, sql_patch from gv$sql where sql_id='90wxxwzxgpxvs';

   INST_ID SQL_ID              PLAN_HASH_VALUE    CHILD_NUMBER LAST_LOAD_TIME         SQL_PATCH                         
__________ ________________ __________________ _______________ ______________________ _________________________________ 
         1 90wxxwzxgpxvs            4040251747               1 2024-11-11/13:18:46    patch_90wxxwzxgpxvs_plan_stats    

DICA: A criação de um SQL Patch causa um “invalidate” do cursor atual e o Oracle gera um novo cursor usando o SQL Patch na próxima vez que a query for executada, esse comportamento pode ser visto normalmente nas queries que executam em poucos segundos. Se o problema for com uma query de longa duração, pode ser mais produtivo solicitar o cancelamento da execução atual, forçar o purge do SQL_ID e iniciar uma nova execução.

Agora ao consultar o plano com o novo CHILD_NUMBER=1 que foi gerado, temos as novas colunas geradas graças ao hint “gather_plan_statistics”:

SELECT * FROM table(DBMS_XPLAN.DISPLAY_CURSOR(
 SQL_ID=>'90wxxwzxgpxvs', 
 CURSOR_CHILD_NO=>1, 
 FORMAT=>'ALL allstats last'
));

No rodapé do plano você deve ver algo como isso, indicando o uso do SQL Patch, ao invés da antiga mensagem:

Note                                                                                                                                                                                                 
-----                                                                                                                                                                                                
   - SQL patch "patch_90wxxwzxgpxvs_plan_stats" used for this statement                                                                                                                              
                                                                                         

Após concluir o throubleshoting e não precisar mais do HINT, remova o SQL Patch com o comando abaixo:

EXEC SYS.DBMS_SQLDIAG.drop_sql_patch(name => 'patch_90wxxwzxgpxvs_plan_stats');

IMPORTANTE: O desempenho da consulta é afetado quando executada com a coleta de todas as estatísticas, então sempre remova o HINT/SQL_PATCH após não precisar mais dele para fins de Throubleshooting.

Referências:

Using SQL Patch to add hints to a packaged application

Video: Quickly fix bad statements with SQL Patch

How to Create a SQL Patch to add Hints to Application SQL Statements (Doc ID 1931944.1)

Leave a Reply

Discover more from Blog do Dibiei

Subscribe now to keep reading and get access to the full archive.

Continue reading