Question

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 cherché sur SO pour une question similaire mais je n'en ai pas trouvé.

L'objectif 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 des 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.Ce n'est peut-être pas le cas, mais j'espérais accélérer les choses.Existe-t-il un moyen meilleur/plus rapide d'y parvenir ?


MODIFIER:

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

CREATE TABLE b LIKE a;

La table a (et donc la table b) comporte quelques index pour accélérer les requêtes effectuées sur elle.Encore une fois, je suis relativement novice dans le domaine de la base de données et j'apprends encore.Je ne sais pas quel effet cela a, le cas échéant, sur les choses.Je suppose que cela a un effet car les index doivent également être nettoyés, n'est-ce pas ?Je me demandais également s'il existait d'autres paramètres de base de données susceptibles d'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 (je l'ai un peu épurée) :

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 vient du fait 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 au problème.Félicitations à dotjoe pour m'avoir amené à m'éloigner du problème en posant la simple question "Est-ce que d'autres tables font 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 renvoyait un FK à a.id et après avoir effectué certaines opérations liées à cet identifiant dans la procédure stockée, elle contenait l'instruction :

DELETE FROM c WHERE c.id = theId;

J'ai examiné l'instruction EXPLAIN et j'ai réécrit ceci comme suit :

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

J'ai donc pu 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 pénible à réaliser et que comme elle allait être appelée 22 500 fois (pour l'ensemble de données donné à supprimer), c'était là le problème.Une fois que j'ai créé un INDEX sur cette colonne other_id et réexécuté 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, voire vraiment génial.

J'ai ajouté que Index_1 et mes heures de suppression sont conformes aux heures signalées par matkemp.Il s’agissait d’une erreur très subtile de ma part en raison de l’utilisation de 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 déclaré, a fini par prendre essentiellement le même temps et comme fusion d'âmes 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 DELETE ont été rapides.

Autopsie:
Deux leçons apprises sont ressorties de cet exercice.Premièrement, 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 m’en vouloir à 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/des tests inadéquats ont conduit à ce que ce problème n'apparaisse pas plus tôt.Si j'avais généré plusieurs ensembles de données de test importants à 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é base de données manquaient de la profondeur que mon côté application avait en place.Maintenant, j'ai l'opportunité d'améliorer cela.

Référence:Déclaration EXPLIQUER

Était-ce utile?

La solution

La suppression de données d’InnoDB est l’opération la plus chère que vous puissiez demander. Comme vous avez déjà découvert que la requête elle-même n’est pas le problème, la plupart d’entre elles seront optimisées selon le même plan d’exécution.

Bien qu'il puisse être difficile de comprendre pourquoi les DELETE de tous les cas sont les plus lents, 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 rien ne s'était passé. Une fois terminé, tout sera parti au même moment. Pendant la suppression, d’autres clients se connectant au serveur verront les enregistrements jusqu’à ce que votre suppression soit terminée.

Pour ce faire, InnoDB utilise une technique appelée MVCC (Multi Version Concurrency Control). En gros, il s'agit 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 également pour cette raison que compter sur InnoDB prend un certain temps - cela dépend de l’état de la photo que vous voyez à ce moment-là.

Pour votre transaction DELETE, chaque enregistrement identifié en fonction de vos conditions de requête est marqué pour suppression. D'autres clients pouvant 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 capture instantanée respective 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, elles ne peuvent pas être immédiatement supprimées 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 soient également terminées.

Donc, en réalité, vos 3 minutes ne sont pas si lentes, compte tenu du fait que tous les enregistrements doivent être modifiés pour pouvoir être supprimés de manière sécurisée pour les transactions. Vous entendrez probablement & "Entendre &"; votre disque dur fonctionne pendant que l'instruction est exécutée. Ceci est causé par 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 d'essayer 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 peut-être lire votre table (principalement) en mémoire et éviter un temps de recherche de disque.

Autres conseils

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 de la table que vous utilisez, cela serait utile.

J'ai créé un script python simple pour produire des données de test et ai 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 étaient également en a). Voici les résultats des différentes commandes de suppression. J'ai laissé tomber et repeuplé la table entre les courses en passant.

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 de RAM avec Ubuntu 8.10 et MySQL 5.0. Notez que l'exécution d'une instruction SQL est toujours à thread unique.

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 (cela ne fonctionnait pas - je ne creusais pas dedans).

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;

Je répète ensuite les mêmes tests avec 100 000 lignes dans a et 25 000 lignes dans b (et en remplissant à nouveau entre les exécutions).

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 constater, le processus est un peu plus lent qu’avant, probablement en raison de la multiplicité des index. Cependant, il est loin de trois minutes.

Vous pouvez également vous intéresser au déplacement du champ de texte intégral vers la fin du schéma. Il semble que je me souvienne que mySQL fonctionne mieux si tous les champs de taille restreinte sont d’abord et que le texte, le blob, etc. sont à la fin.

Essayez ceci:

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

Les sous-requêtes ont tendance à être plus lentes que les jointures car elles sont exécutées pour chaque enregistrement de la requête externe.

C’est ce que je fais toujours lorsque je dois utiliser des données très volumineuses (ici: un exemple de tableau 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, sql filtre 50000 lignes dans la table de sauvegarde. La cascade de requêtes s'exécute sur ma machine lente en 5 secondes. Vous pouvez remplacer l'insertion dans select par votre propre requête de filtre.

C’est l’astuce pour supprimer en masse des bases de données volumineuses!; =)

Vous faites votre sous-requête sur "b" pour chaque ligne dans "a".

Essayez:

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

Essayez ceci:

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

Il est beaucoup plus rapide que les requêtes normales.

Voir la syntaxe: http://dev.mysql.com /doc/refman/5.0/fr/delete.html

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

J'ai personnellement été confronté au fait de devoir supprimer de nombreuses tables d'une table existante dans une autre et, d'après mon expérience, il est préférable de procéder comme suit, en particulier si vous vous attendez à ce que de nombreuses lignes soient supprimées. Cette technique améliorera surtout le décalage de la réplication des esclaves, car plus la requête est longue, plus elle sera longue (la réplication est à thread unique).

Voici donc ce qui suit: commencez par sélectionner SELECT, en tant que requête distincte , en mémorisant les identifiants renvoyés dans votre script / votre application, puis en continuant la suppression par lots (par exemple, 50 000 lignes à la fois). ). Cela permettra d'atteindre les objectifs suivants:

  • chacune des instructions de suppression ne verrouillera pas la table trop longtemps et ne laissera donc pas le temps de réplication s'échapper . Il est particulièrement important que vous comptiez sur votre réplication pour vous fournir des données relativement à jour. L'avantage d'utiliser des 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 aux structures de base de données.
  • Un autre avantage de l'utilisation d'un SELECT distinct est que l'exécution du SELECT lui-même peut prendre beaucoup de temps , en particulier s'il ne peut pas, pour une raison quelconque, utiliser les meilleurs index de base de données. Si le SELECT est interne à un DELETE, lorsque l'ensemble de l'instruction migre vers les esclaves, il devra refaire le SELECT à nouveau, ce qui pourrait retarder les esclaves, car il devra refaire la sélection longue de nouveau. Slag lag, encore une fois, souffre beaucoup. Si vous utilisez une requête SELECT distincte, ce problème disparaît, car vous ne faites que transmettre une liste d'identifiants.

Faites-moi savoir s'il y a une faille dans ma logique quelque part.

Pour plus de détails sur le délai de réplication et les moyens de le combattre, voir Explication de MySQL Slave Lag (Delay) et de 7 façons de le combattre

P.S. Une chose à laquelle il faut faire attention est bien sûr les modifications potentielles apportées à la table entre le moment où SELECT termine et DELETE commence. Je vous laisserai gérer ces détails en utilisant des transactions et / ou des logiques pertinentes pour votre application.

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

Peut-être devriez-vous reconstruire les indicateurs avant d'exécuter une telle requête. Eh bien, vous devriez les reconstruire périodiquement.

REPAIR TABLE a QUICK;
REPAIR TABLE b QUICK;

puis exécutez l'une des requêtes ci-dessus (i.e.)

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

La requête elle-même est déjà sous une forme optimale. La mise à jour des index entraîne une opération aussi longue. Vous pouvez désactiver les clés sur cette table avant la 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 deleted colonne-indicateur à votre table et à ajuster les autres requêtes afin qu’elles prennent cette valeur en compte. 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 après.

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 cela aussi n’est pas ce que vous voulez, vous pouvez consulter ce que les documents mysql ont à dire sur le vitesse de suppression des instructions .

BTW, après avoir publié ce qui précède sur mon blog, le Baron Schwartz de Percona m'a signalé que son maatkit dispose déjà d'un outil spécialement conçu à cet effet: mk-archiver. http://www.maatkit.org/doc/mk-archiver.html.

C’est probablement votre meilleur outil pour le poste.

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

Essayez

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

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

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

En fait, je n'ai pas vérifié si ces paramètres auraient une incidence sur la durée de la requête. Mais ça vaut le coup d'essayer.

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

  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 nécessaire pour supprimer 10 000 enregistrements n’est pas 10 fois plus long que pour supprimer 100 000 enregistrements.Ensuite, outre la recherche d'un moyen de supprimer les enregistrements plus rapidement, il existe des méthodes indirectes.

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

2, Pour supprimer 10 000 enregistrements, nous pouvons supprimer 1 000 enregistrements 10 fois.Il existe un exemple de script Ruby 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

Technique de base pour supprimer plusieurs lignes de MySQL dans une seule table via le champ id

DELETE FROM tbl_name WHERE id <= 100 AND id >=200; Cette requête est responsable de la suppression de la condition correspondante entre 100 ET 200 de la table concernée

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top