¿Una forma más rápida de eliminar filas coincidentes?


Soy un novato relativo cuando se trata de bases de datos. Estamos usando MySQL y actualmente estoy tratando de acelerar una sentencia SQL que parece tardar un tiempo en ejecutarse. Miré a mi alrededor para una pregunta similar, pero no encontré una.

El objetivo es eliminar todas las filas de la tabla A que tengan un id coincidente en la tabla B.

Actualmente estoy haciendo lo siguiente:

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

Hay aproximadamente 100K filas en la tabla a y alrededor de 22K filas en la tabla b. La columna 'id' es la PK para ambas mesas.

Esta declaración tarda unos 3 minutos en ejecutarse en mi caja de prueba: Pentium D, XP SP3, 2 GB de ram, MySQL 5.0.67. Esto me parece lento. Tal vez no, pero esperaba acelerar las cosas. ¿Hay una manera mejor/más rápida de lograr esto?


EDITAR:

Alguna información adicional que podría ser útil. Las tablas A y B tienen la misma estructura que he hecho lo siguiente para crear la tabla B:

CREATE TABLE b LIKE a;

La tabla a (y por lo tanto la tabla b) tiene algunos índices para ayudar a acelerar las consultas que se realizan contra él. De nuevo, soy un novato relativo en el trabajo de DB y todavía estoy aprendiendo. No se cuanto efecto tiene esto en las cosas. Supongo que tiene un efecto ya que los índices tienen que ser limpiados también, ¿verdad? También me preguntaba si había alguna otra configuración de DB que pudiera afectar la velocidad.

Además, estoy usando INNO DB.


Aquí hay información adicional que podría ser útil para usted.

Cuadro A tiene una estructura similar a esta (he desinfectado esto un poco):

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;

Sospecho que parte del problema es que hay una serie de índices para esta tabla. La tabla B es similar a la tabla B, aunque solo contiene las columnas id y h.

Además, los resultados de los perfiles son los siguientes:

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

RESUELTO

Gracias a todas las respuestas y comentarios. Me hicieron pensar en el problema. Felicitaciones a dotjoe por conseguir que me aleje del problema haciendo la pregunta simple "Hacer cualquier otra referencia de tablas a.id?"

El problema era que había un DESENCADENADOR de ELIMINACIÓN en la tabla A que llamaba a un procedimiento almacenado para actualizar otras dos tablas, C y D. La tabla C tenía un FK de vuelta a a.id y después de hacer algunas cosas relacionadas con ese id en el procedimiento almacenado, tenía la declaración,

DELETE FROM c WHERE c.id = theId;

Miré en la declaración de EXPLICAR y reescribí esto como,

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

Así que, pude ver lo que esto estaba haciendo y me dio la siguiente información:

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

Esto me dijo que era una operación dolorosa de hacer y ya que iba a ser llamado 22500 veces (para el conjunto dado de datos que se eliminan), ese era el problema. Una vez que creé un ÍNDICE en esa columna other_id y volví a analizar la EXPLICACIÓN, obtuve:

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

Mucho mejor, de hecho realmente genial.

Agregué que Index_1 y mis tiempos de borrado están en línea con los tiempos reportados por mattkemp. Este fue un error muy sutil de mi parte debido a la funcionalidad adicional en el último minuto. Resultó que la mayoría de las sentencias alternativas sugeridas DELETE/SELECT, como Daniel declaró, terminaron tomando esencialmente la misma cantidad de tiempo y como soulmerge mencionó, la sentencia era más o menos la mejor que iba a ser capaz de construir basado en lo que necesitaba hacer. Una vez que proporcioné un índice para esta otra tabla C, mis eliminaciones fueron rápida.

Postmortem :
De este ejercicio se extrajeron dos lecciones. Primero, está claro que no aproveché el poder de la instrucción EXPLAIN para tener una mejor idea del impacto de mis consultas SQL. Es un error de novato, así que no voy a castigarme por eso. Aprenderé de ese error. En segundo lugar, el código ofensivo fue el resultado de una mentalidad de "hazlo rápido" y un diseño/prueba inadecuado llevó a que este problema no apareciera antes. Tenía Yo generado varios conjuntos de datos de prueba de gran tamaño para usar como entrada de prueba para esta nueva funcionalidad, no habría perdido mi tiempo ni el suyo. Mis pruebas en el lado de la base de datos carecían de la profundidad que mi lado de la aplicación tiene en su lugar. Ahora tengo la oportunidad de mejorar eso.

Referencia: EXPLAIN Statement

Author: Sam, 2009-05-01

14 answers

Eliminar datos de InnoDB es la operación más costosa que puede solicitar. Como ya descubrió, la consulta en sí no es el problema, la mayoría de ellas se optimizarán para el mismo plan de ejecución de todos modos.

Si bien puede ser difícil entender por qué las eliminaciones de todos los casos son las más lentas, hay una explicación bastante simple. InnoDB es un motor de almacenamiento transaccional. Eso significa que si su consulta se abortó a mitad de camino, todos los registros seguirían en su lugar como si nada suceder. Una vez que se complete, todo se habrá ido en el mismo instante. Durante la ELIMINACIÓN, otros clientes que se conecten al servidor verán los registros hasta que se complete la ELIMINACIÓN.

Para lograr esto, InnoDB utiliza una técnica llamada MVCC (Multi Version Concurrency Control). Lo que básicamente hace es dar a cada conexión una vista instantánea de toda la base de datos tal y como estaba cuando se inició la primera instrucción de la transacción. Para lograr esto, cada registro en InnoDB internamente puede tener múltiples valores: uno para cada instantánea. Esta es también la razón por la que contar con InnoDB lleva algún tiempo: depende del estado de la instantánea que vea en ese momento.

Para su transacción de ELIMINACIÓN, todos y cada uno de los registros que se identifican de acuerdo con sus condiciones de consulta, se marcan para su eliminación. Como otros clientes pueden estar accediendo a los datos al mismo tiempo, no puede eliminarlos inmediatamente de la tabla, porque tienen que ver su respectiva instantánea para garantizar la atomicidad del eliminación.

Una vez que todos los registros han sido marcados para su eliminación, la transacción se confirma con éxito. E incluso entonces no se pueden eliminar inmediatamente de las páginas de datos reales, antes de que todas las demás transacciones que funcionaron con un valor de instantánea antes de su transacción de ELIMINACIÓN, también hayan terminado.

Así que, de hecho, sus 3 minutos no son realmente tan lentos, teniendo en cuenta el hecho de que todos los registros tienen que ser modificados con el fin de prepararlos para la eliminación de una manera segura de la transacción. Probablemente "escuchará" su disco duro trabajando mientras se ejecuta la instrucción. Esto es causado por el acceso a todas las filas. Para mejorar el rendimiento, puede intentar aumentar el tamaño del grupo de búfer de InnoDB para su servidor e intentar limitar otros accesos a la base de datos mientras ELIMINA, reduciendo así también el número de versiones históricas que InnoDB tiene que mantener por registro. Con la memoria adicional InnoDB podría ser capaz de leer su tabla (principalmente)en la memoria y evitar un tiempo de búsqueda de disco.

 71
Author: Daniel Schneller,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-06 16:51:57

Su tiempo de tres minutos parece muy lento. Mi conjetura es que la columna id no está siendo indexada correctamente. Si pudiera proporcionar la definición exacta de la tabla que está utilizando, sería útil.

Creé un script python simple para producir datos de prueba y ejecuté varias versiones diferentes de la consulta delete contra el mismo conjunto de datos. Aquí están mis definiciones de tabla:

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;

Luego inserté 100k filas en a y 25k filas en b (22.5 k de los cuales también estaban en a). Aquí están los resultados de los varios comandos delete. Me caí y repoblé la mesa entre carreras por cierto.

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)

Todas las pruebas se ejecutaron en un Intel Core2 quad-core 2.5 GHz, 2GB RAM con Ubuntu 8.10 y MySQL 5.0. Tenga en cuenta que la ejecución de una instrucción sql sigue siendo un subproceso único.


Actualización:

Actualizé mis pruebas para usar el esquema de itsmatt. Lo modifiqué ligeramente al eliminar el incremento automático (estoy generando datos sintéticos) y la codificación del conjunto de caracteres (no funcionaba - no cavé en él).

Aquí están mis nuevas definiciones de tabla:

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;

Luego reviso las mismas pruebas con 100k filas en a y 25k filas en b (y repoblando entre corridas).

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 puede ver, esto es bastante más lento que antes, probablemente debido a los múltiples índices. Sin embargo, no está ni cerca de la marca de tres minutos.

Otra cosa que puede que desee ver es mover el campo longtext al final del esquema. Me parece recordar que MySQL funciona mejor si todos los campos de tamaño restringido están primero y texto, blob, etc. están al final.

 9
Author: mattkemp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-07 01:46:39

Prueba esto:

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

El uso de subconsultas tiende a ser más lento que las uniones a medida que se ejecutan para cada registro en la consulta externa.

 7
Author: Chris Van Opstal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-01 18:42:26

Esto es lo que siempre hago, cuando tengo que operar con datos súper grandes (aquí: una tabla de prueba de muestra con 150000 filas):

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;

En este caso, el sql filtra 50000 filas en la tabla de copia de seguridad. La cascada de consultas se realiza en mi máquina lenta en 5 segundos. Puede reemplazar la inserción en seleccionar por su propia consulta de filtro.

Ese es el truco para realizar la eliminación masiva en grandes bases de datos!;=)

 4
Author: Tom Schaefer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-07 11:52:57

Estás haciendo tu subconsulta en 'b' para cada fila en 'a'.

Intenta:

DELETE FROM a USING a LEFT JOIN b ON a.id = b.id WHERE b.id IS NOT NULL;
 3
Author: Evert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-01 18:17:39

Prueba esto:

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

Es mucho más rápido que las consultas normales.

Refiérase a la sintaxis: http://dev.mysql.com/doc/refman/5.0/en/delete.html

 3
Author: Webrsk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-01 19:27:58

Sé que esta pregunta se ha resuelto debido a las omisiones de indexación de OP, pero me gustaría ofrecer este consejo adicional, que es válido para un caso más genérico de este problema.

Personalmente he tenido que eliminar muchas filas de una tabla que existen en otra y en mi experiencia es mejor hacer lo siguiente, especialmente si espera que se eliminen muchas filas. Esta técnica, lo más importante, mejorará el retraso del esclavo de replicación, ya que cuanto más tiempo dure cada la consulta del mutador se ejecuta, peor sería el retraso (la replicación es un subproceso único).

Así que, aquí está: haga una SELECCIÓN primero, como una consulta separada, recordando los ID devueltos en su script/aplicación, luego continúe eliminando en lotes (digamos, 50,000 filas a la vez). Esto logrará lo siguiente:

  • cada una de las instrucciones delete no bloqueará la tabla durante demasiado tiempo, por lo que no dejará que la replicación se salga de control. Es especialmente importante si confía en su replicación para proporcionarle datos relativamente actualizados. El beneficio de usar lotes es que si encuentra que cada consulta de ELIMINACIÓN aún toma demasiado tiempo, puede ajustarla para que sea más pequeña sin tocar ninguna estructura de BD.
  • otro beneficio de usar un SELECT separado es que el SELECT en sí puede tardar mucho tiempo en ejecutarse, especialmente si no puede usar los mejores índices de BD por cualquier razón. Si el SELECT es interno de un DELETE, cuando la instrucción completa migra a los esclavos, se tendrá que SELECCIONAR todo de nuevo, potencialmente, quedando los esclavos porque tiene que hacer el largo seleccionar todo de nuevo. Slave lag, de nuevo, sufre mucho. Si utiliza una consulta de SELECCIÓN separada, este problema desaparece, ya que todo lo que está pasando es una lista de ID.

Avísame si hay algún error en mi lógica.

Para más discusión sobre el retraso de replicación y formas de combatirlo, similar a este, ver MySQL Slave Lag (Delay) Explained Y 7 Maneras De Combatirlo

P.d. Una cosa a tener en cuenta es, por supuesto, las posibles ediciones de la tabla entre los tiempos en que la SELECCIÓN termina y elimina el inicio. Le dejaré manejar dichos detalles mediante el uso de transacciones y / o lógica pertinente a su aplicación.

 3
Author: Artem Russakovskii,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-10 17:28:45
DELETE FROM a WHERE id IN (SELECT id FROM b)
 2
Author: chaos,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-01 18:20:20

Tal vez debería reconstruir los indicies antes de ejecutar una consulta hugh. Bueno, deberías reconstruirlos periódicamente.

REPAIR TABLE a QUICK;
REPAIR TABLE b QUICK;

Y luego ejecutar cualquiera de las consultas anteriores (es decir,)

DELETE FROM a WHERE id IN (SELECT id FROM b)
 2
Author: Scoregraphic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-06 10:09:44

La consulta en sí ya está en una forma óptima, la actualización de los índices hace que toda la operación tome ese tiempo. Podrías desactivar las teclas en esa tabla antes de la operación, eso debería acelerar las cosas. Puede volver a encenderlos en un momento posterior, si no los necesita de inmediato.

Otro enfoque sería agregar una columna de bandera deleted a su tabla y ajustar otras consultas para que tengan en cuenta ese valor. El tipo booleano más rápido en mysql es CHAR(0) NULL (true = ", false = NULL). Eso sería una operación rápida, puede eliminar los valores después.

Los mismos pensamientos expresados en sentencias 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;

Si eso, también, no es lo que desea, puede echar un vistazo a lo que los documentos de mysql tienen que decir sobre la velocidad de las sentencias delete.

 2
Author: soulmerge,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-06 10:31:06

Por cierto, después de publicar lo anterior en mi blog, Baron Schwartzde Percona me llamó la atención que su maatkit ya tiene una herramienta solo para este propósito: mk - archiver. http://www.maatkit.org/doc/mk-archiver.html .

Es probablemente la mejor herramienta para el trabajo.

 2
Author: Artem Russakovskii,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-11 21:09:58

Obviamente la consulta SELECT que construye la base de su operación DELETE es bastante rápida, por lo que creo que la restricción de clave externa o los índices son las razones de su consulta extremadamente lenta.

Intenta

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

Esto deshabilitaría las comprobaciones de la clave foránea. Desafortunadamente no se puede desactivar (al menos no se como) las actualizaciones de claves con una tabla InnoDB. Con una mesa MyISAM podrías hacer algo como

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

En realidad no probé si estos la configuración afectaría la duración de la consulta. Pero vale la pena intentarlo.

 1
Author: Stefan Gehrig,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-06 16:19:19

Conecte la base de datos usando terminal y ejecute el comando a continuación, mire el tiempo de resultado de cada uno de ellos, encontrará que los tiempos de eliminar 10, 100, 1000, 10000, 100000 registros no se multiplican.

  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;

El tiempo de borrar 10 mil registros no es 10 veces más que borrar 100 mil registros. Entonces, excepto para encontrar una manera de eliminar registros más rápido, hay algunos métodos indirectos.

1, Podemos cambiar el nombre de la table_name a table_name_bak, y luego seleccionar registros desde table_name_bak a table_name.

2, Para eliminar 10000 registros, podemos eliminar 1000 registros 10 veces. Hay un ejemplo de script ruby para hacerlo.

#!/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
 0
Author: yanyingwang,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-03-03 08:37:21

La técnica básica para eliminar múltiples filas de MySQL en una sola tabla a través del campo id

DELETE FROM tbl_name WHERE id <= 100 AND id >=200; Esta consulta es responsable de eliminar la condición coincidente entre 100 Y 200 de la tabla determinada

 -2
Author: Sarkar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-12 06:21:09