Páginas

17 de fev. de 2011

Criando tabelas particionadas para otimizar consultas em tabelas

Olá pessoal,
  
    Neste artigo vou apresentar para vocês o conceito de tabelas particionadas no Oracle Database e vou demonstrar como criar uma tabela particionada para possibilitar ganho de perfomance no acesso e atualização dos dados.
  
    Uma tabela particionada é uma tabela dividida em partes menores, chamadas partições, que são criadas para facilitar o gerenciamento e possibilitar melhor desempenho em consultas e atualizações.
  
    As principais características das partições são:
        - Possuem os mesmos atributos lógicos:
                 Todas as partições possuem as mesmas colunas, constraints e índices;
        - Atributos físicos diferentes:
                  Para melhor desempenho as partições devem ser armazenadas em tablespaces distintos. Se possível, cada tablespace deve ser armazenado em um disco diferente;
        - São transparentes para as aplicações:
                  As aplicações referenciam as tabelas particionadas do mesmo modo que referenciam as tabelas heap (normais), pois as aplicações fazem referência às tabelas e não às partições.
     
    As tabelas podem ser particionadas através de vários métodos:
        - Particionamento por faixa:
                As partições são divididas em faixas lógicas de valores de colunas, como por exemplo, meses de um ano.
        - Particionamento por hash:
                As partições são divididas com base no valor hash de uma chave de particionamento.
        - Particionamento por lista:
                As partições são divididas por listas discretas de valores, fornecidas pelo DBA.
        - Particionamento por faixa/hash:
                As partições são divididas utilizando-se como base o método de faixa e, em cada partição por faixa, criando-se subpartições por hash.
        - Particionamento por faixa/lista:
                As partições são divididas primeiro em uma faixa de valores e, depois, com base em valores discretos.
 
    Para criar tabelas particionadas, no Oracle Database 10G Enterprise Edition (versão do Oracle mais utilizada), é necessário obter licenciamento da option Oracle Partitioning.
               
    A melhor forma de otimizar o acesso e atualização dos dados em tabelas particionadas é armazenar as partições em discos diferentes. A minha recomendação principal para criar tabelas particionadas é utilizar este recurso somente quando uma tabela irá armazenar uma "enorme" quantidade de dados. Tabelas pequenas (com poucas linhas e/ou colunas com poucos dados) dificilmente terão ganhos de performance se forem particionadas.
 
    O método de particionamento mais comumente utilizado é o método de Particionamento por faixa, que será o método do exemplo deste artigo. Para demonstrar o ganho de performance de uma consulta em tabelas particionadas, criaremos 2 tabelas com a mesma estrutura e mesmos dados. A tabela CLIENTE será uma tabela heap (normal) e a tabela CLIENTE_PART será uma tabela particionada, com 3 partições, divididas por faixas de valores através da coluna que armazena a data de nascimento. As tabelas armazenarão cada uma 300 mil registros de clientes.
  
--------------------------------------------------------------------------
    Para iniciar o passo-a-passo abaixo, é necessário conectar-se previamente no Banco de Dados desejado, através do SQL Plus, SQL Delevoper ou outra ferramenta compatível, com um usuário que tenha privilégios para criar tabelas e tablespaces. De preferência não se conecte ou crie as tabelas no schema do usuário SYS.
--------------------------------------------------------------------------

     PASSO 1: Criando os tablespaces que irão armazenar a tabela heap (CLIENTE) e as 3 partições da tabela particionada (CLIENTE_PART):

--------------------------------------------------------------------------
 Se possível, crie os tablespaces TBS_PART_ATE_1920, TBS_PART_1920_1970
e TBS_PART_MAIOR_1970 em discos diferentes.
--------------------------------------------------------------------------

        CREATE TABLESPACE TBS_NORMAL LOGGING DATAFILE 'clientes_normal.dbf' SIZE 1M AUTOEXTEND ON NEXT 10M MAXSIZE UNLIMITED EXTENT MANAGEMENT LOCAL SEGMENT SPACE MANAGEMENT AUTO;  
   
        CREATE TABLESPACE TBS_PART_ATE_1920 LOGGING DATAFILE 'clientes_PART_ATE_1920.dbf' SIZE 1M AUTOEXTEND ON NEXT 10M MAXSIZE UNLIMITED EXTENT MANAGEMENT LOCAL SEGMENT SPACE MANAGEMENT AUTO;  

  
        CREATE TABLESPACE TBS_PART_1920_1970 LOGGING DATAFILE 'clientes_PART_1920_1970.dbf' SIZE 1M AUTOEXTEND ON NEXT 10M MAXSIZE UNLIMITED EXTENT MANAGEMENT LOCAL SEGMENT SPACE MANAGEMENT AUTO;
  
        CREATE TABLESPACE TBS_PART_MAIOR_1970 LOGGING DATAFILE 'clientes_PART_MAIOR_1970.dbf' SIZE 1M AUTOEXTEND ON NEXT 10M MAXSIZE UNLIMITED EXTENT MANAGEMENT LOCAL SEGMENT SPACE MANAGEMENT AUTO;
     
        Obs.: Substitua os nomes dos arquivos (em vermelho) pelo caminho completo do arquivo (pasta + nome do arquivo).
                Ex. no Windows:
                          C:\Oracle\10GR2\ORACLE_SID\clientes_normal.dbf  ---> ORACLE_SID = nome do BD                Ex. no Linux:
                           /ora01/dados/ORACLE_SID/clientes_normal.dbf   ---> ORACLE_SID = nome do BD    
  
  
     PASSO 2: Criando as tabelas para armazenar clientes:
  
        a) Criando a tabela heap:
   
           CREATE TABLE CLIENTES           (
               ID_CLIENTE                          NUMBER(6,0) PRIMARY KEY ,
               NM_CLIENTE                       VARCHAR2(50) NOT NULL ENABLE,
               NR_ANO_NASCIMENTO    NUMBER(4) NOT NULL ENABLE,
               ID_SEXO                                CHAR(1),
               NR_CPF                                  VARCHAR2(11)
           ) TABLESPACE TBS_NORMAL;  

         
        b) Criando a tabela particionada:
  
           CREATE TABLE CLIENTES_PART
           (
               ID_CLIENTE                          NUMBER(6,0) PRIMARY KEY ,
               NM_CLIENTE                       VARCHAR2(50) NOT NULL ENABLE,
               NR_ANO_NASCIMENTO   NUMBER(4) NOT NULL ENABLE,
               ID_SEXO                               CHAR(1),
              NR_CPF                                  VARCHAR2(11)
          )
           PARTITION BY RANGE (NR_ANO_NASCIMENTO)
           (
               PARTITION PART_ATE_1920 VALUES LESS THAN (1920) TABLESPACE TBS_PART_ATE_1920,
               PARTITION PART_1920_1970 VALUES LESS THAN (1970) TABLESPACE TBS_PART_1920_1970,
               PARTITION PART_MAIOR_1970 VALUES LESS THAN (MAXVALUE) TABLESPACE TBS_PART_MAIOR_1970);

              
  
  
     PASSO 3: Inserindo dados (300.000 linhas) nas 2 tabelas criadas no passo anterior :
   
        DECLARE
               I NUMBER;
               v_ano number := 1800;
        BEGIN
               for i in 1..100000 loop
                    INSERT INTO CLIENTES (ID_CLIENTE, NM_CLIENTE, NR_ANO_NASCIMENTO, ID_SEXO)
                    VALUES (I, 'Nome ' || I, V_ANO , 'M');       
                   
                    INSERT INTO CLIENTES_PART (ID_CLIENTE, NM_CLIENTE, NR_ANO_NASCIMENTO, ID_SEXO)
                    VALUES (I, 'Nome ' || I, V_ANO , 'M');       
                    v_ano:=v_ano+1;
                   
                    IF V_ANO = 1919 THEN
                      v_ano:=1800;
                    end if;
               end loop;
              
               V_ANO:= 1920;  
               for i in 100001..200000 loop
                    INSERT INTO CLIENTES (ID_CLIENTE, NM_CLIENTE, NR_ANO_NASCIMENTO, ID_SEXO)
                    VALUES (I, 'Nome ' || I, v_ano , 'F');       
                   
                    INSERT INTO CLIENTES_PART (ID_CLIENTE, NM_CLIENTE, NR_ANO_NASCIMENTO, ID_SEXO)
                    VALUES (I, 'Nome ' || I, v_ano , 'F');       
                    v_ano:=v_ano+1;
                   
                    IF V_ANO = 1969 THEN
                      V_ANO:=1920;
                    end if;
               end loop;
              
               V_ANO:= 1970;
               for i in 200001..300000 loop
                    INSERT INTO CLIENTES (ID_CLIENTE, NM_CLIENTE, NR_ANO_NASCIMENTO, ID_SEXO)
                    VALUES (I, 'Nome ' || I, v_ano , 'F');
                   
                    INSERT INTO CLIENTES_PART (ID_CLIENTE, NM_CLIENTE, NR_ANO_NASCIMENTO, ID_SEXO)
                    VALUES (I, 'Nome ' || I, v_ano , 'F');       
                    v_ano:=v_ano+1;
                   
                    IF V_ANO = 2011 THEN
                      V_ANO:=1970;
                    end if;
               END LOOP;
               COMMIT;
        END;
 
  
  
     Passo 4: Comparando o desempenho de consultas entre as tabelas normal e particionada:
       
        a) Gerando o plano de execução de uma consulta na tabela heap (CLIENTES), filtrando os dados de retorno para exibir somente clientes que nasceram no ano de 1920:
  
            EXPLAIN PLAN FOR
                SELECT * FROM CLIENTES 
                WHERE  NR_ANO_NASCIMENTO IN (1920);
            SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
          
  
        Resultado:       
            -------------------------------------------------------------------------------
            | Id  | Operation                         | Name            | Rows | Bytes | Cost (%CPU)| Time
            -------------------------------------------------------------------------------
            |   0 | SELECT STATEMENT  |                       |  1610 | 90160 |   284   (3)       | 00:00:04                                                                 
            |*  1 |  TABLE ACCESS FULL| CLIENTES  |  1610 | 90160 |   284   (3)        | 00:00:04
            -------------------------------------------------------------------------------

     
        Obs.: No meu ambiente de testes, o plano de execução informou que o tempo de retorno da consulta é de 4s.
          
        b) Gerando o plano de execução de uma consulta na tabela particionada (CLIENTES_PART), filtrando os dados de retorno para exibir somente clientes que nasceram no ano de 1920:
  
            EXPLAIN PLAN FOR
                SELECT * FROM CLIENTES_PART 
                WHERE  NR_ANO_NASCIMENTO IN (1920);
            SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

         
        Resultado:
            ---------------------------------------------------------------------------------------------------
            | Id  | Operation                                      | Name          | Rows  | Bytes | Cost (%CPU)| Time    
            ---------------------------------------------------------------------------------------------------
            |   0 | SELECT STATEMENT               |                     |  2157 |   117K|   123  (11)       | 00:00:02                                              
            |   1 |  PARTITION RANGE SINGLE|                            |  2157 |   117K|   123  (11)| 00:00:02
            |*  2 |   TABLE ACCESS FULL    | CLIENTES_PART |  2157  |   117K|   123  (11)| 00:00:02
            ---------------------------------------------------------------------------------------------------

   
        Obs.: No meu ambiente de testes, o plano de execução informou que o tempo de retorno da consulta é de 2s.

   
CONCLUSÃO:
  
           Nos testes deste artigo pudemos verificar uma situação em que tivemos um ganho de performance de 50% no tempo de execução de uma consulta ao utilizar uma tabela particionada. Em determinadas situações, principalmente em tabelas muito grandes, o ganho de performance pode ser ainda maior, mas ressalto que, nem todas as consultas ou atualizações terão o mesmo desempenho. Se a tabela for pequena o tempo de execução poderá até piorar.

   No exemplo que vimos neste artigo, a consulta foi efetuada filtrando dados em uma coluna que não tinha índices, portanto, o Oracle teve que fazer um FULL TABLE SCAN (FTS) na tabela. O tempo do FTS na tabela particionada foi menor do que na tabela heap, pois os dados do retorno da consulta estavam todos na partição do tablespace TBS_PART_1920_1970, que tinha menos dados que o tablespace TBS_NORMAL, da tabela heap.        

  
Script para limpeza dos testes efetuados neste artigo:
         DROP TABLE CLIENTES PURGE;
            DROP TABLE CLIENTES_PART PURGE;
            DROP TABLESPACE TBS_NORMAL INCLUDING CONTENTS AND DATAFILES;
            DROP TABLESPACE TBS_PART_ATE_1920 INCLUDING CONTENTS AND DATAFILES;
            DROP TABLESPACE TBS_PART_1920_1970 INCLUDING CONTENTS AND DATAFILES;
            DROP TABLESPACE TBS_PART_MAIOR_1970 INCLUDING CONTENTS AND DATAFILES;



Referências:
    - Material do curso oficial: Oracle Database 10G Administration Workshop II.

6 comentários:

  1. Olá Fábio,
    é obrigatório usar tablespaces diferentes para obter uma melhor performace nos particionamentos no método hash? Ou posso usar uma mesmo tablespace dividida por "N" PARTITIONS?

    abs
    Christian

    ResponderExcluir
    Respostas
    1. Christian, não é obrigatório múltiplos tablespaces, mas vc terá ganhos menores se as partições ficarem no mesmo tablespace.

      Uma grande vantagem de armazenar as partições em tablespaces diferentes é que eles podem também estar em discos diferentes, e desse modo, a performance será muito melhor quando uma consulta precisar, por exemplo, consultar dados de N partições!

      []s

      Excluir
  2. Boa noite, em tabelas já populadas existe uma maneira de fazer o particionamento?
    Desde já agradeço!!

    ResponderExcluir
    Respostas
    1. Rodrigo, dá para fazer isso sim e isso se chama Exchange Partition: https://oracle-base.com/articles/misc/partitioning-an-existing-table-using-exchange-partition, ok?

      []s

      Excluir
  3. Boa tarde Fábio, sou seu seguidor e admirador do seu trabalho e por compartilhar seus preciosos conhecimentos conosco. Parabéns! Obrigado por compartilhar o seu conhecimento conosco.

    No entanto eu tenho uma dúvida, quanto aos particionamentos. Se puder me ajudar, desde já eu fico muito agradecido.

    Se eu criar uma partição para um grupo de dados da tabela, estes dados, dentro do range da partição serão alocados em blocos específicos daquela partição. Agora, se eu criar uma partição futura, ou seja, para um range que não existe ainda na tabela, a partição será criada mas estes blocos ainda estarão vazios, por que não existe os dados. Posteriormente, ao inserir alguns dados que abrangem o período criado anteriormente, esses dados serão alocados automaticamente na partição criada?

    Grato.

    ResponderExcluir
    Respostas
    1. Renato, primeiramente obrigado pelos comentários. A partição que ainda não tem dados terá já "normalmente" um segmento iniciado com 8 blocos contendo os seus metadados e espaço para dados. Quando forem inseridas linhas nessa partição nova ela irá encher primeiramente esses blocos que foram alocados inicialmente, e depois criará novos de acordo com a necessidade e configuração do tablespace.

      Excluir