web-dev-qa-db-fra.com

Un moyen plus rapide de supprimer les lignes correspondantes?

Je suis relativement novice en matière de bases de données. Nous utilisons MySQL et j'essaie actuellement d'accélérer une instruction SQL qui semble prendre un certain temps à s'exécuter. J'ai regardé autour de SO pour une question similaire mais je n'en ai pas trouvé.

Le but est de supprimer toutes les lignes du tableau A qui ont un identifiant correspondant dans le tableau B.

Je fais actuellement ce qui suit:

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

Il y a environ 100 000 lignes dans le tableau a et environ 22 000 lignes dans le tableau b. La colonne 'id' est le PK pour les deux tables.

Cette instruction prend environ 3 minutes pour s'exécuter sur ma boîte de test - Pentium D, XP SP3, 2 Go de RAM, MySQL 5.0.67. Cela me semble lent. Peut-être pas, mais je espérait accélérer les choses. Existe-t-il un moyen meilleur/plus rapide d'accomplir cela?


ÉDITER:

Quelques informations supplémentaires qui pourraient être utiles. Les tableaux A et B ont la même structure que j'ai fait ce qui suit pour créer le tableau B:

CREATE TABLE b LIKE a;

La table a (et donc la table b) a quelques index pour accélérer les requêtes qui sont faites contre elle. Encore une fois, je suis un novice relatif au travail DB et j'apprends toujours. Je ne sais pas quel effet, le cas échéant, cela a sur les choses. Je suppose que cela a un effet car les index doivent également être nettoyés, non? Je me demandais également s'il y avait d'autres paramètres DB qui pourraient affecter la vitesse.

De plus, j'utilise INNO DB.


Voici quelques informations supplémentaires qui pourraient vous être utiles.

Le tableau A a une structure similaire à celle-ci (j'ai un peu assaini cela):

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;

Je soupçonne qu'une partie du problème est qu'il existe un certain nombre d'index pour cette table. Le tableau B ressemble au tableau B, bien qu'il ne contienne que les colonnes id et h.

En outre, les résultats du profilage sont les suivants:

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

[~ # ~] résolu [~ # ~]

Merci à toutes les réponses et commentaires. Ils m'ont certainement fait réfléchir sur le problème. Félicitations à dotjoe pour m'avoir permis de m'éloigner du problème en posant la question simple "D'autres tables font-elles référence à a.id?"

Le problème était qu'il y avait un DELETE TRIGGER sur la table A qui appelait une procédure stockée pour mettre à jour deux autres tables, C et D. La table C avait un FK sur a.id et après avoir fait des choses liées à cet id dans la procédure stockée , il avait la déclaration,

DELETE FROM c WHERE c.id = theId;

J'ai examiné la déclaration EXPLAIN et l'ai réécrite comme suit:

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

Donc, je pouvais voir ce que cela faisait et cela m'a donné les informations suivantes:

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

Cela m'a dit que c'était une opération douloureuse à effectuer et comme elle allait être appelée 22500 fois (pour l'ensemble de données à supprimer), c'était le problème. Une fois que j'ai créé un INDEX sur cette colonne other_id et relancé l'EXPLAIN, j'ai obtenu:

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

Beaucoup mieux, en fait vraiment super.

J'ai ajouté que Index_1 et mes heures de suppression sont conformes aux heures rapportées par mattkemp . C'était une erreur vraiment subtile de ma part en raison de la taille de chaussures certaines fonctionnalités supplémentaires à la dernière minute. Il s'est avéré que la plupart des instructions alternatives DELETE/SELECT suggérées, comme Daniel , ont fini par prendre essentiellement le même temps et que soulmerge mentionné, la déclaration était à peu près la meilleure que j'allais pouvoir construire en fonction de ce que je devais faire. Une fois que j'ai fourni un index pour cette autre table C, mes SUPPRESSION ont été rapides.

Post-mortem :
Deux leçons apprises sont ressorties de cet exercice. Tout d'abord, il est clair que je n'ai pas exploité la puissance de l'instruction EXPLAIN pour avoir une meilleure idée de l'impact de mes requêtes SQL. C'est une erreur de débutant, donc je ne vais pas me battre à ce sujet. J'apprendrai de cette erreur. Deuxièmement, le code incriminé était le résultat d'une mentalité de "faire vite" et une conception/tests inadéquats ont conduit à ce que le problème n'apparaisse pas plus tôt. Si j'avais généré plusieurs ensembles de données de test de taille importante à utiliser comme entrée de test pour cette nouvelle fonctionnalité, je n'aurais pas perdu mon temps ni le vôtre. Mes tests côté DB manquaient de la profondeur que mon côté application avait en place. Maintenant, j'ai l'occasion d'améliorer cela.

Référence: instruction EXPLAIN

57
itsmatt

La suppression de données d'InnoDB est l'opération la plus coûteuse que vous puissiez lui demander. Comme vous l'avez déjà découvert, la requête elle-même n'est pas le problème - la plupart d'entre elles seront de toute façon optimisées pour le même plan d'exécution.

Bien qu'il puisse être difficile de comprendre pourquoi les SUPPRESSIONS de tous les cas sont les plus lentes, il existe une explication assez simple. InnoDB est un moteur de stockage transactionnel. Cela signifie que si votre requête était abandonnée à mi-parcours, tous les enregistrements seraient toujours en place comme si de rien n'était. Une fois terminé, tout sera parti au même instant. Pendant la SUPPRESSION, les autres clients se connectant au serveur verront les enregistrements jusqu'à la fin de votre SUPPRESSION.

Pour y parvenir, InnoDB utilise une technique appelée MVCC (Multi Version Concurrency Control). Ce qu'il fait, c'est de donner à chaque connexion une vue instantanée de la base de données entière telle qu'elle était au début de la première instruction de la transaction. Pour ce faire, chaque enregistrement dans InnoDB en interne peut avoir plusieurs valeurs - une pour chaque instantané. C'est aussi pourquoi compter sur InnoDB prend un certain temps - cela dépend de l'état de l'instantané que vous voyez à ce moment-là.

Pour votre transaction DELETE, chaque enregistrement identifié selon vos conditions de requête est marqué pour suppression. Comme d'autres clients peuvent accéder aux données en même temps, il ne peut pas les supprimer immédiatement de la table, car ils doivent voir leur instantané respectif pour garantir l'atomicité de la suppression.

Une fois que tous les enregistrements ont été marqués pour suppression, la transaction est validée avec succès. Et même dans ce cas, ils ne peuvent pas être immédiatement supprimés des pages de données réelles, avant que toutes les autres transactions ayant fonctionné avec une valeur d'instantané avant votre transaction DELETE ne se terminent également.

Donc, en fait, vos 3 minutes ne sont pas vraiment si lentes, compte tenu du fait que tous les enregistrements doivent être modifiés afin de les préparer à la suppression de manière sécurisée pour les transactions. Vous entendrez probablement votre disque dur fonctionner pendant l'exécution de l'instruction. Cela est dû à l'accès à toutes les lignes. Pour améliorer les performances, vous pouvez essayer d'augmenter la taille du pool de mémoire tampon InnoDB pour votre serveur et de limiter les autres accès à la base de données pendant que vous SUPPRIMEZ, ce qui réduit également le nombre de versions historiques qu'InnoDB doit conserver par enregistrement. Avec la mémoire supplémentaire, InnoDB pourrait être capable de lire (principalement) votre table en mémoire et d'éviter un certain temps de recherche de disque.

75

Votre temps de trois minutes semble vraiment lent. Je suppose que la colonne id n'est pas indexée correctement. Si vous pouviez fournir la définition exacte du tableau que vous utilisez, ce serait utile.

J'ai créé un simple script python pour produire des données de test et exécuté plusieurs versions différentes de la requête de suppression sur le même ensemble de données. Voici mes définitions de table:

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;

J'ai ensuite inséré 100k lignes dans a et 25k lignes dans b (dont 22,5k également dans a). Voici les résultats des différentes commandes de suppression. Au fait, j'ai laissé tomber et repeuplé la table entre les courses.

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)

Tous les tests ont été exécutés sur un processeur Intel Core2 quadricœur 2,5 GHz, 2 Go RAM avec Ubuntu 8.10 et MySQL 5.0. Notez que l'exécution d'une instruction sql est toujours monothread.


Mise à jour:

J'ai mis à jour mes tests pour utiliser le schéma de itsmatt. Je l'ai légèrement modifié en supprimant l'incrémentation automatique (je génère des données synthétiques) et l'encodage du jeu de caractères (ne fonctionnait pas - n'y a pas creusé).

Voici mes nouvelles définitions de table:

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;

J'ai ensuite relancé les mêmes tests avec 100k lignes en a et 25k lignes en b (et repeuplé entre les runs).

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)

Comme vous pouvez le voir, cela est un peu plus lent qu'auparavant, probablement en raison des multiples index. Cependant, il est loin de la marque des trois minutes.

Vous pouvez également envisager de déplacer le champ de texte long vers la fin du schéma. Je semble me souvenir que mySQL fonctionne mieux si tous les champs à taille restreinte sont en premier et que le texte, le blob, etc. sont à la fin.

9
mattkemp

Essaye ça:

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

L'utilisation de sous-requêtes a tendance à être plus lente que les jointures car elles sont exécutées pour chaque enregistrement de la requête externe.

8

C'est ce que je fais toujours, quand je dois opérer avec des données très volumineuses (ici: un exemple de table de test avec 150000 lignes):

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;

Dans ce cas, le sql filtre 50000 lignes dans la table de sauvegarde. La cascade de requêtes fonctionne sur ma machine lente en 5 secondes. Vous pouvez remplacer l'insert dans select par votre propre requête de filtre.

C'est l'astuce pour effectuer une suppression de masse sur de grandes bases de données!; =)

5
Tom Schaefer

Essayez ceci:

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

C'est beaucoup plus rapide que les requêtes normales.

Référez-vous à la syntaxe: http://dev.mysql.com/doc/refman/5.0/en/delete.html

3
Webrsk

Je sais que cette question a été à peu près résolue en raison des omissions d'indexation d'OP, mais je voudrais offrir ce conseil supplémentaire, qui est valable pour un cas plus générique de ce problème.

Je me suis personnellement occupé de supprimer de nombreuses lignes d'une table qui existent dans une autre et d'après mon expérience, il est préférable de procéder comme suit, surtout si vous vous attendez à ce que de nombreuses lignes soient supprimées. Plus important encore, cette technique améliorera le décalage de l'esclave de réplication, car plus chaque requête de mutateur s'exécute longtemps, plus le décalage serait mauvais (la réplication est à thread unique).

Donc, le voici: faites d'abord un SELECT, comme une requête séparée , en vous souvenant des ID retournés dans votre script/application, puis continuez à supprimer par lots (disons, 50 000 lignes à la fois). Cela permettra d'atteindre les objectifs suivants:

  • chacune des instructions de suppression ne verrouillera pas la table trop longtemps, ce qui ne laissera pas le retard de réplication devenir incontrôlable. Cela est particulièrement important si vous comptez sur votre réplication pour vous fournir des données relativement à jour. L'avantage de l'utilisation de lots est que si vous trouvez que chaque requête DELETE prend encore trop de temps, vous pouvez l'ajuster pour qu'elle soit plus petite sans toucher à aucune structure de base de données.
  • un autre avantage de l'utilisation d'un SELECT séparé est que le SELECT lui-même peut prendre un certain temps à s'exécuter, surtout s'il ne peut pour une raison quelconque utiliser les meilleurs index DB. Si le SELECT est interne à un DELETE, lorsque l'instruction entière migre vers les esclaves, il devra refaire le SELECT, ce qui risque de retarder les esclaves car il doit refaire le long select. Le décalage des esclaves souffre encore une fois. Si vous utilisez une requête SELECT distincte, ce problème disparaît, car tout ce que vous transmettez est une liste d'ID.

Faites-moi savoir s'il y a un défaut dans ma logique quelque part.

Pour plus de discussion sur le retard de réplication et les moyens de le combattre, semblable à celui-ci, voir MySQL Slave Lag (Delay) Explained And 7 Ways To Battle It

P.S. Une chose à laquelle il faut faire attention est, bien sûr, les modifications potentielles de la table entre les heures de fin de SELECT et de DELETE. Je vous laisse gérer ces détails en utilisant des transactions et/ou une logique pertinentes pour votre application.

3

Vous effectuez votre sous-requête sur "b" pour chaque ligne de "a".

Essayer:

DELETE FROM a USING a LEFT JOIN b ON a.id = b.id WHERE b.id IS NOT NULL;
3
Evert
DELETE FROM a WHERE id IN (SELECT id FROM b)
2
chaos

Vous devriez peut-être reconstruire les index avant d'exécuter une telle requête. Eh bien, vous devez les reconstruire périodiquement.

REPAIR TABLE a QUICK;
REPAIR TABLE b QUICK;

puis exécutez l'une des requêtes ci-dessus (c'est-à-dire)

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

La requête elle-même est déjà sous une forme optimale, la mise à jour des index fait que l'opération entière prend autant de temps. Vous pourriez désactiver les clés sur cette table avant l'opération, cela devrait accélérer les choses. Vous pouvez les réactiver ultérieurement, si vous n'en avez pas besoin immédiatement.

Une autre approche consisterait à ajouter une colonne d'indicateur deleted à votre table et à ajuster les autres requêtes afin qu'elles tiennent compte de cette valeur. Le type booléen le plus rapide dans mysql est CHAR(0) NULL (true = '', false = NULL). Ce serait une opération rapide, vous pouvez supprimer les valeurs par la suite.

Les mêmes pensées exprimées dans les déclarations 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 ce n'est pas ce que vous voulez aussi, vous pouvez voir ce que les docs mysql ont à dire sur les vitesse des instructions de suppression .

2
soulmerge

BTW, après avoir posté ce qui précède sur mon blog, Baron Schwartz de Percona a attiré mon attention sur le fait que son maatkit a déjà un outil juste à cet effet - mk-archiver. http://www.maatkit.org/doc/mk-archiver.html .

C'est probablement votre meilleur outil pour le travail.

2

Connectez la base de données à l'aide du terminal et exécutez la commande ci-dessous, regardez l'heure de résultat de chacun d'eux, vous constaterez que les heures de suppression des enregistrements 10, 100, 1000, 10000, 100000 ne sont pas multipliées.

  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;

Le temps de suppression de 10 000 enregistrements n'est pas 10 fois plus important que la suppression de 100 000 enregistrements. Ensuite, sauf pour trouver un moyen de supprimer les enregistrements plus rapidement, il existe des méthodes indirectes.

1, nous pouvons renommer le nom_table en nom_table_bak, puis sélectionner les enregistrements de nom_table_bak en nom_table.

2, pour supprimer 10000 enregistrements, nous pouvons supprimer 1000 enregistrements 10 fois. Il y a un exemple Ruby script pour le faire.

#!/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
1
yanyingwang

Évidemment, la requête SELECT qui jette les bases de votre opération DELETE est assez rapide, donc je pense que la contrainte de clé étrangère ou les index sont les raisons de votre requête extrêmement lente.

Essayer

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

Cela désactiverait les vérifications sur la clé étrangère. Malheureusement, vous ne pouvez pas désactiver (du moins je ne sais pas comment) les mises à jour de clés avec une table InnoDB. Avec une table MyISAM, vous pourriez faire quelque chose comme

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

En fait, je n'ai pas testé si ces paramètres affectaient la durée de la requête. Mais ça vaut le coup d'essayer.

1
Stefan Gehrig