Pregunta

Soy relativamente novato en lo que respecta a bases de datos.Estamos usando MySQL y actualmente estoy intentando acelerar una declaración SQL que parece tardar un poco en ejecutarse.Busqué en SO una pregunta similar pero no encontré ninguna.

El objetivo es eliminar todas las filas de la tabla A que tienen una identificación 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 100.000 filas en la tabla a y aproximadamente 22.000 filas en la tabla b.La columna 'id' es la PK de ambas tablas.

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.Quizás no lo sea, pero esperaba acelerar las cosas.¿Existe 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 hice a continuación para crear la tabla B:

CREATE TABLE b LIKE a;

La tabla a (y por tanto la tabla b) tiene algunos índices para ayudar a acelerar las consultas que se realizan en ella.Nuevamente, soy relativamente novato en el trabajo de DB y todavía estoy aprendiendo.No sé qué efecto tiene esto, si es que tiene alguno, en las cosas.Supongo que tiene algún efecto ya que los índices también deben limpiarse, ¿verdad?También me preguntaba si había otras configuraciones de base de datos que pudieran afectar la velocidad.

Además, estoy usando INNO DB.


Aquí hay información adicional que podría resultarle útil.

La tabla A tiene una estructura similar a esta (la he desinfectado 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 varios í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 del perfilado 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

SOLUCIONADO

Gracias a todas las respuestas y comentarios.Ciertamente me hicieron pensar en el problema.Felicitaciones a dotjoe por ayudarme a alejarme del problema haciendo la simple pregunta "¿Alguna otra tabla hace referencia a a.id?"

El problema era que había un DELETE TRIGGER 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 regreso a a.id y después de hacer algunas cosas relacionadas con esa identificación en el procedimiento almacenado, tenía la declaración,

DELETE FROM c WHERE c.id = theId;

Miré la declaración EXPLAIN y la reescribí como,

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

Entonces, pude ver lo que estaba haciendo esto 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 realizar y, dado que iban a recibir llamadas 22500 veces (para el conjunto de datos que se eliminaba), ese era el problema.Una vez que creé un ÍNDICE en esa columna other_id y volví a ejecutar EXPLAIN, 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 eliminación están en línea con los tiempos informados por mattkemp.Este fue un error realmente sutil de mi parte debido a que calcé algunas funciones adicionales en el último minuto.Resultó que la mayoría de las declaraciones DELETE/SELECT alternativas sugeridas, como Daniel dicho, terminó tomando esencialmente la misma cantidad de tiempo y como fusionar el alma Como mencioné, la declaración era prácticamente lo mejor que iba a poder construir en función de lo que necesitaba hacer.Una vez que proporcioné un índice para esta otra tabla C, mis BORRADOS fueron rápidos.

Post mortem:
De este ejercicio surgieron dos lecciones aprendidas.Primero, está claro que no aproveché el poder de la declaración EXPLAIN para tener una mejor idea del impacto de mis consultas SQL.Ese es un error de novato, así que no voy a castigarme por eso.Aprenderé de ese error.En segundo lugar, el código infractor fue el resultado de una mentalidad de "hacerlo rápido" y un diseño/pruebas inadecuados provocaron que este problema no apareciera antes.Si hubiera generado varios conjuntos de datos de prueba considerables para usarlos como entrada de prueba para esta nueva funcionalidad, no habría perdido mi tiempo ni el suyo.A mis pruebas en el lado de la base de datos les faltaba la profundidad que tiene mi lado de la aplicación.Ahora tengo la oportunidad de mejorar eso.

Referencia:EXPLICAR Declaración

¿Fue útil?

Solución

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

Si bien puede ser difícil entender por qué los DELETE de todos los casos son los más lentos, hay una explicación bastante simple. InnoDB es un motor de almacenamiento transaccional. Eso significa que si su consulta fue cancelada a la mitad, todos los registros seguirían en su lugar como si nada hubiera pasado. Una vez que esté completo, todo desaparecerá 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 (Control de concurrencia de versiones múltiples). Lo que básicamente hace es dar a cada conexión una vista instantánea de toda la base de datos tal como estaba cuando comenzó la primera declaració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 cual COUNTing en InnoDB lleva algo de tiempo: depende del estado de la instantánea que vea en ese momento.

Para su transacción DELETE, todos y cada uno de los registros identificados de acuerdo con las condiciones de su 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, ya que tienen que ver su instantánea correspondiente para garantizar la atomicidad de la eliminación.

Una vez que todos los registros se han marcado para su eliminación, la transacción se confirma correctamente. E incluso entonces no pueden eliminarse 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 DELETE, también hayan finalizado.

Entonces, de hecho, sus 3 minutos no son realmente tan lentos, teniendo en cuenta el hecho de que todos los registros deben modificarse para prepararlos para la eliminación de una manera segura de la transacción. Probablemente & "; Escuche &"; su disco duro funciona mientras se ejecuta la instrucción. Esto se debe al acceso a todas las filas. Para mejorar el rendimiento, puede intentar aumentar el tamaño de la agrupación de almacenamiento intermedio de InnoDB para su servidor e intentar limitar otro acceso a la base de datos mientras BORRAR, reduciendo así el número de versiones históricas que InnoDB debe mantener por registro. Con la memoria adicional, InnoDB podría leer su tabla (principalmente) en la memoria y evitar algún tiempo de búsqueda de disco.

Otros consejos

Su tiempo de tres minutos parece realmente lento. Supongo que la columna de identificación no se indexa correctamente. Si pudiera proporcionar la definición exacta de la tabla que está utilizando, sería útil.

Creé un script de Python simple para producir datos de prueba y ejecuté varias versiones diferentes de la consulta de eliminación en 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 ay 25k filas en b (22.5k de las cuales también estaban en a). Aquí están los resultados de los diversos comandos de eliminación. Por cierto, dejé caer y repoblé la tabla entre ejecuciones.

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 procesador Intel Core2 quad-core 2.5GHz, 2GB RAM con Ubuntu 8.10 y MySQL 5.0. Tenga en cuenta que la ejecución de una instrucción sql sigue siendo de un solo subproceso.


Actualización:

Actualicé mis pruebas para usar el esquema de itsmatt. Lo modifiqué un poco eliminando el incremento automático (estoy generando datos sintéticos) y la codificación del juego de caracteres (no funcionaba, no profundicé en ello).

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 vuelvo a clasificar las mismas pruebas con 100k filas en ay 25k filas en b (y repoblando entre ejecuciones).

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á cerca de los tres minutos.

Algo más que es posible que desee ver es mover el campo de texto largo al final del esquema. Me parece recordar que mySQL funciona mejor si todos los campos de tamaño restringido son primero y el texto, blob, etc. están al final.

Prueba esto:

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

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

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 respaldo. La cascada de consultas se realiza en mi máquina lenta en 5 segundos. Puede reemplazar la inserción en select por su propia consulta de filtro.

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

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

Prueba:

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

Pruebe esto:

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

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

Consulte la sintaxis: http://dev.mysql.com /doc/refman/5.0/en/delete.html

Sé que esta pregunta se ha resuelto bastante 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.

He tratado personalmente de 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 se ejecute cada consulta de mutadores, peor será el retraso (la replicación es de un solo subproceso).

Entonces, aquí está: primero haga una SELECCIÓN, como una consulta separada , recordando las ID devueltas en su script / aplicación, luego continúe eliminando en lotes (por ejemplo, 50,000 filas a la vez ) Esto logrará lo siguiente:

  • cada una de las declaraciones de eliminación no bloqueará la tabla durante demasiado tiempo, por lo que no permitirá que el retraso de 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 DELETE todavía toma demasiado tiempo, puede ajustarla para que sea más pequeña sin tocar ninguna estructura de base de datos.
  • otro beneficio de usar un SELECT separado es que el SELECT en sí mismo puede tardar mucho tiempo en ejecutarse , especialmente si por alguna razón no puede usar los mejores índices DB. Si SELECT es interno a DELETE, cuando toda la instrucción migra a los esclavos, tendrá que hacer SELECT nuevamente, lo que podría retrasar a los esclavos porque tiene que hacer la selección larga nuevamente. El retraso esclavo, de nuevo, sufre mucho. Si usa una consulta SELECT 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 en alguna parte.

Para obtener más información sobre el retraso de la replicación y las formas de combatirlo, similar a este, consulte MySQL Slave Lag (Delay) explicado y 7 formas de combatirlo

P.S. Una cosa con la que hay que tener cuidado es, por supuesto, las ediciones potenciales en la tabla entre los tiempos en que SELECT termina y DELETE comienza. Le permitiré manejar dichos detalles mediante el uso de transacciones y / o lógica pertinente a su aplicación.

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

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

REPAIR TABLE a QUICK;
REPAIR TABLE b QUICK;

y luego ejecute cualquiera de las consultas anteriores (es decir)

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

La consulta en sí ya está en una forma óptima, la actualización de los índices hace que toda la operación demore ese tiempo.Tú podrías desactivar las teclas en esa mesa antes de la operación, eso debería acelerar las cosas.Puede volver a activarlos más adelante, si no los necesita de inmediato.

Otro enfoque sería agregar un deleted flag-column a su tabla y ajustando otras consultas para que tengan en cuenta ese valor.El tipo booleano más rápido en MySQL es CHAR(0) NULL (verdadero = '', falso = NULO).Esa sería una operación rápida, puedes eliminar los valores después.

Los mismos pensamientos expresados ​​en declaraciones 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 tampoco es lo que desea, puede echar un vistazo a lo que los documentos de MySQL tienen que decir sobre velocidad de eliminación de declaraciones.

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

Es muy probable que sea su mejor herramienta para el trabajo.

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

Probar

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

Esto deshabilitaría las verificaciones en la clave foránea. Desafortunadamente, no puede deshabilitar (al menos no sé cómo) las actualizaciones de clave con una tabla InnoDB. Con una tabla MyISAM podrías hacer algo como

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

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

Conecte la base de datos usando la terminal y ejecute el comando a continuación, observe el tiempo de resultado de cada uno de ellos, encontrará que los tiempos de eliminación de 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 para eliminar 10 mil registros no es 10 veces mayor que el de eliminar 100 mil registros.Luego, además de encontrar una manera de eliminar registros más rápido, existen algunos métodos indirectos.

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

2. Para eliminar 10000 registros, podemos eliminar 1000 registros 10 veces.Hay un script Ruby de ejemplo 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

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

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top