Pergunta

Sou relativamente novato quando se trata de bancos de dados.Estamos usando MySQL e atualmente estou tentando acelerar uma instrução SQL que parece demorar um pouco para ser executada.Procurei no SO uma pergunta semelhante, mas não encontrei nenhuma.

O objetivo é remover todas as linhas da tabela A que possuem um ID correspondente na tabela B.

Atualmente estou fazendo o seguinte:

DELETE FROM a WHERE EXISTS (SELECT b.id FROM b WHERE b.id = a.id);

Existem aproximadamente 100 mil linhas na tabela a e cerca de 22 mil linhas na tabela b.A coluna 'id' é o PK de ambas as tabelas.

Esta instrução leva cerca de 3 minutos para ser executada na minha caixa de teste - Pentium D, XP SP3, 2 GB de RAM, MySQL 5.0.67.Isso parece lento para mim.Talvez não seja, mas eu esperava acelerar as coisas.Existe uma maneira melhor/mais rápida de fazer isso?


EDITAR:

Algumas informações adicionais que podem ser úteis.As tabelas A e B têm a mesma estrutura que fiz a seguir para criar a tabela B:

CREATE TABLE b LIKE a;

A tabela a (e, portanto, a tabela b) possui alguns índices para ajudar a acelerar as consultas feitas nela.Novamente, sou relativamente novato no trabalho de banco de dados e ainda estou aprendendo.Não sei quanto efeito, se houver, isso tem nas coisas.Presumo que isso tenha algum efeito, pois os índices também precisam ser limpos, certo?Eu também queria saber se havia alguma outra configuração de banco de dados que pudesse afetar a velocidade.

Além disso, estou usando o INNO DB.


Aqui estão algumas informações adicionais que podem ser úteis para você.

A Tabela A tem uma estrutura semelhante a esta (eu higienizei um pouco):

DROP TABLE IF EXISTS `frobozz`.`a`;
CREATE TABLE  `frobozz`.`a` (
  `id` bigint(20) unsigned NOT NULL auto_increment,
  `fk_g` varchar(30) NOT NULL,
  `h` int(10) unsigned default NULL,
  `i` longtext,
  `j` bigint(20) NOT NULL,
  `k` bigint(20) default NULL,
  `l` varchar(45) NOT NULL,
  `m` int(10) unsigned default NULL,
  `n` varchar(20) default NULL,
  `o` bigint(20) NOT NULL,
  `p` tinyint(1) NOT NULL,
  PRIMARY KEY  USING BTREE (`id`),
  KEY `idx_l` (`l`),
  KEY `idx_h` USING BTREE (`h`),
  KEY `idx_m` USING BTREE (`m`),
  KEY `idx_fk_g` USING BTREE (`fk_g`),
  KEY `fk_g_frobozz` (`id`,`fk_g`),
  CONSTRAINT `fk_g_frobozz` FOREIGN KEY (`fk_g`) REFERENCES `frotz` (`g`)
) ENGINE=InnoDB AUTO_INCREMENT=179369 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

Suspeito que parte do problema é que existem vários índices para esta tabela.A Tabela B é semelhante à tabela B, embora contenha apenas as colunas id e h.

Além disso, os resultados do perfil são os seguintes:

starting 0.000018
checking query cache for query 0.000044
checking permissions 0.000005
Opening tables 0.000009
init 0.000019
optimizing 0.000004
executing 0.000043
end 0.000005
end 0.000002
query end 0.000003
freeing items 0.000007
logging slow query 0.000002
cleaning up 0.000002

RESOLVIDO

Obrigado a todas as respostas e comentários.Eles certamente me fizeram pensar sobre o problema.Parabéns a dotjoe por me afastar do problema fazendo a simples pergunta "Alguma outra tabela faz referência a a.id?"

O problema era que havia um DELETE TRIGGER na tabela A que chamava um procedimento armazenado para atualizar duas outras tabelas, C e D.A tabela C tinha um FK de volta para a.id e depois de fazer algumas coisas relacionadas a esse id no procedimento armazenado, tinha a instrução,

DELETE FROM c WHERE c.id = theId;

Eu olhei para a instrução EXPLAIN e reescrevi isso como,

EXPLAIN SELECT * FROM c WHERE c.other_id = 12345;

Então, eu pude ver o que isso estava fazendo e me deu as seguintes informações:

id            1
select_type   SIMPLE
table         c
type          ALL
possible_keys NULL
key           NULL
key_len       NULL
ref           NULL
rows          2633
Extra         using where

Isso me disse que era uma operação dolorosa de fazer e como seria chamado 22.500 vezes (para um determinado conjunto de dados sendo excluído), esse era o problema.Depois de criar um INDEX naquela coluna other_id e executar novamente o EXPLAIN, obtive:

id            1
select_type   SIMPLE
table         c
type          ref
possible_keys Index_1
key           Index_1
key_len       8
ref           const
rows          1
Extra         

Muito melhor, na verdade muito bom.

Adicionei que Index_1 e meus tempos de exclusão estão alinhados com os tempos relatados por mattkemp.Este foi um erro muito sutil da minha parte devido a algumas funcionalidades adicionais no último minuto.Descobriu-se que a maioria das instruções alternativas DELETE/SELECT sugeridas, como Danilo afirmou, acabou levando essencialmente a mesma quantidade de tempo e como fusão de alma mencionei, a declaração era praticamente o melhor que eu seria capaz de construir com base no que eu precisava fazer.Depois de fornecer um índice para essa outra tabela C, meus DELETEs foram rápidos.

Pós-morte:
Duas lições aprendidas resultaram deste exercício.Primeiro, está claro que não aproveitei o poder da instrução EXPLAIN para ter uma ideia melhor do impacto das minhas consultas SQL.Esse é um erro de novato, então não vou me culpar por isso.Vou aprender com esse erro.Em segundo lugar, o código ofensivo foi o resultado de uma mentalidade de “fazer tudo rápido” e o design/teste inadequados fizeram com que esse problema não aparecesse mais cedo.Se eu tivesse gerado vários conjuntos de dados de teste consideráveis ​​para usar como entrada de teste para essa nova funcionalidade, não teria perdido meu tempo nem o seu.Meus testes no lado do banco de dados não tinham a profundidade que meu lado do aplicativo possui.Agora tenho a oportunidade de melhorar isso.

Referência:Declaração EXPLAIN

Foi útil?

Solução

Excluir dados do InnoDB é a operação mais cara que você pode solicitar.Como você já descobriu, a consulta em si não é o problema - a maioria delas será otimizada para o mesmo plano de execução de qualquer maneira.

Embora possa ser difícil entender por que os DELETEs de todos os casos são os mais lentos, há uma explicação bastante simples.InnoDB é um mecanismo de armazenamento transacional.Isso significa que se sua consulta fosse abortada no meio, todos os registros ainda estariam em vigor como se nada tivesse acontecido.Quando estiver concluído, tudo desaparecerá no mesmo instante.Durante o DELETE, outros clientes que se conectam ao servidor verão os registros até que o DELETE seja concluído.

Para conseguir isso, o InnoDB usa uma técnica chamada MVCC (Multi Version Concurrency Control).O que basicamente faz é fornecer a cada conexão uma visão instantânea de todo o banco de dados, como era quando a primeira instrução da transação foi iniciada.Para conseguir isso, cada registro no InnoDB internamente pode ter vários valores – um para cada instantâneo.É também por isso que COUNTing no InnoDB leva algum tempo - depende do estado do snapshot que você vê naquele momento.

Para sua transação DELETE, todo e qualquer registro identificado de acordo com as condições de sua consulta é marcado para exclusão.Como outros clientes podem estar acessando os dados ao mesmo tempo, não é possível removê-los imediatamente da tabela, pois eles precisam ver o respectivo snapshot para garantir a atomicidade da exclusão.

Depois que todos os registros forem marcados para exclusão, a transação será confirmada com sucesso.E mesmo assim eles não podem ser removidos imediatamente das páginas de dados reais, antes que todas as outras transações que funcionavam com um valor de instantâneo antes de sua transação DELETE também tenham terminado.

Então, na verdade, seus 3 minutos não são tão lentos, considerando o fato de que todos os registros precisam ser modificados para prepará-los para remoção de maneira segura para a transação.Provavelmente você "ouvirá" seu disco rígido funcionando enquanto a instrução é executada.Isso é causado pelo acesso a todas as linhas.Para melhorar o desempenho, você pode tentar aumentar o tamanho do buffer pool do InnoDB para o seu servidor e tentar limitar outros acessos ao banco de dados enquanto você DELETE, reduzindo também o número de versões históricas que o InnoDB precisa manter por registro.Com a memória adicional, o InnoDB poderá ler sua tabela (principalmente) na memória e evitar algum tempo de busca no disco.

Outras dicas

Seu tempo de três minutos parece muito lento.Meu palpite é que a coluna id não está sendo indexada corretamente.Se você pudesse fornecer a definição exata da tabela que está usando, isso seria útil.

Criei um script python simples para produzir dados de teste e executei várias versões diferentes da consulta de exclusão no mesmo conjunto de dados.Aqui estão minhas definições de tabela:

drop table if exists a;
create table a
 (id bigint unsigned  not null primary key,
  data varchar(255) not null) engine=InnoDB;

drop table if exists b;
create table b like a;

Em seguida, inseri 100 mil linhas em a e 25 mil linhas em b (22,5 mil das quais também estavam em a).Aqui estão os resultados dos vários comandos de exclusão.A propósito, deixei cair e repovoei a mesa entre as execuções.

mysql> DELETE FROM a WHERE EXISTS (SELECT b.id FROM b WHERE a.id=b.id);
Query OK, 22500 rows affected (1.14 sec)

mysql> DELETE FROM a USING a LEFT JOIN b ON a.id=b.id WHERE b.id IS NOT NULL;
Query OK, 22500 rows affected (0.81 sec)

mysql> DELETE a FROM a INNER JOIN b on a.id=b.id;
Query OK, 22500 rows affected (0.97 sec)

mysql> DELETE QUICK a.* FROM a,b WHERE a.id=b.id;
Query OK, 22500 rows affected (0.81 sec)

Todos os testes foram executados em um processador Intel Core2 quad-core de 2,5 GHz, 2 GB de RAM com Ubuntu 8.10 e MySQL 5.0.Observe que a execução de uma instrução SQL ainda é de thread único.


Atualizar:

Atualizei meus testes para usar o esquema do itsmatt.Eu o modifiquei levemente removendo o incremento automático (estou gerando dados sintéticos) e a codificação do conjunto de caracteres (não estava funcionando - não me aprofundei nisso).

Aqui estão minhas novas definições de tabela:

drop table if exists a;
drop table if exists b;
drop table if exists c;

create table c (id varchar(30) not null primary key) engine=InnoDB;

create table a (
  id bigint(20) unsigned not null primary key,
  c_id varchar(30) not null,
  h int(10) unsigned default null,
  i longtext,
  j bigint(20) not null,
  k bigint(20) default null,
  l varchar(45) not null,
  m int(10) unsigned default null,
  n varchar(20) default null,
  o bigint(20) not null,
  p tinyint(1) not null,
  key l_idx (l),
  key h_idx (h),
  key m_idx (m),
  key c_id_idx (id, c_id),
  key c_id_fk (c_id),
  constraint c_id_fk foreign key (c_id) references c(id)
) engine=InnoDB row_format=dynamic;

create table b like a;

Em seguida, executei novamente os mesmos testes com 100 mil linhas em a e 25 mil linhas em b (e repreenchendo entre as execuções).

mysql> DELETE FROM a WHERE EXISTS (SELECT b.id FROM b WHERE a.id=b.id);
Query OK, 22500 rows affected (11.90 sec)

mysql> DELETE FROM a USING a LEFT JOIN b ON a.id=b.id WHERE b.id IS NOT NULL;
Query OK, 22500 rows affected (11.48 sec)

mysql> DELETE a FROM a INNER JOIN b on a.id=b.id;
Query OK, 22500 rows affected (12.21 sec)

mysql> DELETE QUICK a.* FROM a,b WHERE a.id=b.id;
Query OK, 22500 rows affected (12.33 sec)

Como você pode ver, isso é um pouco mais lento do que antes, provavelmente devido aos vários índices.No entanto, não está nem perto da marca dos três minutos.

Outra coisa que você pode querer observar é mover o campo de texto longo para o final do esquema.Parece que me lembro que o MySQL tem melhor desempenho se todos os campos restritos de tamanho estiverem primeiro e texto, blob, etc. estiverem no final.

Experimente isto:

DELETE a
FROM a
INNER JOIN b
 on a.id = b.id

O uso de subconsultas tende a ser mais lento do que as junções à medida que são executadas para cada registro na consulta externa.

Isso é o que sempre faço quando tenho que operar com dados supergrandes (aqui:uma tabela de teste de amostra com 150.000 linhas):

drop table if exists employees_bak;
create table employees_bak like employees;
insert into employees_bak 
    select * from employees
    where emp_no > 100000;

rename table employees to employees_todelete;
rename table employees_bak to employees;

Neste caso, o sql filtra 50.000 linhas na tabela de backup.A cascata de consultas é executada em minha máquina lenta em 5 segundos.Você pode substituir a inserção em select por sua própria consulta de filtro.

Esse é o truque para realizar exclusão em massa em grandes bancos de dados!;=)

Você está fazendo sua subconsulta em 'b' para cada linha em 'a'.

Tentar:

DELETE FROM a USING a LEFT JOIN b ON a.id = b.id WHERE b.id IS NOT NULL;

Experimente isto:

DELETE QUICK A.* FROM A,B WHERE A.ID=B.ID

É muito mais rápido que as consultas normais.

Consulte a sintaxe: http://dev.mysql.com/doc/refman/5.0/en/delete.html

Sei que esta questão foi praticamente resolvida devido às omissões de indexação do OP, mas gostaria de oferecer este conselho adicional, que é válido para um caso mais genérico deste problema.

Eu pessoalmente lidei com a necessidade de excluir muitas linhas de uma tabela que existem em outra e, em minha experiência, é melhor fazer o seguinte, especialmente se você espera que muitas linhas sejam excluídas.O mais importante é que essa técnica melhorará o atraso do escravo da replicação, pois quanto mais tempo cada consulta do modificador for executada, pior será o atraso (a replicação é de thread único).

Então aqui está: faça um SELECT primeiro, como uma consulta separada, lembrando-se dos IDs retornados em seu script/aplicativo e, em seguida, continue excluindo em lotes (digamos, 50.000 linhas por vez).Isso alcançará o seguinte:

  • cada uma das instruções delete não bloqueará a tabela por muito tempo, evitando assim que o atraso na replicação fique fora de controle.É especialmente importante se você confiar na sua replicação para fornecer dados relativamente atualizados.A vantagem de usar lotes é que se você achar que cada consulta DELETE ainda demora muito, você pode ajustá-la para ser menor sem tocar em nenhuma estrutura de banco de dados.
  • outro benefício de usar um SELECT separado é que o próprio SELECT pode demorar muito para ser executado, especialmente se, por algum motivo, não for possível usar os melhores índices de banco de dados.Se o SELECT for interno a um DELETE, quando toda a instrução migrar para os escravos, ele terá que fazer o SELECT novamente, potencialmente atrasando os escravos porque terá que fazer a seleção longa novamente.O atraso do escravo, novamente, sofre muito.Se você usar uma consulta SELECT separada, esse problema desaparecerá, pois tudo o que você está passando é uma lista de IDs.

Deixe-me saber se há alguma falha na minha lógica em algum lugar.

Para obter mais discussões sobre o atraso de replicação e maneiras de combatê-lo, semelhante a esta, consulte MySQL Slave Lag (Delay) explicado e 7 maneiras de combatê-lo

P.S.Uma coisa a ter cuidado é, obviamente, possíveis edições na tabela entre os horários em que SELECT termina e DELETEs iniciam.Deixarei você lidar com esses detalhes usando transações e/ou lógica pertinentes ao seu aplicativo.

DELETE FROM a WHERE id IN (SELECT id FROM b)

Talvez você deva reconstruir os índices antes de executar uma consulta tão grande.Bem, você deve reconstruí-los periodicamente.

REPAIR TABLE a QUICK;
REPAIR TABLE b QUICK;

e, em seguida, execute qualquer uma das consultas acima (ou seja)

DELETE FROM a WHERE id IN (SELECT id FROM b)

A consulta em si já está na forma ideal, atualizar os índices faz com que toda a operação demore tanto.Você poderia desabilitar as chaves naquela mesa antes da operação, isso deve acelerar as coisas.Você pode ativá-los novamente mais tarde, se não precisar deles imediatamente.

Outra abordagem seria adicionar um deleted flag-column à sua tabela e ajustando outras consultas para que levem esse valor em consideração.O tipo booleano mais rápido no mysql é CHAR(0) NULL (verdadeiro = '', falso = NULO).Seria uma operação rápida, você pode excluir os valores posteriormente.

Os mesmos pensamentos expressos em instruções SQL:

ALTER TABLE a ADD COLUMN deleted CHAR(0) NULL DEFAULT NULL;

-- The following query should be faster than the delete statement:
UPDATE a INNER JOIN b SET a.deleted = '';

-- This is the catch, you need to alter the rest
-- of your queries to take the new column into account:
SELECT * FROM a WHERE deleted IS NULL;

-- You can then issue the following queries in a cronjob
-- to clean up the tables:
DELETE FROM a WHERE deleted IS NOT NULL;

Se isso também não é o que você deseja, você pode dar uma olhada no que os documentos do mysql têm a dizer sobre o velocidade de instruções de exclusão.

Aliás, depois de postar o que foi dito acima no meu blog, Barão Schwartz de Percona chamou minha atenção para o fato de que seu maatkit já possui uma ferramenta só para esse fim - mk-archiver. http://www.maatkit.org/doc/mk-archiver.html.

É provavelmente a sua melhor ferramenta para o trabalho.

Obviamente o SELECT consulta que constrói a base do seu DELETE a operação é bastante rápida, então acho que a restrição de chave estrangeira ou os índices são os motivos de sua consulta extremamente lenta.

Tentar

SET foreign_key_checks = 0;
/* ... your query ... */
SET foreign_key_checks = 1;

Isso desativaria as verificações da chave estrangeira.Infelizmente você não pode desabilitar (pelo menos não sei como) as atualizações de chave com uma tabela InnoDB.Com uma tabela MyISAM você poderia fazer algo como

ALTER TABLE a DISABLE KEYS
/* ... your query ... */
ALTER TABLE a ENABLE KEYS 

Na verdade, não testei se essas configurações afetariam a duração da consulta.Mas vale a pena tentar.

Conecte a base de dados usando o terminal e execute o comando abaixo, veja o tempo de resultado de cada um deles, você verá que os tempos de exclusão de 10, 100, 1000, 10000, 100000 registros não são multiplicados.

  DELETE FROM #{$table_name} WHERE id < 10;
  DELETE FROM #{$table_name} WHERE id < 100;
  DELETE FROM #{$table_name} WHERE id < 1000;
  DELETE FROM #{$table_name} WHERE id < 10000;
  DELETE FROM #{$table_name} WHERE id < 100000;

O tempo de exclusão de 10 mil registros não é 10 vezes maior que o tempo de exclusão de 100 mil registros.Então, exceto para encontrar uma maneira de excluir registros mais rapidamente, existem alguns métodos indiretos.

1, podemos renomear table_name para table_name_bak e, em seguida, selecionar registros de table_name_bak para table_name.

2, Para excluir 10.000 registros, podemos excluir 1.000 registros 10 vezes.Existe um exemplo de script Ruby para fazer isso.

#!/usr/bin/env ruby
require 'mysql2'


$client = Mysql2::Client.new(
  :as => :array,
  :host => '10.0.0.250',
  :username => 'mysql',
  :password => '123456',
  :database => 'test'
)


$ids = (1..1000000).to_a
$table_name = "test"

until $ids.empty?
  ids = $ids.shift(1000).join(", ")
  puts "delete =================="
  $client.query("
                DELETE FROM #{$table_name}
                WHERE id IN ( #{ids} )
                ")
end

A técnica básica para excluir múltiplas linhas do MySQL em uma única tabela por meio do campo id

DELETE FROM tbl_name WHERE id <= 100 AND id >=200; Esta consulta é responsável por excluir a condição correspondente entre 100 E 200 de determinada tabela

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top