Millionen von Zeilen in MySQL löschen

76

Ich habe kürzlich einen Fehler in einer Site gefunden und behoben, an dem ich gearbeitet habe, der zu Millionen doppelter Datenzeilen in einer Tabelle führte, die auch ohne sie ziemlich groß sein wird (immer noch in Millionenhöhe). Ich kann diese doppelten Zeilen leicht finden und eine einzelne Löschabfrage ausführen, um sie alle zu töten. Das Problem ist, dass der Versuch, so viele Zeilen auf einmal zu löschen, die Tabelle für eine lange Zeit sperrt, was ich nach Möglichkeit vermeiden möchte. Die einzigen Möglichkeiten, die ich sehen kann, um diese Zeilen zu entfernen, ohne die Site zu entfernen (indem die Tabelle gesperrt wird), sind:

  1. Schreiben Sie ein Skript, das Tausende kleinerer Löschabfragen in einer Schleife ausführt. Dies umgeht theoretisch das Problem mit gesperrten Tabellen, da andere Abfragen in die Warteschlange gelangen und zwischen den Löschvorgängen ausgeführt werden können. Die Datenbank wird jedoch immer noch stark belastet, und die Ausführung wird lange dauern.
  2. Benennen Sie die Tabelle um und erstellen Sie die vorhandene Tabelle neu (sie ist jetzt leer). Führen Sie dann meine Bereinigung für die umbenannte Tabelle durch. Benennen Sie die neue Tabelle um, benennen Sie die alte zurück und führen Sie die neuen Zeilen in der umbenannten Tabelle zusammen. Dies erfordert wesentlich mehr Schritte, sollte aber die Arbeit mit minimaler Unterbrechung erledigen. Der einzige schwierige Teil hier ist, dass es sich bei der fraglichen Tabelle um eine Berichtstabelle handelt. Sobald sie aus dem Weg umbenannt und die leere an ihre Stelle gesetzt wurde, verschwinden alle historischen Berichte, bis ich sie wieder einrichte. Außerdem kann der Zusammenführungsprozess aufgrund der Art der gespeicherten Daten etwas schmerzhaft sein. Insgesamt ist dies momentan meine wahrscheinliche Wahl.

Ich habe mich nur gefragt, ob jemand anderes dieses Problem schon einmal hatte und wenn ja, wie Sie damit umgegangen sind, ohne die Website herunterzufahren und hoffentlich mit minimalen oder gar keinen Unterbrechungen für die Benutzer? Wenn ich mich für Nummer 2 oder einen anderen, ähnlichen Ansatz entscheide, kann ich festlegen, dass das Material spät in der Nacht ausgeführt wird und die Zusammenführung am nächsten Morgen früh durchgeführt wird. Lassen Sie die Benutzer dies einfach im Voraus wissen, sodass dies keine große Sache ist. Ich bin nur auf der Suche nach Ideen für eine bessere oder einfachere Art der Bereinigung.

Steven Surowiec
quelle
Kürzlich wurden in weniger als einer Stunde rund 70 Millionen Datensätze im Produktionssystem durch eine gespeicherte Prozedur gelöscht. Überprüfen Sie diese Seite. Dies kann auch anderen helfen. Rathishkumar.in/2017/12/…
Rathish

Antworten:

151
DELETE FROM `table`
WHERE (whatever criteria)
ORDER BY `id`
LIMIT 1000

Waschen, spülen, wiederholen, bis keine Reihen mehr betroffen sind. Vielleicht in einem Skript, das zwischen den Iterationen eine oder drei Sekunden lang schläft.

Chaos
quelle
7
KISS gewinnt immer.
Electro
52
Wenn Sie DELETE mit LIMIT verwenden, sollten Sie ORDER BY wirklich verwenden, um die Abfrage deterministisch zu machen.
Wenn Sie dies
Ja stimmt. Bearbeitet per.
Chaos
6
Beachten Sie, dass man nicht DELETE ... JOIN mit ORDER BYoder kombinieren kannLIMIT .
Bischof
3
Ich habe immer noch meine Zweifel, ob ein Pivot-Tisch nicht der beste Weg ist, aber ich habe eine Prozedur durchgeführt, nur um die Vernunft zu wahren
Diogo Medeiros
9

Ich hatte einen Anwendungsfall des Löschens von 1M + Zeilen in der 25M + Zeilen Tabelle in MySQL. Versuchte verschiedene Ansätze wie Batch-Löschungen (oben beschrieben).
Ich habe herausgefunden, dass der schnellste Weg (Kopie der erforderlichen Datensätze in eine neue Tabelle):

  1. Erstellen Sie eine temporäre Tabelle, die nur IDs enthält.

CREATE TABLE id_temp_table (temp_id int);

  1. Fügen Sie IDs ein, die entfernt werden sollen:

in id_temp_table (temp_id) einfügen select .....

  1. Neue Tabelle erstellen table_new

  2. Fügen Sie alle Datensätze von table nach table_new ein, ohne unnötige Zeilen in id_temp_table

in table_new einfügen .... wobei table_id NOT IN (unterscheiden Sie (temp_id) von id_temp_table);

  1. Tabellen umbenennen

Der gesamte Vorgang dauerte ca. 1 Stunde. In meinem Anwendungsfall dauerte das einfache Löschen eines Stapels auf 100 Datensätzen 10 Minuten.

user1459144
quelle
Für Schritt 4 können Sie den Join verlassen, um den Index zu verwenden: Einfügen in table_new ... select ... from table left join id_temp_table t auf t.temp_id = table.id wobei t.temp_id NULL ist;
Softlion
8

Ich würde auch empfehlen, Ihrer Tabelle einige Einschränkungen hinzuzufügen, um sicherzustellen, dass Ihnen dies nicht noch einmal passiert. Für eine Million Zeilen mit 1000 pro Aufnahme sind 1000 Wiederholungen eines Skripts erforderlich. Wenn das Skript alle 3,6 Sekunden einmal ausgeführt wird, sind Sie in einer Stunde fertig. Keine Sorgen. Es ist unwahrscheinlich, dass Ihre Kunden es bemerken.

Duffymo
quelle
7

Im Folgenden werden nacheinander 1.000.000 Datensätze gelöscht.

 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

Sie könnten sie zusammen gruppieren und Tabellennamen löschen, wobei IN (id1, id2, .. idN) sicher auch ohne große Schwierigkeiten ist

Reich
quelle
1
Dies ist die einzige Lösung, die für mich mit einem 100-GB-Tisch funktioniert hat. Die Auswahl mit dem Limit 1000 betrug nur wenige Millisekunden, aber das Löschen mit derselben Abfrage dauerte eine Stunde für nur 1000 Datensätze, obwohl eine SSD vorhanden ist. Das Löschen auf diese Weise ist immer noch langsam, aber mindestens tausend Zeilen pro Sekunde und nicht Stunde.
user2693017
Das Löschen von 1 M Datensatz auf einmal wird Ihren Server töten
user21546
Ich konnte 100.000 Datensätze gleichzeitig löschen ( DELETE FROM table WHERE id <= 100000dann 200000 usw.). Jede Charge dauerte zwischen 30 Sekunden und 1 Minute. Als ich zuvor versucht habe, 1.300.000 auf einmal zu löschen, wurde die Abfrage mindestens 30 Minuten lang ausgeführt, bevor sie fehlschlug. ERROR 2013 (HY000): Lost connection to MySQL server during query.Ich habe diese Abfragen im MySQL-Client auf derselben virtuellen Maschine wie der Server ausgeführt, aber möglicherweise ist die Verbindung abgelaufen.
Buttle Butkus
3

Ich würde mk-archiver aus dem hervorragenden Maatkit- Dienstprogrammpaket (eine Reihe von Perl-Skripten für die MySQL-Verwaltung) verwenden. Maatkit stammt von Baron Schwartz, dem Autor des O'Reilly-Buches "High Performance MySQL".

Das Ziel ist ein Job mit geringen Auswirkungen und nur vorwärts, um alte Daten aus der Tabelle zu entfernen, ohne die OLTP-Abfragen stark zu beeinträchtigen. Sie können die Daten in eine andere Tabelle einfügen, die sich nicht auf demselben Server befinden muss. Sie können es auch in eine Datei in einem für LOAD DATA INFILE geeigneten Format schreiben. Oder Sie können beides nicht tun. In diesem Fall handelt es sich nur um ein inkrementelles LÖSCHEN.

Es wurde bereits für die Archivierung Ihrer unerwünschten Zeilen in kleinen Stapeln entwickelt und kann als Bonus die gelöschten Zeilen in einer Datei speichern, falls Sie die Abfrage vermasseln, mit der die zu entfernenden Zeilen ausgewählt werden.

Keine Installation erforderlich, greifen Sie einfach auf http://www.maatkit.org/get/mk-archiver zu und führen Sie perldoc darauf aus (oder lesen Sie die Website), um eine Dokumentation zu erhalten.

outcassed
quelle
3

Hier ist die empfohlene Vorgehensweise:

rows_affected = 0
do {
 rows_affected = do_query(
   "DELETE FROM messages WHERE created < DATE_SUB(NOW(),INTERVAL 3 MONTH)
   LIMIT 10000"
 )
} while rows_affected > 0

Das gleichzeitige Löschen von 10.000 Zeilen ist in der Regel groß genug, um jede Abfrage effizient zu gestalten, und kurz genug, um die Auswirkungen auf den Server4 zu minimieren (Transaktionsspeicher-Engines können von kleineren Transaktionen profitieren). Es kann auch eine gute Idee sein, zwischen den DELETE-Anweisungen eine gewisse Ruhezeit hinzuzufügen, um die Last über die Zeit zu verteilen und die Anzahl der gehaltenen Sperren zu verringern.

Referenz MySQL High Performance

Tho
quelle
2

Für uns war die DELETE WHERE %s ORDER BY %s LIMIT %dAntwort keine Option, da die WHERE-Kriterien langsam waren (eine nicht indizierte Spalte) und den Master treffen würden.

Wählen Sie aus einer Lesereplik eine Liste der Primärschlüssel aus, die Sie löschen möchten. Exportieren Sie mit diesem Format:

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

Verwenden Sie das folgende Bash-Skript, um diese Eingabe zu erfassen und in DELETE-Anweisungen zu unterteilen [erfordert Bash ≥ 4 aufgrund der mapfileintegrierten Funktion ]:

sql-chunker.sh (Denken Sie an chmod +xmich und ändern Sie den Shebang so, dass er auf Ihre ausführbare Bash 4-Datei zeigt.) :

#!/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"

Rufen Sie so auf:

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

Dadurch erhalten Sie eine Datei mit einer so formatierten Ausgabe (ich habe eine Stapelgröße von 2 verwendet):

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');

Führen Sie dann die Anweisungen folgendermaßen aus:

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

Für diejenigen, die nicht vertraut sind login-path, ist es nur eine Verknüpfung zum Anmelden, ohne ein Passwort in die Befehlszeile einzugeben.

Birchlabs
quelle
2

Ich hatte ein ähnliches Problem. Wir hatten eine wirklich große Tabelle mit einer Größe von ungefähr 500 GB ohne Partitionierung und einem einzigen Index für die Spalte primary_key. Unser Master war eine riesige Maschine, 128 Kerne und 512 GB RAM, und wir hatten auch mehrere Slaves. Wir haben einige Techniken ausprobiert, um das großflächige Löschen von Zeilen in Angriff zu nehmen. Ich werde sie alle hier auflisten, vom schlechtesten bis zum besten, das wir gefunden haben.

  1. Abrufen und Löschen von jeweils einer Zeile. Dies ist das absolut Schlimmste, was Sie tun können. Also haben wir das nicht einmal versucht.
  2. Abrufen der ersten 'X'-Zeilen aus der Datenbank mithilfe einer Grenzwertabfrage in der Spalte primary_key, Überprüfen der zu löschenden Zeilen-IDs in der Anwendung und Auslösen einer einzelnen Löschabfrage mit einer Liste der IDs von primary_key. Also 2 Abfragen pro 'X'-Zeile. Dieser Ansatz war in Ordnung, aber mit einem Batch-Job wurden in etwa 10 Minuten etwa 5 Millionen Zeilen gelöscht, wodurch die Slaves unserer MySQL-Datenbank um 105 Sekunden verzögert wurden. 105 Sekunden Verzögerung bei 10-minütiger Aktivität. Also mussten wir aufhören.
  3. Bei dieser Technik haben wir eine Verzögerung von 50 ms zwischen unserem nachfolgenden Batch-Abruf und Löschungen der Größe 'X' eingeführt. Dies löste das Verzögerungsproblem, aber wir löschten jetzt 1,2 bis 1,3 Millionen Zeilen pro 10 Minuten im Vergleich zu 5 Millionen in Technik 2.
  4. Partitionieren der Datenbanktabelle und Löschen der gesamten Partitionen, wenn diese nicht benötigt werden. Dies ist die beste Lösung, die wir haben, erfordert jedoch eine vorpartitionierte Tabelle. Wir folgten Schritt 3, da wir eine nicht partitionierte, sehr alte Tabelle hatten, in der nur die Spalte primary_key indiziert wurde. Das Erstellen einer Partition hätte zu viel Zeit in Anspruch genommen, und wir befanden uns in einem Krisenmodus. Hier sind einige Links zur Partitionierung, die ich hilfreich fand: Offizielle MySQL-Referenz , tägliche Oracle DB-Partitionierung .

Also, IMO, wenn Sie es sich leisten können, den Luxus zu haben, eine Partition in Ihrer Tabelle zu erstellen, wählen Sie Option 4, andernfalls bleiben Sie bei Option 3.

Mukul Bansal
quelle
1

Tun Sie dies in Stapeln von beispielsweise 2000 Zeilen gleichzeitig. Dazwischen festschreiben. Eine Million Zeilen sind nicht so viel und das wird schnell gehen, es sei denn, Sie haben viele Indizes in der Tabelle.

Cherouvim
quelle
0

Ich denke, die Langsamkeit ist auf den "Clustered Index" von MySQl zurückzuführen, in dem die tatsächlichen Datensätze im Primärschlüsselindex gespeichert sind - in der Reihenfolge des Primärschlüsselindex. Dies bedeutet, dass der Zugriff auf einen Datensatz über den Primärschlüssel extrem schnell ist, da nur ein Festplattenabruf erforderlich ist, da der Datensatz auf der Festplatte genau dort liegt, wo er den richtigen Primärschlüssel im Index gefunden hat.

In anderen Datenbanken ohne Clustered-Indizes enthält der Index selbst nicht den Datensatz, sondern nur einen "Offset" oder "Speicherort", der angibt, wo sich der Datensatz in der Tabellendatei befindet. Anschließend muss in dieser Datei ein zweiter Abruf durchgeführt werden, um die tatsächlichen Daten abzurufen .

Sie können sich vorstellen, dass beim Löschen eines Datensatzes in einem Clustered-Index alle Datensätze über diesem Datensatz in der Tabelle nach unten verschoben werden müssen, um zu vermeiden, dass im Index massive Löcher entstehen (genau daran erinnere ich mich zumindest vor einigen Jahren - spätere Versionen kann dies geändert haben).

Wenn wir wissen, dass das Löschen in MySQL wirklich beschleunigt wurde, müssen wir die Löschvorgänge in umgekehrter Reihenfolge ausführen. Dies führt zu der geringsten Bewegung von Datensätzen, da Sie Datensätze vom Ende zuerst löschen, was bedeutet, dass nachfolgende Löschvorgänge weniger Objekte verschieben müssen.

Volksman
quelle
0

Ich hatte eine wirklich geladene Basis, die ständig einige ältere Einträge löschen musste. Einige der Löschabfragen hingen an, sodass ich sie beenden musste. Wenn zu viele Löschvorgänge vorhanden waren, reagierte die gesamte Basis nicht mehr, sodass ich die parallelen Läufe einschränken musste. Also habe ich cron jobjede Minute einen Lauf erstellt , der dieses Skript startet:

#!/bin/bash

#######################
#
i_size=1000
max_delete_queries=10
sleep_interval=15
min_operations=8
max_query_time=1000

USER="user"
PASS="super_secret_password"

log_max_size=1000000
log_file="/var/tmp/clean_up.log"
#
#######################

touch $log_file
log_file_size=`stat -c%s "$log_file"`
if (( $log_file_size > $log_max_size ))
then
    rm -f "$log_file"
fi 

delete_queries=`mysql -u user -p$PASS -e  "SELECT * FROM information_schema.processlist WHERE Command = 'Query' AND INFO LIKE 'DELETE FROM big.table WHERE result_timestamp %';"| grep Query|wc -l`

## -- here the hanging DELETE queries will be stopped
mysql-u $USER -p$PASS -e "SELECT ID FROM information_schema.processlist WHERE Command = 'Query' AND INFO LIKE 'DELETE FROM big.table WHERE result_timestamp %'and TIME>$max_query_time;" |grep -v ID| while read -r id ; do
    echo "delete query stopped on `date`" >>  $log_file
    mysql -u $USER -p$PASS -e "KILL $id;"
done

if (( $delete_queries > $max_delete_queries ))
then
  sleep $sleep_interval

  delete_queries=`mysql-u $USER -p$PASS -e  "SELECT * FROM information_schema.processlist WHERE Command = 'Query' AND INFO LIKE 'DELETE FROM big.table WHERE result_timestamp %';"| grep Query|wc -l`

  if (( $delete_queries > $max_delete_queries ))
  then

      sleep $sleep_interval

      delete_queries=`mysql -u $USER -p$PASS -e  "SELECT * FROM information_schema.processlist WHERE Command = 'Query' AND INFO LIKE 'DELETE FROM big.table WHERE result_timestamp %';"| grep Query|wc -l`

      # -- if there are too many delete queries after the second wait
      #  the table will be cleaned up by the next cron job
      if (( $delete_queries > $max_delete_queries ))
        then
            echo "clean-up skipped on `date`" >> $log_file
            exit 1
        fi
  fi

fi

running_operations=`mysql-u $USER -p$PASS -p -e "SELECT * FROM INFORMATION_SCHEMA.PROCESSLIST WHERE COMMAND != 'Sleep';"| wc -l`

if (( $running_operations < $min_operations ))
then
    # -- if the database is not too busy this bigger batch can be processed
    batch_size=$(($i_size * 5))
else 
    batch_size=$i_size
fi

echo "starting clean-up on `date`" >>  $log_file

mysql-u $USER -p$PASS -e 'DELETE FROM big.table WHERE result_timestamp < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 31 DAY))*1000 limit '"$batch_size"';'

if [ $? -eq 0 ]; then
    # -- if the sql command exited normally the exit code will be 0
    echo "delete finished successfully on `date`" >>  $log_file
else
    echo "delete failed on `date`" >>  $log_file
fi

Damit habe ich ungefähr 2 Millionen Löschungen pro Tag erreicht, was für meinen Anwendungsfall in Ordnung war.

npocmaka
quelle
-3

Ich habe nichts geschrieben, um dies zu tun, und um es richtig zu machen, wäre unbedingt ein Skript erforderlich, aber eine andere Option besteht darin, eine neue, doppelte Tabelle zu erstellen und alle Zeilen auszuwählen, die Sie darin behalten möchten. Verwenden Sie einen Auslöser, um ihn während dieses Vorgangs auf dem neuesten Stand zu halten. Wenn es synchron ist (abzüglich der Zeilen, die Sie löschen möchten), benennen Sie beide Tabellen in einer Transaktion um, sodass die neue die alte ersetzt. Lass den alten Tisch fallen und voila!

Dies erfordert (offensichtlich) viel zusätzlichen Speicherplatz und kann Ihre E / A-Ressourcen belasten, kann aber ansonsten viel schneller sein.

Abhängig von der Art der Daten oder im Notfall können Sie die alte Tabelle umbenennen und an ihrer Stelle eine neue, leere Tabelle erstellen und die Zeilen "behalten" in der neuen Tabelle nach Belieben auswählen ...

Tyler Hains
quelle
-4

Nach Angaben der MySQL - Dokumentation , TRUNCATE TABLEist eine schnelle Alternative zu DELETE FROM. Versuche dies:

TRUNCATE TABLE Tabellenname

Ich habe dies in 50 Millionen Reihen versucht und es war innerhalb von zwei Minuten erledigt.

Hinweis: Kürzungsvorgänge sind nicht transaktionssicher. Beim Versuch einer aktiven Transaktion oder einer aktiven Tabellensperre tritt ein Fehler auf

by0
quelle
25
Dadurch werden Zeilen definitiv gelöscht. Ich bin mir ziemlich sicher, dass das OP selektiv sein will.
Adam Rowe