Muitas vezes nos vemos na necessidade de identificar o último registro de uma tabela baseando-se numa regra específica. Veja o exemplo abaixo:
Criando a Tabela de exemplo:
Create table plz_nota_fiscal (nr_nota_fiscal number ,Tipo_nota_fiscal number ,Data_nota_fiscal date ,Valor_nota_fiscal number); create index plz_nf01 on plz_nota_fiscal (tipo_nota_fiscal);
Inserindo um conjunto de registros para testes:
DECLARE
V_MOD NUMBER;
BEGIN
FOR x IN 1..10 LOOP
FOR y IN 1..50 LOOP
--> Modificador para diferenciar números
if x = 1 then
v_mod := 5;
elsif x = 2 then
v_mod := 13;
elsif x = 3 then
v_mod := 19;
elsif x = 4 then
v_mod := 43;
else
v_mod := 198;
end if;
--> Insere a nota
insert into PLZ_NOTA_FISCAL
(nr_nota_fiscal
,tipo_nota_fiscal
,data_nota_fiscal
,valor_nota_fiscal)
values
(y+v_mod
,x
,sysdate+y+x
,y+x);
END LOOP;
END LOOP;
commit;
END;
/
Agora, precisa identificar somente as últimas notas geradas para cada tipo específico de nota:
Select nr_nota_fiscal
from PLZ_NOTA_FISCAL
where tipo_nota_fiscal = 3
and nr_nota_fiscal = (select MAX(nr_nota_fiscal)
from PLZ_NOTA_FISCAL
where tipo_nota_fiscal = 3)
Agora, vejamos como fica o plano de ação para esta query:
-------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 26 | 2 (0)| 00:00:01 |
|* 1 | TABLE ACCESS BY INDEX ROWID | PLZ_NOTA_FISCAL | 1 | 26 | 1 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | PLZ_NF01 | 2 | | 1 (0)| 00:00:01 |
| 3 | SORT AGGREGATE | | 1 | 26 | | |
| 4 | TABLE ACCESS BY INDEX ROWID| PLZ_NOTA_FISCAL | 50 | 1300 | 1 (0)| 00:00:01 |
|* 5 | INDEX RANGE SCAN | PLZ_NF01 | 2 | | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("NR_NOTA_FISCAL"= (SELECT MAX("NR_NOTA_FISCAL")
FROM "PLZ_NOTA_FISCAL"
WHERE "TIPO_NOTA_FISCAL"=3))
2 - access("TIPO_NOTA_FISCAL"=3)
5 - access("TIPO_NOTA_FISCAL"=3)
Agora vamos testar de outra maneira, só que desta vez usando as funções analíticas disponíveis no Oracle:
select tipo_nota_fiscal
, ultimo_numero
from (select nr_nota_fiscal
, tipo_nota_fiscal
, data_nota_fiscal
, valor_nota_fiscal
, MAX(nr_nota_fiscal) over (partition by tipo_nota_fiscal) ultimo_numero
from PLZ_NOTA_FISCAL)
where nr_nota_fiscal = ultimo_numero
and tipo_nota_fiscal = 3
Agora, vejamos como fica o plano de acesso para esta query:
-------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 50 | 1950 | 1 (0)| 00:00:01 |
|* 1 | VIEW | | 50 | 1950 | 1 (0)| 00:00:01 |
| 2 | WINDOW BUFFER | | 50 | 1300 | 1 (0)| 00:00:01 |
| 3 | TABLE ACCESS BY INDEX ROWID| PLZ_NOTA_FISCAL | 50 | 1300 | 1 (0)| 00:00:01 |
|* 4 | INDEX RANGE SCAN | PLZ_NF01 | 2 | | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("NR_NOTA_FISCAL"="ULTIMO_NUMERO")
4 - access("TIPO_NOTA_FISCAL"=3)
Resultado da comparação entre os planos de acesso:
Na busca usando subquery, ele gerou um custo maior e um plano de acesso onde acessou duas vezes a mesma tabela e o mesmo índice.
Na busca usando funções analíticas, ele gerou um custo menor e um plano de acesso simplificado.
Agora, Vamos fazer o teste retornando o último número de notas fiscais para cada tipo existente:
Primeiro, com a subquery:
Select nr_nota_fiscal
from PLZ_NOTA_FISCAL
where (tipo_nota_fiscal, nr_nota_fiscal) in (select tipo_nota_fiscal
, MAX(nr_nota_fiscal)
from PLZ_NOTA_FISCAL
group by tipo_nota_fiscal)
Vejamos o plano e acesso para esta query:
--------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 25000 | 1562K| 5 (40)| 00:00:01 |
|* 1 | FILTER | | | | | |
| 2 | HASH GROUP BY | | 25000 | 1562K| 5 (40)| 00:00:01 |
| 3 | NESTED LOOPS | | | | | |
| 4 | NESTED LOOPS | | 25000 | 1562K| 3 (0)| 00:00:01 |
| 5 | TABLE ACCESS FULL | PLZ_NOTA_FISCAL | 500 | 13000 | 3 (0)| 00:00:01 |
|* 6 | INDEX RANGE SCAN | PLZ_NF01 | 50 | | 0 (0)| 00:00:01 |
| 7 | TABLE ACCESS BY INDEX ROWID| PLZ_NOTA_FISCAL | 50 | 1900 | 0 (0)| 00:00:01 |
--------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("NR_NOTA_FISCAL"=MAX("NR_NOTA_FISCAL"))
6 - access("TIPO_NOTA_FISCAL"="TIPO_NOTA_FISCAL")
Agora, usando a função analítica:
select tipo_nota_fiscal
, ultimo_numero
from (select nr_nota_fiscal
, tipo_nota_fiscal
, data_nota_fiscal
, valor_nota_fiscal
, MAX(nr_nota_fiscal) over (partition by tipo_nota_fiscal) ultimo_numero
from PLZ_NOTA_FISCAL)
where nr_nota_fiscal = ultimo_numero;
---------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 500 | 19500 | 4 (25)| 00:00:01 |
|* 1 | VIEW | | 500 | 19500 | 4 (25)| 00:00:01 |
| 2 | WINDOW SORT | | 500 | 13000 | 4 (25)| 00:00:01 |
| 3 | TABLE ACCESS FULL| PLZ_NOTA_FISCAL | 500 | 13000 | 3 (0)| 00:00:01 |
---------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("NR_NOTA_FISCAL"="ULTIMO_NUMERO")
Resultado da comparação entre os planos de acesso:
Na busca pela subquery, ele retornou um número bem maior de linhas por causa da cardinalidade entre elas, fazendo acesso full na tabela.
Na busca pela função analítica, ele retornou o número exato de bytes existentes na tabela por não gerar cardinalidade, o que resulta num custo menor, apesar do acesso full.
Apesar da pouca diferença do custo, a diferença no consumo de CPU bem como no número de linhas e bytes processados foi significativo.
Conclusão
Substituir a subquery por uma função analítica nestes casos pode gerar um ganho significativo de desempenho, especialmente se esse comando é executando dentro de um loop.