Você está na página 1de 12

Oracle SQL Tuning: Quando ocorre um Full Table Scan?

Posted by ricardo@nervinformatica.com.br
http://nervinformatica.com.br/blog/index.php/2014/05/28/sql-tuning-quando-ocorre-um-full-table-scan/

Os fatores que influenciam o CBO na decisão sobre utilizar um índice, ou executar um Full Table
Scan são três:

 Seletividade: a fração de linhas retornadas pela consulta em relação ao total da tabela;


 Clustering Factor: o alinhamento dos dados na tabela (que não é ordenada) em relação ao
índice (que é ordenado);
 Quantos blocos serão desprezados após a aplicação de um filtro utilizado na consulta.

Ou seja, não é apenas o percentual dos dados retornados pela consulta em relação ao total da
tabela.

Vamos comprovar, criando duas tabelas idênticas em estrutura, e que possuem os mesmos dados,
mas distribuídos de forma diferente.

Estas duas tabelas terão um índice do mesmo tipo e na mesma coluna, e as estatísticas serão
coletadas da mesma forma para ambas.

Microsoft Windows [versão 6.1.7601]

Copyright (c) 2009 Microsoft Corporation. Todos os direitos reservados.

C:UsersRicardo>sqlplus SCOTT/TIGER

SQL*Plus: Release 11.2.0.4.0 Production on Qua Mai 28 19:41:42 2014

Copyright (c) 1982, 2013, Oracle. All rights reserved.

Conectado a: Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 - Production

With the Partitioning, Oracle Label Security, OLAP, Data Mining,

Oracle Database Vault and Real Application Testing options

SQL> SET PAGES 1000

SQL> SET LINES 210

SQL> CREATE TABLE T1 AS SELECT TRUNC((ROWNUM-1)/100) ID,


RPAD(ROWNUM,100) NAME FROM DBA_SOURCE WHERE ROWNUM <= 10000;

Tabela criada.

SQL> CREATE INDEX IDX1_T1 ON T1(ID);

índice criado.
SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS('SCOTT', 'T1', METHOD_OPT=>'FOR ALL COLUMNS SIZE
1', CASCADE=>TRUE);

Procedimento PL/SQL concluído com sucesso.

SQL> CREATE TABLE T2 AS SELECT MOD(ROWNUM,100) ID, RPAD(ROWNUM,100) NAME FROM DBA_SOURCE
WHERE ROWNUM <= 10000;

Tabela criada.

SQL> CREATE INDEX IDX2_T2 ON T2(ID);

Índice criado.

SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS('SCOTT', 'T2', METHOD_OPT=>'FOR ALL COLUMNS SIZE


1', CASCADE=>TRUE);

Procedimento PL/SQL concluído com sucesso.

SQL>

Vejam que as tabela possuem a mesma quantidade de linhas, e também a mesma quantidade de
linhas com o valor 1 na coluna ID, assim como a quantidade de linhas com o valor menor que 5. Na
verdade, as tabelas são idênticas em todas as linhas.

SQL> SELECT COUNT(*) FROM T1;

COUNT(*)

----------

10000

SQL> SELECT COUNT(*) FROM T2;

COUNT(*)

----------

10000

SQL> SELECT COUNT(*) FROM T1 WHERE ID = 1;

COUNT(*)

----------

100

SQL> SELECT COUNT(*) FROM T2 WHERE ID = 1;

COUNT(*)

----------
100

SQL> SELECT COUNT(*) FROM T1 WHERE ID IN (1,2,3,4,5);

COUNT(*)

----------

500

SQL> SELECT COUNT(*) FROM T2 WHERE ID IN (1,2,3,4,5);

COUNT(*)

----------

500

SQL> SELECT COUNT(*) FROM T1 WHERE ID IN (1,2,3,4,5,6,7,8,9,10);

COUNT(*)

----------

1000

SQL> SELECT COUNT(*) FROM T2 WHERE ID IN (1,2,3,4,5,6,7,8,9,10);

COUNT(*)

----------

1000

SQL>

Vamos então ver alguns planos de execução. Primeiramente, vamos ver o método de acesso que o
CBO escolhe para uma seletividade de 1% de cada tabela.

SQL> SET AUTOTRACE TRACEONLY

SQL> SELECT ID, NAME FROM T1 WHERE ID = 1;

100 linhas selecionadas.

Plano de Execução

----------------------------------------------------------

Plan hash value: 1182251260

---------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

---------------------------------------------------------------------------------------

| 0 | SELECT STATEMENT | | 100 | 10400 | 3 (0)| 00:00:01 |

| 1 | TABLE ACCESS BY INDEX ROWID| T1 | 100 | 10400 | 3 (0)| 00:00:01 |

|* 2 | INDEX RANGE SCAN | IDX1_T1 | 100 | | 1 (0)| 00:00:01 |

---------------------------------------------------------------------------------------

Predicate Information (identified by operation id):

---------------------------------------------------

2 - access("ID"=1)

Estatísticas

----------------------------------------------------------

1 recursive calls

0 db block gets

19 consistent gets

0 physical reads

0 redo size

12192 bytes sent via SQL*Net to client

481 bytes received via SQL*Net from client

8 SQL*Net roundtrips to/from client

0 sorts (memory)

0 sorts (disk)

100 rows processed

SQL> SELECT ID, NAME FROM T2 WHERE ID = 1;

100 linhas selecionadas.

Plano de Execução

----------------------------------------------------------

Plan hash value: 1513984157


--------------------------------------------------------------------------

| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

--------------------------------------------------------------------------

| 0 | SELECT STATEMENT | | 100 | 10400 | 13 (0)| 00:00:01 |

|* 1 | TABLE ACCESS FULL| T2 | 100 | 10400 | 13 (0)| 00:00:01 |

--------------------------------------------------------------------------

Predicate Information (identified by operation id):

---------------------------------------------------

1 - filter("ID"=1)

Estatísticas

----------------------------------------------------------

1 recursive calls

0 db block gets

162 consistent gets

0 physical reads

0 redo size

11872 bytes sent via SQL*Net to client

481 bytes received via SQL*Net from client

8 SQL*Net roundtrips to/from client

0 sorts (memory)

0 sorts (disk)

100 rows processed

SQL>

Epa, como os métodos de acesso são diferentes para dados iguais? E como o índice não é utilizado
para a tabela T2, se estou solicitando apenas 1% da tabela?
Vamos repetir o teste com 5% das linhas das tabelas.

SQL> SELECT ID, NAME FROM T1 WHERE ID IN (1,2,3,4,5);

500 linhas selecionadas.

Plano de Execução

----------------------------------------------------------

Plan hash value: 3371745543

----------------------------------------------------------------------------------------

| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

----------------------------------------------------------------------------------------

| 0 | SELECT STATEMENT | | 500 | 52000 | 12 (0)| 00:00:01 |

| 1 | INLIST ITERATOR | | | | | |

| 2 | TABLE ACCESS BY INDEX ROWID| T1 | 500 | 52000 | 12 (0)| 00:00:01 |

|* 3 | INDEX RANGE SCAN | IDX1_T1 | 500 | | 4 (0)| 00:00:01 |

----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):

---------------------------------------------------

3 - access("ID"=1 OR "ID"=2 OR "ID"=3 OR "ID"=4 OR "ID"=5)

Estatísticas

----------------------------------------------------------

0 recursive calls

0 db block gets

88 consistent gets

0 physical reads

0 redo size

59302 bytes sent via SQL*Net to client

778 bytes received via SQL*Net from client


35 SQL*Net roundtrips to/from client

0 sorts (memory)

0 sorts (disk)

500 rows processed

SQL> SELECT ID, NAME FROM T2 WHERE ID IN (1,2,3,4,5);

500 linhas selecionadas.

Plano de Execução

----------------------------------------------------------

Plan hash value: 1513984157

--------------------------------------------------------------------------

| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

--------------------------------------------------------------------------

| 0 | SELECT STATEMENT | | 500 | 52000 | 14 (8)| 00:00:01 |

|* 1 | TABLE ACCESS FULL| T2 | 500 | 52000 | 14 (8)| 00:00:01 |

--------------------------------------------------------------------------

Predicate Information (identified by operation id):

---------------------------------------------------

1 - filter("ID"=1 OR "ID"=2 OR "ID"=3 OR "ID"=4 OR "ID"=5)

Estatísticas

----------------------------------------------------------

0 recursive calls

0 db block gets

189 consistent gets

0 physical reads

0 redo size

57358 bytes sent via SQL*Net to client

778 bytes received via SQL*Net from client


35 SQL*Net roundtrips to/from client

0 sorts (memory)

0 sorts (disk)

500 rows processed

SQL>

O comportamento continua. E para 9% dos dados das tabelas?

SQL> SELECT ID, NAME FROM T1 WHERE ID IN (1,2,3,4,5,6,7,8,9);

900 linhas selecionadas.

Plano de Execução

----------------------------------------------------------

Plan hash value: 3617692013

--------------------------------------------------------------------------

| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

--------------------------------------------------------------------------

| 0 | SELECT STATEMENT | | 900 | 93600 | 14 (8)| 00:00:01 |

|* 1 | TABLE ACCESS FULL| T1 | 900 | 93600 | 14 (8)| 00:00:01 |

--------------------------------------------------------------------------

Predicate Information (identified by operation id):

---------------------------------------------------

1 - filter("ID"=1 OR "ID"=2 OR "ID"=3 OR "ID"=4 OR "ID"=5 OR "ID"=6

OR "ID"=7 OR "ID"=8 OR "ID"=9)

Estatísticas

----------------------------------------------------------

0 recursive calls

0 db block gets

215 consistent gets

0 physical reads
0 redo size

103427 bytes sent via SQL*Net to client

1064 bytes received via SQL*Net from client

61 SQL*Net roundtrips to/from client

0 sorts (memory)

0 sorts (disk)

900 rows processed

SQL> SELECT ID, NAME FROM T2 WHERE ID IN (1,2,3,4,5,6,7,8,9);

900 linhas selecionadas.

Plano de Execução

----------------------------------------------------------

Plan hash value: 1513984157

--------------------------------------------------------------------------

| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

--------------------------------------------------------------------------

| 0 | SELECT STATEMENT | | 900 | 93600 | 14 (8)| 00:00:01 |

|* 1 | TABLE ACCESS FULL| T2 | 900 | 93600 | 14 (8)| 00:00:01 |

--------------------------------------------------------------------------

Predicate Information (identified by operation id):

---------------------------------------------------

1 - filter("ID"=1 OR "ID"=2 OR "ID"=3 OR "ID"=4 OR "ID"=5 OR "ID"=6

OR "ID"=7 OR "ID"=8 OR "ID"=9)

Estatísticas

----------------------------------------------------------

1 recursive calls

0 db block gets
215 consistent gets

0 physical reads

0 redo size

102738 bytes sent via SQL*Net to client

1064 bytes received via SQL*Net from client

61 SQL*Net roundtrips to/from client

0 sorts (memory)

0 sorts (disk)

900 rows processed

SQL>

Agora chegamos a um ponto onde os métodos de acesso são iguais para as duas tabelas, e isto
aconteceu para a T1 até antes do lendário limite de 10% de seletividade para uso de índice.

O motivo dos planos de execução diferentes é este:

SQL> SELECT LEAF_BLOCKS, BLEVEL, CLUSTERING_FACTOR FROM USER_INDEXES WHERE INDEX_NAME =


'IDX1_T1';

LEAF_BLOCKS BLEVEL CLUSTERING_FACTOR

----------- ---------- -----------------

20 1 152

SQL> SELECT LEAF_BLOCKS, BLEVEL, CLUSTERING_FACTOR FROM USER_INDEXES WHERE INDEX_NAME =


'IDX2_T2';

LEAF_BLOCKS BLEVEL CLUSTERING_FACTOR

----------- ---------- -----------------

20 1 10000

SQL>

Por causa da forma da alimentação dos dados na tabela (TRUNC((ROWNUM-1)/100) na T1, versus
MOD(ROWNUM,100) na T2), as linhas com o mesmo valor (por exemplo, 1) na coluna ID ficaram
todas juntas na tabela T1, no nível de bloco, enquanto na tabela T2 uma linha com o valor 1 na
coluna ID ficou longe (em outro bloco) da próxima linha com o valor 1. Ou seja, na T2 os dados
estão espalhados, enquanto o índice é ordenado. Logo, chega um momento em que o CBO vê que é
mais custoso percorrer entrada a entrada do índice, para buscar blocos não contíguos na tabela.

Para finalizar, veja o que faz um REBUILD dos índices.


SQL> ALTER INDEX IDX1_T1 REBUILD;

Índice alterado.

SQL> ALTER INDEX IDX2_T2 REBUILD;

Índice alterado.

SQL> SELECT LEAF_BLOCKS, BLEVEL, CLUSTERING_FACTOR FROM USER_INDEXES WHERE INDEX_NAME =


'IDX1_T1';

LEAF_BLOCKS BLEVEL CLUSTERING_FACTOR

----------- ---------- -----------------

20 1 152

SQL> SELECT LEAF_BLOCKS, BLEVEL, CLUSTERING_FACTOR FROM USER_INDEXES WHERE INDEX_NAME =


'IDX2_T2';

LEAF_BLOCKS BLEVEL CLUSTERING_FACTOR

----------- ---------- -----------------

20 1 10000

SQL>

Absolutamente nada. Isto acontece porque o Clustering Factor se refere a ordenação de dados na
tabela, e esta não foi alterada.

Se quiséssemos “arrumar” a tabela T2, teríamos que fazer algo assim:

SQL> CREATE TABLE T3 AS SELECT * FROM T2 ORDER BY ID;

Tabela criada.

SQL> CREATE INDEX IDX3_T3 ON T3(ID);

Índice criado.

SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS('SCOTT', 'T3', METHOD_OPT=>'FOR ALL COLUMNS SIZE


1', CASCADE=>TRUE);

Procedimento PL/SQL concluÝdo com sucesso.

SQL> SELECT LEAF_BLOCKS, BLEVEL, CLUSTERING_FACTOR FROM USER_INDEXES WHERE INDEX_NAME =


'IDX3_T3';

LEAF_BLOCKS BLEVEL CLUSTERING_FACTOR

----------- ---------- -----------------

20 1 152
SQL>

Veja que isto não necessariamente tornaria o SQL mais rápido, apenas faria com que o plano de
execução utilizasse outro método de acesso. E se a tabela tivesse mais índices, em outras colunas,
você provavelmente estaria aumentando o Clustering Factor de outro índice.

Em resumo: para SQL Tuning, o DBA deve não só conhecer os dados, mas como eles são
armazenados, e de que forma são selecionados.