Melhor maneira de excluir um conjunto de registros muito grande no Oracle

18

Gerencio um aplicativo que possui um back-end de banco de dados Oracle muito grande (quase 1 TB de dados com mais de 500 milhões de linhas em uma tabela). O banco de dados realmente não faz nada (sem SProcs, sem gatilhos ou qualquer coisa), é apenas um armazenamento de dados.

Todo mês, somos obrigados a limpar os registros das duas tabelas principais. O critério para a limpeza varia e é uma combinação de idade da linha e alguns campos de status. Normalmente, acabamos limpando entre 10 e 50 milhões de linhas por mês (adicionamos cerca de 3-5 milhões de linhas por semana através de importações).

Atualmente, temos que fazer essa exclusão em lotes de cerca de 50.000 linhas (ou seja, excluir 50000, confirmar, excluir 50000, confirmar, repetir). Tentar excluir o lote inteiro de uma só vez deixa o banco de dados sem resposta por cerca de uma hora (dependendo do número de linhas). A exclusão das linhas em lotes como esse é muito difícil para o sistema e normalmente precisamos fazê-lo "conforme o tempo permitir" ao longo de uma semana; permitir que o script seja executado continuamente pode resultar em uma degradação do desempenho que é inaceitável para o usuário.

Acredito que esse tipo de exclusão de lote também prejudica o desempenho do índice e tem outros impactos que acabam causando a degradação do desempenho do banco de dados. Existem 34 índices em apenas uma tabela e o tamanho dos dados do índice é realmente maior que os próprios dados.

Aqui está o script que um de nossos funcionários de TI usa para fazer essa limpeza:

BEGIN
LOOP

delete FROM tbl_raw 
  where dist_event_date < to_date('[date]','mm/dd/yyyy') and rownum < 50000;

  exit when SQL%rowcount < 49999;

  commit;

END LOOP;

commit;

END;

Esse banco de dados deve estar acima de 99,99999% e só temos uma janela de manutenção de 2 dias uma vez por ano.

Estou procurando um método melhor para remover esses registros, mas ainda não encontrei nenhum. Alguma sugestão?

Codificação de gorila
fonte
Observe também que há mais de 30 índices em jogo aqui
jcolebrand

Respostas:

17

A lógica com 'A' e 'B' pode estar "oculta" atrás de uma coluna virtual na qual você pode fazer o particionamento:

alter session set nls_date_format = 'yyyy-mm-dd';
drop   table tq84_partitioned_table;

create table tq84_partitioned_table (
  status varchar2(1)          not null check (status in ('A', 'B')),
  date_a          date        not null,
  date_b          date        not null,
  date_too_old    date as
                       (  case status
                                 when 'A' then add_months(date_a, -7*12)
                                 when 'B' then            date_b
                                 end
                        ) virtual,
  data            varchar2(100) 
)
partition   by range  (date_too_old) 
( 
  partition p_before_2000_10 values less than (date '2000-10-01'),
  partition p_before_2000_11 values less than (date '2000-11-01'),
  partition p_before_2000_12 values less than (date '2000-12-01'),
  --
  partition p_before_2001_01 values less than (date '2001-01-01'),
  partition p_before_2001_02 values less than (date '2001-02-01'),
  partition p_before_2001_03 values less than (date '2001-03-01'),
  partition p_before_2001_04 values less than (date '2001-04-01'),
  partition p_before_2001_05 values less than (date '2001-05-01'),
  partition p_before_2001_06 values less than (date '2001-06-01'),
  -- and so on and so forth..
  partition p_ values less than (maxvalue)
);

insert into tq84_partitioned_table (status, date_a, date_b, data) values 
('B', date '2008-04-14', date '2000-05-17', 
 'B and 2000-05-17 is older than 10 yrs, must be deleted');


insert into tq84_partitioned_table (status, date_a, date_b, data) values 
('B', date '1999-09-19', date '2004-02-12', 
 'B and 2004-02-12 is younger than 10 yrs, must be kept');


insert into tq84_partitioned_table (status, date_a, date_b, data) values 
('A', date '2000-06-16', date '2010-01-01', 
 'A and 2000-06-16 is older than 3 yrs, must be deleted');


insert into tq84_partitioned_table (status, date_a, date_b, data) values 
('A', date '2009-06-09', date '1999-08-28', 
 'A and 2009-06-09 is younger than 3 yrs, must be kept');

select * from tq84_partitioned_table order by date_too_old;

-- drop partitions older than 10 or 3 years, respectively:

alter table tq84_partitioned_table drop partition p_before_2000_10;
alter table tq84_partitioned_table drop partition p_before_2000_11;
alter table tq84_partitioned_table drop partition p2000_12;

select * from tq84_partitioned_table order by date_too_old;
René Nyffenegger
fonte
Talvez eu tenha simplificado demais a lógica por trás de como os registros a serem removidos são determinados, mas essa é uma ideia muito interessante. Uma coisa que deve ser considerada, no entanto, é o desempenho do dia a dia. Limpar é "nosso problema", o cliente não aceitará desempenho degradado apenas para resolver isso. Parece que, a partir de alguns dos comentários e da resposta de Gary, isso pode ser um problema de particionamento?
Codificação de gorila
Não tenho certeza se essa é a resposta que estamos procurando, mas essa é definitivamente uma abordagem muito interessante que iremos investigar.
Coding Gorilla
14

A solução clássica para isso é particionar suas tabelas, por exemplo, por mês ou por semana. Se você nunca as encontrou antes, uma tabela particionada é como várias tabelas estruturadas de forma idêntica, implícitas UNIONna seleção, e o Oracle armazenará automaticamente uma linha na partição apropriada ao inseri-la com base nos critérios de particionamento. Você menciona índices - bem, cada partição também recebe seus próprios índices particionados. É uma operação muito barata no Oracle descartar uma partição (é análoga a umaTRUNCATEem termos de carga, porque é isso que você realmente está fazendo - truncando ou descartando uma dessas sub-tabelas invisíveis). Será uma quantidade significativa de processamento para particionar "após o fato", mas não faz sentido chorar por leite derramado - as vantagens de fazer até agora superam os custos. Todo mês você dividiria a partição superior para criar uma nova partição para os dados do próximo mês (você pode automatizar facilmente isso com a DBMS_JOB).

E com as partições, você também pode explorar a consulta paralela e a eliminação da partição , o que deve deixar seus usuários muito felizes ...

Gaius
fonte
FWIW usamos essa técnica no meu site em um banco de dados de 30Tb + #
Gaius
O problema com o particionamento é que não há uma maneira clara de particionar os dados. Em uma das duas tabelas (não a mostrada abaixo), os critérios usados ​​para a limpeza são baseados em dois campos de data diferentes (e distintos) e um campo de status. Por exemplo, se o status é A, então, se DateAé mais de 3 anos, ele é purgado. Se o Status for Be DateBtiver mais de 10 anos, ele será removido. Se minha compreensão do particionamento estiver correta, o particionamento não seria útil em uma situação como essa (pelo menos no que diz respeito à remoção).
Coding Gorilla
Você pode particionar por status e subpartição por período. Mas se o status (ou data) mudar, ele efetivamente excluirá uma sub-partição e inserirá a outra. Em resumo, você pode obter sucesso nos seus processos diários para economizar tempo em sua limpeza.
Gary
6
Como alternativa, você pode criar uma coluna virtual que mostre DateA quando o status for A e DateB quando o status for B e, em seguida, particione na coluna virtual. A mesma migração de partição ocorreria, mas ajudaria sua limpeza. Parece que isso já foi postado como resposta.
Leigh Riffel
4

Um aspecto a considerar é quanto do desempenho da exclusão resulta dos índices e quanto da tabela bruta. Todo registro excluído da tabela requer a mesma exclusão da linha de todo índice btree. Se você possui mais de 30 índices btree, suspeito que a maior parte do seu tempo seja gasta em manutenção de índices.

Isso afeta a utilidade do particionamento. Digamos que você tenha um índice no nome. Um índice Btree padrão, tudo em um segmento, pode ter que fazer quatro saltos para ir do bloco raiz ao bloco folha e uma quinta leitura para obter a linha. Se esse índice for particionado em 50 segmentos e você não tiver a chave de partição como parte da consulta, cada um desses 50 segmentos precisará ser verificado. Cada segmento será menor, portanto, você pode precisar fazer apenas dois saltos, mas ainda assim poderá fazer 100 leituras, em vez das 5 anteriores.

Se eles são índices de bitmap, as equações são diferentes. Você provavelmente não está usando índices para identificar linhas individuais, mas sim conjuntos delas. Portanto, em vez de uma consulta usando 5 IOs para retornar um único registro, estava usando 10.000 IOs. Como tal, a sobrecarga extra em partições extras para o índice não importa.

Gary
fonte
2

a exclusão de 50 milhões de registros por mês em lotes de 50.000 é de apenas 1000 iterações. se você excluir 1 a cada 30 minutos, ele deverá atender aos seus requisitos. uma tarefa agendada para executar a consulta que você postou, mas remover o loop para que seja executada apenas uma vez, não deve causar uma degradação perceptível para os usuários. Fazemos o mesmo volume de registros em nossa fábrica que funciona praticamente 24 horas por dia, 7 dias por semana e atende às nossas necessidades. Na verdade, distribuímos um pouco mais de 10.000 registros a cada 10 minutos, que são executados em cerca de 1 ou 2 segundos em execução nos servidores Oracle unix.

Jason Jakob
fonte
E quanto ao massivo 'desfazer' e 'refazer' 'excluir' gerará? Ele também bloqueia IO ... A abordagem baseada em "exclusão" certamente deve ser um NÃO.
pahariayogi
1

Se o espaço em disco não for muito importante, você poderá criar uma cópia "de trabalho" da tabela, digamos my_table_new, usando CTAS (Criar tabela como seleção) com critérios que omitiriam a exclusão dos registros. Você pode fazer a instrução create em paralelo e com a dica anexa para torná-la rápida e criar todos os seus índices. Em seguida, quando terminar, (e testado), renomeie a tabela existente para my_table_olde renomeie a tabela "trabalho" para my_table. Quando estiver confortável com tudo drop my_table_old purgepara se livrar da mesa antiga. Se houver várias restrições de chave estrangeira, consulte o dbms_redefinition pacote PL / SQL . Clonará seus índices, restrições, etc. ao usar as opções apropriadas. Este é um resumo de uma sugestão de Tom Kyte, da AskTomfama. Após a primeira execução, você pode automatizar tudo, e a tabela de criação deve ser muito mais rápida, e isso pode ser feito enquanto o sistema estiver ativo, e o tempo de inatividade do aplicativo seria limitado a menos de um minuto para renomear as tabelas. O uso do CTAS será muito mais rápido do que fazer várias exclusões de lotes. Essa abordagem pode ser particularmente útil se você não tiver o particionamento licenciado.

Amostra de CTAS, mantendo linhas com dados dos últimos 365 dias e flag_inactive = 'N':

create /*+ append */ table my_table_new 
   tablespace data as
   select /*+ parallel */ * from my_table 
       where some_date >= sysdate -365 
       and flag_inactive = 'N';

-- test out my_table_new. then if all is well:

alter table my_table rename to my_table_old;
alter table my_table_new rename to my_table;
-- test some more
drop table my_table_old purge;
Mark Stewart
fonte
11
Isso pode ser considerado se (a) a limpeza for uma tarefa pontual. (b) se você menos linhas de reter e a maioria dos dados para remover ...
pahariayogi
0

ao descartar uma partição, você deixa os índices globais inutilizáveis, que precisam ser reconstruídos; a reconstrução dos índices globais seria um grande problema; como se você fizer isso on-line, será bastante lento; caso contrário, você precisará de tempo de inatividade. em ambos os casos, não pode atender ao requisito.

"Normalmente, acabamos limpando entre 10 e 50 milhões de linhas por mês"

Eu recomendaria o uso de exclusão em lote PL / SQL, várias horas está ok, eu acho.

iceburge5
fonte
11
Se você possui uma chave primária, a eliminação de uma partição não deve tornar inutilizáveis ​​os índices globais. Mas se o OP tiver muitos índices globais, haverá um alto custo para descartar partições. Em um caso ideal, quando alguém está particionando uma tabela, o particionamento é baseado na chave primária e não precisa de nenhum índice global. Que toda consulta possa tirar proveito da remoção de partição.
Gandolf989
@ Gandolf989 soltando uma partição wil sempre fazer um índice global inutilizável
miracle173