web-dev-qa-db-fra.com

Supprimer des millions de lignes dans MySQL

J'ai récemment trouvé et corrigé un bogue dans un site sur lequel je travaillais, ce qui a entraîné des millions de lignes de données en double dans une table qui sera assez grande même sans elles (toujours dans les millions). Je peux facilement trouver ces lignes en double et exécuter une seule requête de suppression pour les tuer toutes. Le problème est qu'essayer de supprimer autant de lignes en une seule fois verrouille la table pendant longtemps, ce que j'aimerais éviter si possible. Les seules façons que je peux voir pour se débarrasser de ces lignes, sans supprimer le site (en verrouillant la table) sont:

  1. Écrivez un script qui exécutera des milliers de petites requêtes de suppression dans une boucle. Cela contournera théoriquement le problème de la table verrouillée car d'autres requêtes pourront le faire dans la file d'attente et s'exécuter entre les suppressions. Mais cela va encore augmenter considérablement la charge de la base de données et son exécution prendra beaucoup de temps.
  2. Renommez la table et recréez la table existante (elle sera désormais vide). Ensuite, faites mon nettoyage sur la table renommée. Renommez la nouvelle table, renommez l'ancienne et fusionnez les nouvelles lignes dans la table renommée. Cela prend beaucoup plus d'étapes, mais devrait faire le travail avec une interruption minimale. La seule partie délicate ici est que le tableau en question est un tableau de rapport, donc une fois qu'il a été renommé et que le vide a été mis à sa place, tous les rapports historiques disparaissent jusqu'à ce que je le remette en place. De plus, le processus de fusion pourrait être un peu pénible en raison du type de données stockées. Dans l'ensemble, c'est mon choix probable en ce moment.

Je me demandais simplement si quelqu'un d'autre avait déjà rencontré ce problème et, dans l'affirmative, comment vous l'avez résolu sans interrompre le site et, espérons-le, avec une interruption minimale voire inexistante pour les utilisateurs? Si je choisis le numéro 2, ou une approche différente et similaire, je peux planifier que les choses s'exécutent tard le soir et faire la fusion tôt le lendemain matin et informer les utilisateurs à l'avance, donc ce n'est pas énorme. Je cherche juste à voir si quelqu'un a des idées pour une meilleure ou plus facile façon de faire le nettoyage.

67
Steven Surowiec
DELETE FROM `table`
WHERE (whatever criteria)
ORDER BY `id`
LIMIT 1000

Laver, rincer, répéter jusqu'à ce que zéro rang soit affecté. Peut-être dans un script qui dort une seconde ou trois entre les itérations.

132
chaos

Je recommanderais également d'ajouter quelques contraintes à votre table pour vous assurer que cela ne se reproduise plus. Un million de lignes, à 1000 par tir, nécessiteront 1000 répétitions d'un script. Si le script s'exécute une fois toutes les 3,6 secondes, vous aurez terminé dans une heure. Pas de soucis. Vos clients ne remarqueront probablement pas.

8
duffymo

J'ai eu un cas d'utilisation de la suppression de 1M + lignes dans la table 25M + lignes dans MySQL. J'ai essayé différentes approches comme les suppressions de lots (décrites ci-dessus).
J'ai découvert que le moyen le plus rapide (copie des enregistrements requis dans une nouvelle table):

  1. Créez une table temporaire contenant uniquement des identifiants.

CREATE TABLE id_temp_table (temp_id int);

  1. Insérez les identifiants à supprimer:

insérer dans id_temp_table (temp_id) sélectionnez .....

  1. Créer une nouvelle table table_new

  2. Insérez tous les enregistrements de la table dans table_new sans lignes inutiles qui se trouvent dans id_temp_table

insérer dans table_new .... où table_id PAS DANS (sélectionnez distinct (temp_id) de id_temp_table);

  1. Renommer les tables

L'ensemble du processus a pris environ 1 heure. Dans mon cas d'utilisation, la suppression simple d'un lot sur 100 enregistrements a pris 10 minutes.

6
user1459144

ce qui suit supprime 1 000 000 d'enregistrements, un par un.

 for i in `seq 1 1000`; do 
     mysql  -e "select id from table_name where (condition) order by id desc limit 1000 " | sed 's;/|;;g' | awk '{if(NR>1)print "delete from table_name where id = ",$1,";" }' | mysql; 
 done

vous pouvez les regrouper et supprimer le nom_table où IN (id1, id2, .. idN) est trop sûr sans trop de difficultés

6
rich

J'ai fait face à un problème similaire. Nous avions une très grande table, d'environ 500 Go de taille sans partitionnement et un seul index sur la colonne primary_key. Notre maître était une grosse machine, 128 cœurs et 512 Go de RAM et nous avions aussi plusieurs esclaves. Nous avons essayé quelques techniques pour lutter contre la suppression à grande échelle des lignes. Je vais énumérer tous ici du pire au meilleur que nous avons trouvé-

  1. Récupération et suppression d'une ligne à la fois. C'est le pire absolu que vous puissiez faire. Donc, nous n'avons même pas essayé cela.
  2. Récupération des premières lignes "X" de la base de données à l'aide d'une requête de limite sur la colonne primary_key, puis vérification des ID de ligne à supprimer dans l'application et lancement d'une seule requête de suppression avec une liste d'ID de primary_key. Donc, 2 requêtes par ligne "X". Maintenant, cette approche était correcte, mais cela en utilisant un travail par lots a supprimé environ 5 millions de lignes en 10 minutes environ, à cause desquelles les esclaves de notre base de données MySQL ont été retardés de 105 secondes. Décalage de 105 secondes en 10 minutes d'activité. Nous avons donc dû arrêter.
  3. Dans cette technique, nous avons introduit un décalage de 50 ms entre notre extraction par lots ultérieure et les suppressions de taille "X" chacune. Cela a résolu le problème de décalage, mais nous supprimions maintenant 1,2 à 1,3 million de lignes par 10 minutes, contre 5 millions dans la technique n ° 2.
  4. Partitionnement de la table de base de données, puis suppression de toutes les partitions lorsque cela n'est pas nécessaire. C'est la meilleure solution que nous ayons mais elle nécessite une table pré-partitionnée. Nous avons suivi l'étape 3 parce que nous avions une très ancienne table non partitionnée avec seulement l'indexation sur la colonne primary_key. La création d'une partition aurait pris trop de temps et nous étions en mode crise. Voici quelques liens relatifs au partitionnement que j'ai trouvé utiles - Référence MySQL officielle , Partitionnement quotidien Oracle DB .

Donc, IMO, si vous pouvez vous permettre d'avoir le luxe de créer une partition dans votre table, optez pour l'option # 4, sinon, vous êtes coincé avec l'option # 3.

3
Mukul Bansal

J'utiliserais mk-archiver de l'excellent Maatkit paquet d'utilitaires (un tas de scripts Perl pour la gestion de MySQL) Maatkit est du Baron Schwartz, l'auteur de O'Reilly Livre "High Performance MySQL".

L'objectif est un travail à faible impact et uniquement vers l'avant pour grignoter les anciennes données de la table sans affecter OLTP interroge beaucoup. Vous pouvez insérer les données dans une autre table, qui ne doit pas nécessairement se trouver sur le même serveur. Vous pouvez également l'écrire dans un fichier dans un format adapté à LOAD DATA INFILE. Ou vous ne pouvez faire ni l'un ni l'autre, auquel cas il s'agit simplement d'une suppression incrémentielle.

Il est déjà conçu pour archiver vos lignes indésirables en petits lots et en prime, il peut enregistrer les lignes supprimées dans un fichier au cas où vous bousilleriez la requête qui sélectionne les lignes à supprimer.

Aucune installation requise, saisissez simplement http://www.maatkit.org/get/mk-archiver et exécutez perldoc dessus (ou lisez le site Web) pour la documentation.

3
casey

Faites-le par lots de disons 2000 lignes à la fois. Engagez-vous entre les deux. Un million de lignes n'est pas tant que ça et ce sera rapide, à moins que vous ayez de nombreux index sur la table.

1
cherouvim

Pour nous, le DELETE WHERE %s ORDER BY %s LIMIT %d la réponse n'était pas une option, car les critères WHERE étaient lents (une colonne non indexée) et frappaient le maître.

Sélectionnez dans une réplique en lecture une liste de clés primaires que vous souhaitez supprimer. Exportez avec ce type de format:

00669163-4514-4B50-B6E9-50BA232CA5EB
00679DE5-7659-4CD4-A919-6426A2831F35

Utilisez le script bash suivant pour saisir cette entrée et la fragmenter en instructions DELETE [nécessite bash ≥ 4 en raison de mapfile intégré]:

sql-chunker.sh(se souvenir de chmod +x moi, et changez le Shebang pour pointer vers votre exécutable bash 4):

#!/usr/local/Cellar/bash/4.4.12/bin/bash

# Expected input format:
: <<!
00669163-4514-4B50-B6E9-50BA232CA5EB
00669DE5-7659-4CD4-A919-6426A2831F35
!

if [ -z "$1" ]
  then
    echo "No chunk size supplied. Invoke: ./sql-chunker.sh 1000 ids.txt"
fi

if [ -z "$2" ]
  then
    echo "No file supplied. Invoke: ./sql-chunker.sh 1000 ids.txt"
fi

function join_by {
    local d=$1
    shift
    echo -n "$1"
    shift
    printf "%s" "${@/#/$d}"
}

while mapfile -t -n "$1" ary && ((${#ary[@]})); do
    printf "DELETE FROM my_cool_table WHERE id IN ('%s');\n" `join_by "','" "${ary[@]}"`
done < "$2"

Invoquez ainsi:

./sql-chunker.sh 1000 ids.txt > batch_1000.sql

Cela vous donnera un fichier avec une sortie formatée comme ceci (j'ai utilisé une taille de lot de 2):

DELETE FROM my_cool_table WHERE id IN ('006CC671-655A-432E-9164-D3C64191EDCE','006CD163-794A-4C3E-8206-D05D1A5EE01E');
DELETE FROM my_cool_table WHERE id IN ('006CD837-F1AD-4CCA-82A4-74356580CEBC','006CDA35-F132-4F2C-8054-0F1D6709388A');

Exécutez ensuite les instructions comme suit:

mysql --login-path=master billing < batch_1000.sql

Pour ceux qui ne connaissent pas login-path, c'est juste un raccourci pour se connecter sans taper de mot de passe dans la ligne de commande.

1
Birchlabs

Selon la documentation mysql , TRUNCATE TABLE est une alternative rapide à DELETE FROM. Essaye ça:

TRUNCATE TABLE nom_table

J'ai essayé ceci sur 50 millions de lignes et cela a été fait en deux minutes.

Remarque: les opérations de troncature ne sont pas sécurisées pour les transactions; une erreur se produit lors d'une tentative au cours d'une transaction active ou d'un verrouillage de table actif

1
by0

Je pense que la lenteur est due à "l'index clusterisé" de MySQl où les enregistrements réels sont stockés dans l'index de clé primaire - dans l'ordre de l'index de clé primaire. Cela signifie que l'accès à un enregistrement via la clé primaire est extrêmement rapide car il ne nécessite qu'une seule extraction de disque car l'enregistrement sur le disque là où il a trouvé la clé primaire correcte dans l'index.

Dans d'autres bases de données sans index cluster, l'index lui-même ne contient pas l'enregistrement mais juste un "décalage" ou un "emplacement" indiquant où se trouve l'enregistrement dans le fichier de table, puis une seconde extraction doit être effectuée dans ce fichier pour récupérer les données réelles .

Vous pouvez imaginer lors de la suppression d'un enregistrement dans un index cluster que tous les enregistrements au-dessus de cet enregistrement dans la table doivent être déplacés vers le bas pour éviter la création de trous massifs dans l'index (enfin, c'est ce dont je me souviens il y a quelques années au moins - versions ultérieures peut avoir changé cela).

Sachant ce qui précède, nous avons constaté que les suppressions accélérées dans MySQL consistaient à effectuer les suppressions dans l'ordre inverse. Cela produit le moins de mouvements d'enregistrement car vous supprimez d'abord les enregistrements de la fin, ce qui signifie que les suppressions suivantes ont moins d'objets à déplacer.

0
Volksman

Je n'ai rien scripté pour le faire, et le faire correctement nécessiterait absolument un script, mais une autre option consiste à créer un nouveau tableau en double et à sélectionner toutes les lignes que vous souhaitez y conserver. Utilisez un déclencheur pour le maintenir à jour pendant la fin de ce processus. Lorsqu'il est synchronisé (moins les lignes que vous souhaitez supprimer), renommez les deux tables dans une transaction, de sorte que la nouvelle remplace la précédente. Laissez tomber la vieille table, et le tour est joué!

Cela (évidemment) nécessite beaucoup d'espace disque supplémentaire et peut taxer vos ressources d'E/S, mais sinon, cela peut être beaucoup plus rapide.

Selon la nature des données ou en cas d'urgence, vous pouvez renommer l'ancienne table et créer une nouvelle table vide à sa place, et sélectionner les lignes "garder" dans la nouvelle table à votre guise ...

0
Tyler Hains