Wie aktualisiere ich über 10 Millionen Zeilen in einer MySQL-Einzeltabelle so schnell wie möglich?

32

Verwenden von MySQL 5.6 mit der InnoDB-Speicher-Engine für die meisten Tabellen. Die Größe des InnoDB-Pufferpools beträgt 15 GB, und die Innodb DB + -Indizes liegen bei etwa 10 GB. Der Server verfügt über 32 GB RAM und führt Cent OS 7 x64 aus.

Ich habe einen großen Tisch, der über 10 Millionen Datensätze enthält.

Ich erhalte alle 24 Stunden eine aktualisierte Dump-Datei von einem Remote-Server. Die Datei ist im CSV-Format. Ich habe keine Kontrolle über dieses Format. Die Datei ist ~ 750 MB groß. Ich habe versucht, Daten zeilenweise in eine MyISAM-Tabelle einzufügen, und es dauerte 35 Minuten.

Ich muss nur 3 Werte pro Zeile von 10-12 aus der Datei entnehmen und in der Datenbank aktualisieren.

Was ist der beste Weg, um so etwas zu erreichen?

Ich muss das täglich tun.

Momentan sieht Flow so aus:

  1. mysqli_begin_transaction
  2. Dump-Datei Zeile für Zeile lesen
  3. Aktualisieren Sie jeden Datensatz Zeile für Zeile.
  4. mysqli_commit

Die oben genannten Vorgänge dauern ca. 30-40 Minuten. Dabei werden noch weitere Aktualisierungen durchgeführt, die ich erhalten kann

Wartezeitüberschreitung der Sperre überschritten; Starten Sie die Transaktion erneut

Update 1

Daten laden in neue Tabelle mit LOAD DATA LOCAL INFILE. In MyISAM hat es gedauert, 38.93 secwährend es in InnoDB 7 Minuten 5,21 Sekunden gedauert hat. Dann habe ich gemacht:

UPDATE table1 t1, table2 t2
SET 
t1.field1 = t2.field1,
t1.field2 = t2.field2,
t1.field3 = t2.field3
WHERE t1.field10 = t2.field10

Query OK, 434914 rows affected (22 hours 14 min 47.55 sec)

Update 2

Gleiches Update mit Join-Abfrage

UPDATE table1 a JOIN table2 b 
ON a.field1 = b.field1 
SET 
a.field2 = b.field2,
a.field3 = b.field3,
a.field4 = b.field4

(14 hours 56 min 46.85 sec)

Erläuterungen zu Fragen in Kommentaren:

  • Ungefähr 6% der Zeilen in der Tabelle werden von der Datei aktualisiert, aber manchmal können es bis zu 25% sein.
  • Es gibt Indizes für die Felder, die aktualisiert werden. Die Tabelle enthält 12 Indizes, und 8 Indizes enthalten die Aktualisierungsfelder.
  • Es ist nicht erforderlich , das Update in einer Transaktion durchzuführen. Es kann einige Zeit dauern, aber nicht länger als 24 Stunden. Ich versuche, es in 1 Stunde zu erledigen, ohne die gesamte Tabelle zu sperren, da ich später den von dieser Tabelle abhängigen Sphinx-Index aktualisieren muss. Es spielt keine Rolle, ob die Schritte länger dauern, solange die Datenbank für andere Aufgaben verfügbar ist.
  • Ich könnte das CSV-Format in einem Vorverarbeitungsschritt ändern. Das einzige, was zählt, ist ein schnelles Update und ohne Sperren.
  • Tabelle 2 ist MyISAM. Dies ist die neu erstellte Tabelle aus der CSV-Datei, die die Ladedaten-Datei verwendet. MYI Dateigröße beträgt 452 MB. Tabelle 2 ist in der Spalte field1 indiziert.
  • MYD der MyISAM-Tabelle ist 663 MB.

Update 3:

Hier finden Sie weitere Details zu beiden Tabellen.

CREATE TABLE `content` (
  `hash` char(40) CHARACTER SET ascii NOT NULL DEFAULT '',
  `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
  `og_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
  `keywords` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
  `files_count` smallint(5) unsigned NOT NULL DEFAULT '0',
  `more_files` smallint(5) unsigned NOT NULL DEFAULT '0',
  `files` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '0',
  `category` smallint(3) unsigned NOT NULL DEFAULT '600',
  `size` bigint(19) unsigned NOT NULL DEFAULT '0',
  `downloaders` int(11) NOT NULL DEFAULT '0',
  `completed` int(11) NOT NULL DEFAULT '0',
  `uploaders` int(11) NOT NULL DEFAULT '0',
  `creation_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `upload_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `last_updated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `vote_up` int(11) unsigned NOT NULL DEFAULT '0',
  `vote_down` int(11) unsigned NOT NULL DEFAULT '0',
  `comments_count` int(11) NOT NULL DEFAULT '0',
  `imdb` int(8) unsigned NOT NULL DEFAULT '0',
  `video_sample` tinyint(1) NOT NULL DEFAULT '0',
  `video_quality` tinyint(2) NOT NULL DEFAULT '0',
  `audio_lang` varchar(127) CHARACTER SET ascii NOT NULL DEFAULT '',
  `subtitle_lang` varchar(127) CHARACTER SET ascii NOT NULL DEFAULT '',
  `verified` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `uploader` int(11) unsigned NOT NULL DEFAULT '0',
  `anonymous` tinyint(1) NOT NULL DEFAULT '0',
  `enabled` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `tfile_size` int(11) unsigned NOT NULL DEFAULT '0',
  `scrape_source` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `record_num` int(11) unsigned NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`record_num`),
  UNIQUE KEY `hash` (`hash`),
  KEY `uploaders` (`uploaders`),
  KEY `tfile_size` (`tfile_size`),
  KEY `enabled_category_upload_date_verified_` (`enabled`,`category`,`upload_date`,`verified`),
  KEY `enabled_upload_date_verified_` (`enabled`,`upload_date`,`verified`),
  KEY `enabled_category_verified_` (`enabled`,`category`,`verified`),
  KEY `enabled_verified_` (`enabled`,`verified`),
  KEY `enabled_uploader_` (`enabled`,`uploader`),
  KEY `anonymous_uploader_` (`anonymous`,`uploader`),
  KEY `enabled_uploaders_upload_date_` (`enabled`,`uploaders`,`upload_date`),
  KEY `enabled_verified_category` (`enabled`,`verified`,`category`),
  KEY `verified_enabled_category` (`verified`,`enabled`,`category`)
) ENGINE=InnoDB AUTO_INCREMENT=7551163 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ROW_FORMAT=FIXED


CREATE TABLE `content_csv_dump_temp` (
  `hash` char(40) CHARACTER SET ascii NOT NULL DEFAULT '',
  `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `category_id` int(11) unsigned NOT NULL DEFAULT '0',
  `uploaders` int(11) unsigned NOT NULL DEFAULT '0',
  `downloaders` int(11) unsigned NOT NULL DEFAULT '0',
  `verified` tinyint(1) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`hash`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

und hier ist die Aktualisierungsabfrage, die die contentTabelle unter Verwendung von Daten aus aktualisiertcontent_csv_dump_temp

UPDATE content a JOIN content_csv_dump_temp b 
ON a.hash = b.hash 
SET 
a.uploaders = b.uploaders,
a.downloaders = b.downloaders,
a.verified = b.verified

Update 4:

Alle oben genannten Tests wurden auf einer Testmaschine durchgeführt, aber jetzt habe ich dieselben Tests auf der Produktionsmaschine durchgeführt, und die Abfragen sind sehr schnell.

mysql> UPDATE content_test a JOIN content_csv_dump_temp b
    -> ON a.hash = b.hash
    -> SET
    -> a.uploaders = b.uploaders,
    -> a.downloaders = b.downloaders,
    -> a.verified = b.verified;
Query OK, 2673528 rows affected (7 min 50.42 sec)
Rows matched: 7044818  Changed: 2673528  Warnings: 0

Ich entschuldige mich für meinen Fehler. Es ist besser, Join anstelle jedes Datensatzupdates zu verwenden. Jetzt versuche ich, mpre mit dem von rick_james vorgeschlagenen Index zu verbessern. Wird aktualisiert, sobald das Benchmarking abgeschlossen ist.

AMB
quelle
Haben Sie eine Zusammensetzung INDEX(field2, field3, field4) (in beliebiger Reihenfolge)? Bitte zeigen Sie uns SHOW CREATE TABLE.
Rick James
1
12 und 8 Indizes sind ein schwerwiegender Teil Ihres Problems. MyISAM ist ein weiterer wichtiger Teil. InnoDB oder TokuDB erzielen mit mehreren Indizes eine viel bessere Leistung.
Rick James
Du hast zwei verschiedene UPDATEs . Bitte teilen Sie uns genau mit, wie die einfache Anweisung zum Aktualisieren der Tabelle aus den CSV-Daten aussieht. Dann können wir Ihnen möglicherweise dabei helfen, eine Technik zu entwickeln, die Ihren Anforderungen entspricht.
Rick James
@ RickJames gibt es nur eine update, und überprüfen Sie bitte aktualisierte Frage., Danke
AMB

Antworten:

17

Nach meiner Erfahrung würde ich LOAD DATA INFILE verwenden , um Ihre CSV-Datei zu importieren.

Die LOAD DATA INFILE-Anweisung liest Zeilen aus einer Textdatei mit sehr hoher Geschwindigkeit in eine Tabelle.

Beispiel Ich habe im Internet Load Data Beispiel gefunden . Ich habe dieses Beispiel auf meiner Box getestet und es hat gut funktioniert

Beispieltabelle

CREATE TABLE example (
  `Id` int(11) NOT NULL AUTO_INCREMENT,
  `Column2` varchar(14) NOT NULL,
  `Column3` varchar(14) NOT NULL,
  `Column4` varchar(14) NOT NULL,
  `Column5` DATE NOT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB

Beispiel CSV-Datei

# more /tmp/example.csv
Column1,Column2,Column3,Column4,Column5
1,A,Foo,sdsdsd,4/13/2013
2,B,Bar,sdsa,4/12/2013
3,C,Foo,wewqe,3/12/2013
4,D,Bar,asdsad,2/1/2013
5,E,FOObar,wewqe,5/1/2013

Import-Anweisung, die von der MySQL-Konsole ausgeführt werden soll

LOAD DATA LOCAL INFILE '/tmp/example.csv'
    -> INTO TABLE example
    -> FIELDS TERMINATED BY ','
    -> LINES TERMINATED BY '\n'
    -> IGNORE 1 LINES
    -> (id, Column3,Column4, @Column5)
    -> set
    -> Column5 = str_to_date(@Column5, '%m/%d/%Y');

Ergebnis

MySQL [testcsv]> select * from example;
+----+---------+---------+---------+------------+
| Id | Column2 | Column3 | Column4 | Column5    |
+----+---------+---------+---------+------------+
|  1 |         | Column2 | Column3 | 0000-00-00 |
|  2 |         | B       | Bar     | 0000-00-00 |
|  3 |         | C       | Foo     | 0000-00-00 |
|  4 |         | D       | Bar     | 0000-00-00 |
|  5 |         | E       | FOObar  | 0000-00-00 |
+----+---------+---------+---------+------------+

IGNORE ignoriert einfach die erste Zeile, die Spaltenüberschriften sind.

Nach IGNORE geben wir die zu importierenden Spalten an (überspringen Spalte 2), die einem der Kriterien in Ihrer Frage entsprechen.

Hier ist ein weiteres Beispiel direkt von Oracle: LOAD DATA INFILE-Beispiel

Dies sollte ausreichen, um Ihnen den Einstieg zu erleichtern.

Craig Efrein
quelle
Ich könnte Ladedaten zum Laden von Daten in die temporäre Tabelle verwenden und dann andere Abfragen verwenden, um sie in der Haupttabelle zu aktualisieren. Danke
AMB
14

In Anbetracht all der erwähnten Dinge sieht es so aus, als ob der Engpass der Join selbst ist.

Aspekt 1: Puffergröße verbinden

Höchstwahrscheinlich ist Ihre join_buffer_size zu niedrig.

Gemäß der MySQL-Dokumentation zur Verwendung des Join Buffer Cache durch MySQL

Wir speichern nur die verwendeten Spalten im Join-Puffer, nicht die gesamten Zeilen.

In diesem Fall bleiben die Schlüssel des Join-Puffers im RAM.

Sie haben 10 Millionen Zeilen mal 4 Bytes für jeden Schlüssel. Das sind ungefähr 40 Millionen.

Versuchen Sie es in der Sitzung auf 42 Millionen (etwas größer als 40 Millionen) zu erhöhen.

SET join_buffer_size = 1024 * 1024 * 42;
UPDATE table1 a JOIN table2 b 
ON a.field1 = b.field1 
SET 
a.field2 = b.field2,
a.field3 = b.field3,
a.field4 = b.field4;

Wenn dies der Trick ist, fahren Sie fort, um dies hinzuzufügen my.cnf

[mysqld]
join_buffer_size = 42M

Ein Neustart von mysqld ist für neue Verbindungen nicht erforderlich. Renn einfach

mysql> SET GLOBAL join_buffer_size = 1024 * 1024 * 42;

ASPECT # 2: Join-Vorgang

Sie können den Stil der Verknüpfungsoperation ändern, indem Sie den Optimierer tweeten

Gemäß der MySQL-Dokumentation zum Blockieren von Nested-Loop- und Batched-Key-Access-Joins

Wenn BKA verwendet wird, definiert der Wert von join_buffer_size, wie groß der Stapel von Schlüsseln in jeder Anforderung an die Speicher-Engine ist. Je größer der Puffer ist, desto sequentieller wird der Zugriff auf die rechte Tabelle einer Verknüpfungsoperation ausgeführt, wodurch die Leistung erheblich verbessert werden kann.

Damit BKA verwendet werden kann, muss das batched_key_access-Flag der Systemvariablen optimizer_switch auf on gesetzt sein. BKA verwendet MRR, daher muss auch das mrr-Flag aktiviert sein. Derzeit ist die Kostenschätzung für MRR zu pessimistisch. Daher muss mrr_cost_based auch ausgeschaltet sein, damit BKA verwendet werden kann.

Dieselbe Seite empfiehlt dies zu tun:

mysql> SET optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';

ASPECT # 3: Schreiben von Updates auf die Festplatte (OPTIONAL)

Die meisten vergessen , die zu erhöhen innodb_write_io_threads zu schreiben schmutzigen Seiten aus dem Pufferpool schneller.

[mysqld]
innodb_write_io_threads = 16

Sie müssen MySQL für diese Änderung neu starten

VERSUCHE ES !!!

RolandoMySQLDBA
quelle
Nett! +1 für den abstimmbaren Join-Buffer-Tipp. Wenn Sie sich anschließen müssen, schließen Sie sich dem Gedächtnis an. Guter Tipp!
Peter Dixon-Moses
3
  1. CREATE TABLE das passt zur CSV
  2. LOAD DATA in diesen Tisch
  3. UPDATE real_table JOIN csv_table ON ... SET ..., ..., ...;
  4. DROP TABLE csv_table;

Schritt 3 ist viel schneller als zeilenweise, sperrt jedoch alle Zeilen in der Tabelle für einen nicht unerheblichen Zeitraum. Wenn diese Sperrzeit wichtiger ist als die Dauer des gesamten Vorgangs, ...

Wenn nichts anderes an den Tisch schreibt, dann ...

  1. CREATE TABLEdas passt zur CSV; keine Indizes außer dem, was in der JOINin der benötigt wird UPDATE. Wenn es einzigartig ist, machen Sie es PRIMARY KEY.
  2. LOAD DATA in diesen Tisch
  3. kopiere das real_tablenach new_table( CREATE ... SELECT)
  4. UPDATE new_table JOIN csv_table ON ... SET ..., ..., ...;
  5. RENAME TABLE real_table TO old, new_table TO real_table;
  6. DROP TABLE csv_table, old;

Schritt 3 ist schneller als das Update, insbesondere wenn nicht benötigte Indizes weggelassen werden.
Schritt 5 ist "augenblicklich".

Rick James
quelle
Nehmen wir in Sekunden an, dass wir nach Schritt 3 Schritt 4 ausführen. Dann werden die neuen Daten in die Tabelle real_table eingefügt, sodass wir diese Daten in der Tabelle new_table verpassen. Was ist die Problemumgehung dafür? Danke
AMB
Sehen Sie was pt-online-schema-digest; Es kümmert sich um solche Fragen über ein TRIGGER.
Rick James
Sie haben wahrscheinlich noch nicht brauchen keine Indizes für die Tabelle aus LOAD DATA. Das Hinzufügen unnötiger Indizes ist zeitaufwändig.
Rick James
Basierend auf den neuesten Informationen neige ich dazu, die CSV-Datei mit nur einer AUTO_INCREMENTZeile in eine MyISAM-Tabelle zu laden und dann auf der Grundlage der PK jeweils 1 KB-Zeilen zu teilen. Aber ich muss alle Anforderungen und das Tabellenschema sehen, bevor ich versuchen kann, die Details zu formulieren.
Rick James
Ich habe Hash als festgelegt PRIMARY index, aber während das Chunking in 50 KB mit der Bestellabfrage mehr Zeit in Anspruch nimmt. Wäre es besser, wenn ich ein automatisches Inkrement erstelle? und setze es als PRIMARY index?
AMB
3

Du hast gesagt:

  • Aktualisierungen betreffen 6-25% Ihrer Tabelle
  • Sie möchten dies so schnell wie möglich tun (<1 Std.)
  • ohne verriegelung
  • Es muss sich nicht um eine einzelne Transaktion handeln
  • Noch (im Kommentar zur Antwort von Rick James) äußern Sie Besorgnis über die Rennbedingungen

Viele dieser Aussagen können widersprüchlich sein. Zum Beispiel große Aktualisierungen ohne Sperren der Tabelle. Oder vermeiden Sie Rennbedingungen ohne eine einzige große Transaktion.

Da Ihre Tabelle stark indiziert ist, können sowohl Einfügungen als auch Aktualisierungen langsam sein.


Rennbedingungen vermeiden

Wenn Sie Ihrer Tabelle einen aktualisierten Zeitstempel hinzufügen können, können Sie die Rennbedingungen ermitteln und gleichzeitig vermeiden, eine halbe Million Aktualisierungen in einer einzigen Transaktion zu protokollieren.

Auf diese Weise können Sie zeilenweise Aktualisierungen durchführen (wie derzeit), jedoch mit automatischer Festschreibung oder sinnvolleren Transaktionsstapeln.

Sie vermeiden Rennbedingungen (während der zeilenweisen Aktualisierung), indem Sie prüfen, ob noch keine spätere Aktualisierung stattgefunden hat ( UPDATE ... WHERE pk = [pk] AND updated < [batchfile date]).

Auf diese Weise können Sie vor allem parallele Aktualisierungen durchführen.


So schnell wie möglich laufen - Parallelisieren

Mit diesem Zeitstempel prüfen Sie jetzt an Ort und Stelle:

  1. Teilen Sie Ihre Batch-Datei in Teile mit einer angemessenen Größe auf (z. B. 50.000 Zeilen / Datei).
  2. Lassen Sie parallel dazu in jeder Datei ein Skript lesen und eine Datei mit 50.000 UPDATE-Anweisungen ausgeben.
  3. Parallel dazu muss nach Abschluss von (2) mysqljede SQL-Datei ausgeführt werden.

(zB im bashBlick auf splitund xargs -Pnach Möglichkeiten , um einfach einen Befehl viele Möglichkeiten , parallel laufen. Grad der Parallelität hängt davon ab , wie viele Threads Sie bereit sind , zu dem widmen Update )

Peter Dixon-Moses
quelle
Denken Sie daran, dass "Zeile für Zeile" wahrscheinlich 10x langsamer ist als das Ausführen von Dingen in Chargen von mindestens 100.
Rick James
In diesem Fall müssten Sie einen Benchmark durchführen, um sicherzugehen. Beim Aktualisieren von 6-25% einer Tabelle (mit 8 Indizes, die mit den aktualisierten Spalten verknüpft sind) würde ich die Möglichkeit in Betracht ziehen, dass die Indexverwaltung zum Engpass wird.
Peter Dixon-Moses
Ich meine, in einigen Fällen ist es möglicherweise schneller, Indizes zu löschen, zu aktualisieren und sie nach ... neu zu erstellen, aber OP möchte keine Ausfallzeiten.
Peter Dixon-Moses
1

Große Updates sind E / A-gebunden. Ich würde vorschlagen:

  1. Erstellen Sie eine eindeutige Tabelle, in der Ihre 3 häufig aktualisierten Felder gespeichert werden. Nennen wir eine Tabelle assets_static, in der Sie statische Daten speichern, und die andere assets_dynamic , in der Uploader, Downloader und verifizierte Daten gespeichert werden.
  2. Wenn Sie können, verwenden Sie die MEMORY-Engine für die Tabelle assets_dynamic . (Sicherung nach jedem Update auf Festplatte).
  3. Aktualisieren Sie Ihr leichtes und flinkes assets_dynamic gemäß Update 4 (dh LOAD INFILE ... INTO temp; UPDATE assets_dynamic a JOIN temp b auf a.id = b.id SET [was aktualisiert werden muss]. Dies sollte weniger als a dauern Minute. (Auf unserem System hat assets_dynamic 95 Millionen Zeilen und die Aktualisierung hat Auswirkungen auf ~ 6 Millionen Zeilen in etwas mehr als 40 Sekunden .)
  4. Wenn Sie den Sphinx-Indexer ausführen , müssen Sie die Attribute assets_static und assets_dynamic JOIN (vorausgesetzt, Sie möchten eines dieser Felder als Attribut verwenden).
user3127882
quelle
0

Damit das UPDATEschnell läuft, braucht man

INDEX(uploaders, downloaders, verified)

Es kann auf beiden Tischen stehen. Die drei Felder können in beliebiger Reihenfolge sein.

Dies erleichtert die UPDATEschnelle Zuordnung der Zeilen zwischen den beiden Tabellen.

Und macht die Datentypen das gleiche in den beiden Tabellen (beide INT SIGNEDoder beide INT UNSIGNED).

Rick James
quelle
Dies hat das Update tatsächlich verlangsamt.
AMB
Hmmm ... Bitte geben Sie an EXPLAIN UPDATE ...;.
Rick James