So optimieren Sie den MySQL-Deduplizierungsvorgang maximal

So optimieren Sie den MySQL-Deduplizierungsvorgang maximal

• Aufgeworfene Fragen

Die Struktur der Quelltabelle t_source ist wie folgt:

Artikel-ID int,
 Erstellungszeit Datum/Uhrzeit,
 Änderungszeit Datum/Uhrzeit,
 Artikelname varchar (20),
 anderer varchar(20)

Erfordern:

1. Die Quelltabelle enthält 1 Million Datensätze, von denen 500.000 doppelte Werte für „create_time“ und „item_name“ aufweisen.
2. Schreiben Sie die 500.000 deduplizierten Daten in die Zieltabelle.
3. Wenn mehrere Daten mit derselben Erstellungszeit und demselben Elementnamen vorhanden sind, können Sie beliebige davon ohne Regeleinschränkungen behalten.

• Experimentelle Umgebung

Virtuelle Linux-Maschine: CentOS Version 6.4; 8 GB physischer Speicher (MySQL mit 4 GB konfiguriert); 100 GB mechanische Festplatte; duale physische Dual-Core-CPU, insgesamt vier Prozessoren; MySQL 8.0.16.

• Erstellen Sie Testtabellen und Daten

-- Erstellen Sie eine Quelltabelle create table t_source 
(Artikel-ID int, 
 Erstellungszeit Datum/Uhrzeit, 
 Änderungszeit Datum/Uhrzeit, 
 Artikelname varchar (20), 
 anderer varchar(20) 
); 
- Erstellen Sie die Zieltabelle. Erstellen Sie die Tabelle t_target wie t_source. 
-- Generieren Sie 1 Million Testdaten, von denen 500.000 „created_time“ und „item_name“ doppelte Trennzeichen haben // 
Prozedur sp_generate_data() erstellen 
beginnen 
 setze @i := 1; 
 während @i<=500000 
 setze @created_time := date_add('2017-01-01',Intervall @i Sekunde); 
 setze @modifizierte_zeit := @erstellte_zeit; 
 setze @item_name := concat('a',@i); 
 in t_source einfügen 
 Werte (@i,@Erstellungszeit,@Änderungszeit,@Artikelname,'andere'); 
 setze @i:=@i+1; 
 Ende während; 
 begehen; 
 setze @last_insert_id := 500000; 
 in t_source einfügen 
 wähle item_id + @last_insert_id, 
 Erstellungszeit, 
 date_add(modifizierte_Zeit,Intervall @last_insert_id Sekunde), 
 Artikelname, 
 'andere' 
 von t_source; 
 begehen;
Ende 
// 
Trennzeichen ; 
rufen Sie sp_generate_data() auf; 

-- Die Quelltabelle hat keinen Primärschlüssel oder eindeutige Einschränkung und es können zwei identische Daten vorhanden sein. Fügen Sie daher einen weiteren Datensatz ein, um diese Situation zu simulieren.
in t_source einfügen, auswählen * aus t_source, wobei item_id=1;

 Es gibt 1.000.001 Datensätze in der Quelltabelle und die Zieltabelle sollte nach der Deduplizierung 500.000 Datensätze enthalten.
mysql> wähle count(*), count(unterschiedliche Erstellungszeit, Elementname) aus t_source;
+----------+----------------------------------------+
| Anzahl(*) | Anzahl(unterschiedliche Erstellungszeit, Artikelname) |
+----------+----------------------------------------+
| 1000001 | 500000 |
+----------+----------------------------------------+
1 Reihe im Satz (1,92 Sek.)

1. Clevere Verwendung von Indizes und Variablen

1. Vergleichstest ohne Index

(1) Verwendung korrelierter Unterabfragen

t_target abschneiden; 
in t_target einfügen 
wähle eindeutig t1.* aus t_source t1, wobei item_id in 
(Wählen Sie min(item_id) aus t_source t2, wobei t1.created_time=t2.created_time und t1.item_name=t2.item_name);

Es dauert lange, bis diese Anweisung Ergebnisse liefert. Schauen Sie sich also einfach den Ausführungsplan an.

mysql> erklären select distinct t1.* from t_source t1 where item_id in 
 -> (wählen Sie min(item_id) aus t_source t2, wobei t1.created_time=t2.created_time und t1.item_name=t2.item_name); 
+----+--------------------+-------+------------+------+---------------+---------+---------+---------+---------+---------+----------+----------+------------------------------+
| ID | Auswahltyp | Tabelle | Partitionen | Typ | mögliche Schlüssel | Schlüssel | Schlüssellänge | Ref. | Zeilen | gefiltert | Extra |
+----+--------------------+-------+------------+------+---------------+---------+---------+---------+---------+---------+----------+----------+------------------------------+
| 1 | PRIMARY | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 997282 | 100,00 | Mit Where; Mit Temporär |
| 2 | ABHÄNGIGE UNTERABFRAGE | t2 | NULL | ALLE | NULL | NULL | NULL | NULL | 997282 | 1,00 | Verwenden von „where“ |
+----+--------------------+-------+------------+------+---------------+---------+---------+---------+---------+---------+----------+----------+------------------------------+
2 Zeilen im Set, 3 Warnungen (0,00 Sek.)

Sowohl die Hauptabfrage als auch die zugehörige Unterabfrage scannen die gesamte Tabelle, was insgesamt das Scannen von 1 Million x 1 Million Datenzeilen erfordert. Kein Wunder, dass keine Ergebnisse geliefert werden.

(2) Verwenden von Tabellenverknüpfungen

t_target abschneiden; 
in t_target einfügen 
wähle eindeutig t1.* aus t_source t1, 
(Wählen Sie min(item_id) item_id,created_time,item_name aus der t_source-Gruppe nach created_time,item_name) t2 
wobei t1.item_id = t2.item_id;

Diese Methode dauert 14 Sekunden und der Abfrageplan sieht wie folgt aus:

mysql> erklären Sie „select distinct t1.* from t_source t1“, (select min(item_id) item_id,created_time,item_name from t_source group by created_time,item_name) t2, wobei t1.item_id = t2.item_id;
+----+----------+------------+------------+------+---------------+--------------+----------+---------+----------------+--------+----------+---------------+
| ID | Auswahltyp | Tabelle | Partitionen | Typ | mögliche Schlüssel | Schlüssel | Schlüssellänge | Ref. | Zeilen | gefiltert | Extra |
+----+----------+------------+------------+------+---------------+--------------+----------+---------+----------------+--------+----------+---------------+
| 1 | PRIMARY | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 997282 | 100,00 | Mit „where“; Mit „temporär“ |
| 1 | PRIMARY | <derived2> | NULL | ref | <auto_key0> | <auto_key0> | 5 | test.t1.item_id | 10 | 100,00 | Eindeutig |
| 2 | ABGELEITET | t_source | NULL | ALLE | NULL | NULL | NULL | NULL | 997282 | 100,00 | Temporäre | werden verwendet |
+----+----------+------------+------------+------+---------------+--------------+----------+----------------+--------+----------+---------------+
3 Zeilen im Satz, 1 Warnung (0,00 Sek.)

•Die innere Abfrage scannt 1 Million Zeilen der t_source-Tabelle, erstellt eine temporäre Tabelle, findet die minimale item_id nach der Deduplizierung und generiert die abgeleitete Tabelle derived2 mit 500.000 Zeilen.
•MySQL erstellt automatisch einen Index auto_key0 für das Feld item_id in der abgeleiteten Tabelle derived2.
•Die äußere Abfrage muss außerdem 1 Million Datenzeilen in der t_source-Tabelle scannen. Beim Verknüpfen mit der Exporttabelle wird für die item_id jeder Zeile in der t_source-Tabelle der Index auto_key0 verwendet, um die passende Zeile in der Exporttabelle zu finden, und der unterschiedliche Vorgang wird zu diesem Zeitpunkt optimiert. Nachdem die erste passende Zeile gefunden wurde, wird die Suche nach demselben Wert gestoppt.

(3) Verwendung von Variablen

setze @a:='1000-01-01 00:00:00'; 
setze @b:=' '; 
setze @f:=0; 
t_target abschneiden; 
in t_target einfügen 
Wählen Sie Artikel-ID, Erstellungszeit, Änderungszeit, Artikelname, Sonstiges 
 aus 
(wählen Sie t0.*, wenn (@a = Erstellungszeit und @b = Elementname, @f:=0, @f:=1) f, @a:= Erstellungszeit, @b:= Elementname 
 aus 
(Wählen Sie * aus t_Quelle, sortiert nach Erstellungszeit, Artikelname) t0) t1, wobei f=1;

Diese Methode dauert 13 Sekunden und der Abfrageplan sieht wie folgt aus:

mysql> erklären wählen Sie item_id,created_time,modified_time,item_name,other 
 -> von 
 -> (wähle t0.*, wenn (@a=Erstellungszeit und @b=Artikelname,@f:=0,@f:=1) f, @a:=Erstellungszeit,@b:=Artikelname 
 -> von 
 -> (Wählen Sie * aus t_source, sortiert nach Erstellungszeit, Artikelname) t0) t1, wobei f=1; 
+----+-------------+------------+------------+------+---------------+---------------+-------------+---------+---------+-----------+--------+-----------+----------------+
| ID | Auswahltyp | Tabelle | Partitionen | Typ | mögliche Schlüssel | Schlüssel | Schlüssellänge | Ref. | Zeilen | gefiltert | Extra |
+----+-------------+------------+------------+------+---------------+---------------+-------------+---------+---------+-----------+--------+-----------+----------------+
| 1 | PRIMARY | <derived2> | NULL | ref | <auto_key0> | <auto_key0> | 4 | const | 10 | 100,00 | NULL |
| 2 | ABGELEITET | <abgeleitet3> | NULL | ALLE | NULL | NULL | NULL | NULL | 997282 | 100,00 | NULL |
| 3 | ABGELEITET | t_source | NULL | ALLE | NULL | NULL | NULL | NULL | 997282 | 100,00 | Dateisortierung wird verwendet |
+----+-------------+------------+------------+------+---------------+---------------+-------------+---------+---------+-----------+--------+-----------+----------------+
3 Zeilen im Set, 5 Warnungen (0,00 Sek.)

•Die innerste Abfrage scannt 1 Million Zeilen der t_source-Tabelle und verwendet die Dateisortierung, um die abgeleitete Tabelle derived3 zu generieren.
•Die Abfrage der zweiten Ebene durchsucht 1 Million Zeilen von derived3, generiert die abgeleitete Tabelle derived2, schließt den Vergleich und die Zuweisung der Variablen ab und erstellt automatisch einen Index auto_key0 für die abgeleitete Spalte f.
•Die äußerste Ebene verwendet den Index auto_key0, um derived2 zu scannen und die deduplizierten Ergebniszeilen zu erhalten.

Im Vergleich zur obigen Methode 2 bleibt die Gesamtzahl der gescannten Zeilen unverändert und beträgt 2 Millionen Zeilen. Es gibt nur einen kleinen Unterschied. Dieses Mal liegt der automatisch generierte Index in der konstanten Spalte f, während der automatisch generierte Index für den Tabellenjoin in der Spalte item_id liegt. Die Abfragezeit ist also nahezu gleich.

Bisher haben wir keine Indizes für die Quelltabelle erstellt. Unabhängig davon, welche Schreibmethode verwendet wird, müssen die Felder created_time und item_name sortiert werden, um nach Duplikaten zu suchen. Daher ist es naheliegend anzunehmen, dass, wenn ein gemeinsamer Index für diese beiden Felder erstellt wird, die geordnete Natur des Indexes selbst verwendet werden kann, um zusätzliche Sortierungen zu vermeiden und so die Abfrageleistung zu verbessern.

2. Erstellen Sie einen gemeinsamen Indexvergleichstest für created_time und item_name

- Erstellen Sie einen gemeinsamen Index der Felder „created_time“ und „item_name“. Erstellen Sie den Index idx_sort für t_source(created_time,item_name,item_id); 
Tabelle t_source analysieren;

(1) Verwendung korrelierter Unterabfragen

t_target abschneiden; 
in t_target einfügen 
wähle different t1.* aus t_source t1, wobei item_id in 
(Wählen Sie min(item_id) aus t_source t2, wobei t1.created_time=t2.created_time und t1.item_name=t2.item_name);

Diesmal dauerte es 19 Sekunden und der Abfrageplan lautet wie folgt:

mysql> erklären select distinct t1.* from t_source t1 where item_id in 
 -> (wählen Sie min(item_id) aus t_source t2, wobei t1.created_time=t2.created_time und t1.item_name=t2.item_name); 
+----+--------------------+-------+------------+------+---------------+-----------+----------+--------+---------+----------------------------------------+--------+----------+--------------+------------------+
| ID | Auswahltyp | Tabelle | Partitionen | Typ | mögliche Schlüssel | Schlüssel | Schlüssellänge | Ref. | Zeilen | gefiltert | Extra |
+----+--------------------+-------+------------+------+---------------+-----------+----------+--------+---------+----------------------------------------+--------+----------+--------------+------------------+
| 1 | PRIMARY | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 997281 | 100,00 | Mit „where“; Mit „temporär“ |
| 2 | ABHÄNGIGE UNTERABFRAGE | t2 | NULL | ref | idx_sort | idx_sort | 89 | test.t1.created_time,test.t1.item_name | 2 | 100.00 | Index wird verwendet |
+----+--------------------+-------+------------+------+---------------+-----------+----------+--------+---------+----------------------------------------+--------+----------+--------------+------------------+
2 Zeilen im Set, 3 Warnungen (0,00 Sek.)

•Die t_source-Tabelle der äußeren Abfrage ist die treibende Tabelle und muss 1 Million Zeilen scannen.

•Für die item_id jeder Zeile in der Treibertabelle werden zwei Datenzeilen über den idx_sort-Index abgefragt.

(2) Verwenden von Tabellenverknüpfungen

t_target abschneiden; 
in t_target einfügen 
wähle eindeutig t1.* aus t_source t1, 
(Wählen Sie min(item_id) item_id,created_time,item_name aus der t_source-Gruppe nach created_time,item_name) t2 
wobei t1.item_id = t2.item_id;

Diesmal dauerte es 13 Sekunden und der Abfrageplan ist wie folgt:

mysql> erkläre select distinct t1.* from t_source t1, 
 -> (wähle min(item_id) item_id,created_time,item_name aus der t_source-Gruppe nach created_time,item_name) t2 
 -> wobei t1.item_id = t2.item_id; 
+----+----------+------------+------------+-------+---------------+---------------+------------+---------+-----------------+--------+----------+---------------+
| ID | Auswahltyp | Tabelle | Partitionen | Typ | mögliche Schlüssel | Schlüssel | Schlüssellänge | Ref. | Zeilen | gefiltert | Extra |
+----+----------+------------+------------+-------+---------------+---------------+------------+---------+-----------------+--------+----------+---------------+
| 1 | PRIMARY | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 997281 | 100,00 | Mit „where“; Mit „temporär“ |
| 1 | PRIMARY | <derived2> | NULL | ref | <auto_key0> | <auto_key0> | 5 | test.t1.item_id | 10 | 100,00 | Eindeutig |
| 2 | ABGELEITET | t_source | NULL | index | idx_sort | idx_sort | 94 | NULL | 997281 | 100.00 | Index wird verwendet |
+----+----------+------------+------------+-------+---------------+---------------+------------+---------+-----------------+--------+----------+---------------+
3 Zeilen im Satz, 1 Warnung (0,00 Sek.)

Im Vergleich zur Verwendung ohne Index müssen bei der Unterabfrage zwar 1 Million Datensatzzeilen gescannt werden, obwohl sie von einem vollständigen Tabellenscan zu einem vollständigen Indexscan wechselt. Daher ist die Verbesserung der Abfrageleistung nicht offensichtlich.

(3) Verwendung von Variablen

setze @a:='1000-01-01 00:00:00'; 
setze @b:=' '; 
setze @f:=0; 
t_target abschneiden; 
in t_target einfügen 
Wählen Sie Artikel-ID, Erstellungszeit, Änderungszeit, Artikelname, Sonstiges 
 aus 
(wählen Sie t0.*, wenn (@a = Erstellungszeit und @b = Elementname, @f:=0, @f:=1) f, @a:= Erstellungszeit, @b:= Elementname 
 aus 
(Wählen Sie * aus t_Quelle, sortiert nach Erstellungszeit, Artikelname) t0) t1, wobei f=1;

Dieses Mal dauerte es 13 Sekunden und der Abfrageplan war genau derselbe wie ohne Index. Es ist ersichtlich, dass der Index auf diese Schreibmethode keinen Einfluss hat. Ist es möglich, die Verschachtelung zu eliminieren und die Ergebnisse mit nur einer Ebene abzufragen?

(4) Verwenden Sie Variablen und vermeiden Sie verschachtelte Abfragen

setze @a:='1000-01-01 00:00:00'; 
setze @b:=' '; 
t_target abschneiden; 
in t_target einfügen 
wähle * aus t_source, erzwinge Index (idx_sort) 
 wobei (@a!=created_time oder @b!=item_name) und (@a:=created_time) nicht null ist und (@b:=item_name) nicht null ist 
 Sortieren nach Erstellungszeit, Artikelname;

Diesmal dauerte es 12 Sekunden und der Abfrageplan ist wie folgt:

mysql> erklären select * from t_source force index (idx_sort) 
 -> wobei (@a!=created_time oder @b!=item_name) und (@a:=created_time) nicht null ist und (@b:=item_name) nicht null ist 
 -> Sortierung nach Erstellungszeit, Artikelname;
+----+----------+----------+---------+-----------+---------------+-----------+----------+----------+---------+---------+----------+----------+-------------+
| ID | Auswahltyp | Tabelle | Partitionen | Typ | mögliche Schlüssel | Schlüssel | Schlüssellänge | Ref. | Zeilen | gefiltert | Extra |
+----+----------+----------+---------+-----------+---------------+-----------+----------+----------+---------+---------+----------+----------+-------------+
| 1 | SIMPLE | t_source | NULL | index | NULL | idx_sort | 94 | NULL | 997281 | 99.00 | Verwenden von „where“ |
+----+----------+----------+---------+-----------+---------------+-----------+----------+----------+---------+---------+----------+----------+-------------+
1 Zeile im Satz, 3 Warnungen (0,00 Sek.)

Diese Anweisung weist die folgenden Merkmale auf:

•Die verschachtelte Unterabfrage wird eliminiert und es ist nur ein vollständiger Indexscan der t_source-Tabelle erforderlich, sodass der Abfrageplan optimal ist.
• Sie müssen DISTINCT nicht erneut verwenden, um nach doppelten Inhalten zu suchen.
•Variablenbeurteilung und -zuweisung erscheinen nur in der Where-Klausel.
• Eliminieren Sie die Dateisortierung durch die Verwendung von Indizes.

Vor MySQL 8 war diese Anweisung die beste Lösung für die einfädige Deduplizierung. Eine sorgfältige Analyse dieser Anweisung zeigt, dass sie die logischen Abfrageverarbeitungsschritte und Indexfunktionen von SQL-Anweisungen geschickt nutzt. Die logischen Schritte einer SQL-Abfrage sind:

1. Führen Sie ein kartesisches Produkt durch (Cross Join)
2. ON-Filter anwenden (Joinbedingung)
3. Äußere Zeilen hinzufügen (Outer Join)
4. Wenden Sie den Where-Filter an
5. Gruppierung
6. Würfel oder Rollup auftragen
7. Wenden Sie den Filter an
8. Auswahllisten verarbeiten
9. Wenden Sie die differente Klausel an
10. Anwendung der Order-by-Klausel
11. Anwendung der Limit-Klausel

Die logischen Ausführungsschritte jeder Abfrageanweisung sind eine Teilmenge dieser 11 Schritte. Nehmen Sie diese Abfrageanweisung als Beispiel. Die Ausführungsreihenfolge ist: Suche nach Datenzeilen über den Index idx_sort erzwingen -> Where-Filter anwenden -> Auswahlliste verarbeiten -> Order-By-Klausel anwenden.

Damit die Variablen in der sortierten Reihenfolge von „created_time“ und „item_name“ zugewiesen und verglichen werden können, müssen die Datenzeilen in der Indexreihenfolge gefunden werden. Der Force-Index-Hinweis (idx_sort) spielt hier diese Rolle und muss auf diese Weise geschrieben werden, damit die gesamte Anweisung zur Duplikatsprüfung gültig ist. Andernfalls kann die Reihenfolge der Variablenzuweisung nicht garantiert und die Richtigkeit der Abfrageergebnisse nicht sichergestellt werden, da die Tabelle zuerst gescannt und dann sortiert wird. Die Order-By-Klausel kann ebenfalls nicht ignoriert werden. Andernfalls führt MySQL trotz der Force-Index-Eingabeaufforderung einen vollständigen Tabellenscan anstelle eines vollständigen Indexscans aus, was zu falschen Ergebnissen führt. Der Index stellt außerdem die Reihenfolge von Erstellungszeit und Elementname sicher und vermeidet so eine Dateisortierung. force index (idx_sort) und die Order-By-Klausel sind beide unverzichtbar. Der Index idx_sort ist hier genau richtig und schlägt zwei Fliegen mit einer Klappe.

Initialisieren Sie vor dem Start der Abfrageanweisung die Variable auf einen Wert, der in den Daten nicht vorkommen kann, und geben Sie dann die Where-Klausel ein, um von links nach rechts zu beurteilen. Vergleichen Sie zuerst die Variablen- und Feldwerte, weisen Sie dann der Variablen die Werte „created_time“ und „item_name“ dieser Zeile zu und verarbeiten Sie jede Zeile in der Reihenfolge „created_time“ und „item_name“. item_name ist ein Zeichenfolgentyp und (@b:=item_name) ist kein gültiger Boolescher Ausdruck. Daher sollte es als (@b:=item_name) ist nicht null geschrieben werden.

Abschließend möchte ich noch anmerken, dass die Schreibweise von " insert into t_target select * from t_source group by created_time,item_name ;" hier ignoriert wird, da diese durch “sql_mode='ONLY_FULL_GROUP_BY' " eingeschränkt ist.

2. Verwenden von Fensterfunktionen

Die neuen Fensterfunktionen in MySQL 8 vereinfachen den bislang lästigen Deduplizierungsvorgang erheblich.

t_target abschneiden; 
in t_target einfügen 
Wählen Sie item_id, Erstellungszeit, Änderungszeit, item_name, Sonstiges
 von (select *, row_number() über (Partition nach Erstellungszeit, Artikelname) als rn
 von t_source) t, wobei rn=1;

Die Ausführung dieser Anweisung dauert nur 12 Sekunden und der Text ist klar und leicht verständlich. Der Abfrageplan lautet wie folgt:

mysql> erklären select item_id, created_time, modified_time, item_name, other
 -> von (Auswahl *, Zeilennummer() über (Partition nach Erstellungszeit, Elementname) als rn
 -> von t_source) t, wobei rn=1;
+----+-------------+------------+------------+------+---------------+---------------+-------------+---------+---------+-----------+--------+-----------+----------------+
| ID | Auswahltyp | Tabelle | Partitionen | Typ | mögliche Schlüssel | Schlüssel | Schlüssellänge | Ref. | Zeilen | gefiltert | Extra |
+----+-------------+------------+------------+------+---------------+---------------+-------------+---------+---------+-----------+--------+-----------+----------------+
| 1 | PRIMARY | <derived2> | NULL | ref | <auto_key0> | <auto_key0> | 8 | const | 10 | 100,00 | NULL |
| 2 | ABGELEITET | t_source | NULL | ALLE | NULL | NULL | NULL | NULL | 997281 | 100,00 | Dateisortierung wird verwendet |
+----+-------------+------------+------------+------+---------------+---------------+-------------+---------+---------+-----------+--------+-----------+----------------+
2 Zeilen im Satz, 2 Warnungen (0,00 Sek.)

Diese Abfrage führt einen vollständigen Tabellenscan der Tabelle „t_source“ durch und verwendet Filesort, um die Tabelle nach den Partitionsfeldern „created_time“ und „item_name“ zu sortieren. Die äußere Abfrage behält ein Datenelement von jeder Partition bei. Da alle Daten mit wiederholter created_time und item_name beibehalten werden können, besteht keine Notwendigkeit, die Order-By-Klausel in OEVR zu verwenden.

Aus dem Ausführungsplan geht hervor, dass die Anweisung zur Deduplizierung der Fensterfunktion nicht so gut ist wie die Variablendeduplizierung verschachtelter Abfragen, aber tatsächlich ist diese Methode die schnellste.

Eine Beschreibung der MySQL-Fensterfunktionen finden Sie unter https://dev.mysql.com/doc/refman/8.0/en/window-functions.html.

3. Parallele Ausführung mit mehreren Threads

Die einzelne Anweisung zur Duplikatsprüfung wurde auf den optimalen Zustand eingestellt, wird jedoch immer noch einfädig ausgeführt. Können wir mehrere Prozessoren nutzen, um Deduplizierungsvorgänge mithilfe mehrerer Threads parallel auszuführen und so die Geschwindigkeit weiter zu erhöhen? Meine experimentelle Umgebung besteht beispielsweise aus 4 Prozessoren. Wenn ich 4 Threads gleichzeitig verwende, um das SQL zur Duplikatsprüfung auszuführen, sollte sich die Leistung theoretisch um fast das Vierfache verbessern.

1. Daten-Sharding

Beim Generieren von Testdaten verwendet created_time die Methode, jedem Datensatz eine Sekunde hinzuzufügen, dh der maximale und minimale Zeitunterschied beträgt 500.000 Sekunden und die Daten sind gleichmäßig verteilt, sodass die Daten zunächst in vier Teile aufgeteilt werden.

(1) Fragen Sie die erstellten Zeitgrenzwerte der vier Datensätze ab

mysql> select date_add('2017-01-01',Intervall 125000 Sekunden) dt1,
 -> date_add('2017-01-01',Intervall 2*125000 Sekunden) dt2,
 -> date_add('2017-01-01',Intervall 3*125000 Sekunden) dt3,
 -> max(Erstellungszeit) dt4
 -> von t_source;
+---------------------+---------------------+---------------------+---------------------+
| dt1 | dt2 | dt3 | dt4 |
+---------------------+---------------------+---------------------+---------------------+
| 02.01.2017 10:43:20 | 03.01.2017 21:26:40 | 05.01.2017 08:10:00 | 06.01.2017 18:53:20 |
+---------------------+---------------------+---------------------+---------------------+
1 Zeile im Satz (0,00 Sek.)

(2) Überprüfen Sie die Anzahl der Datensätze in jedem Datensatz, um sicherzustellen, dass die Daten gleichmäßig verteilt sind

mysql> Fall auswählen, wenn Erstellungszeit >= '2017-01-01' 
 -> und Erstellungszeit < '2017-01-02 10:43:20'
 -> dann '2017-01-01'
 -> wann Erstellungszeit >= '2017-01-02 10:43:20'
 -> und Erstellungszeit < '2017-01-03 21:26:40'
 -> dann '2017-01-02 10:43:20'
 -> wann Erstellungszeit >= '2017-01-03 21:26:40' 
 -> und Erstellungszeit < '2017-01-05 08:10:00'
 -> dann '2017-01-03 21:26:40' 
 -> sonst '2017-01-05 08:10:00'
 -> Ende min_dt,
 -> Fall, wenn Erstellungszeit >= '2017-01-01' 
 -> und Erstellungszeit < '2017-01-02 10:43:20'
 -> dann '2017-01-02 10:43:20'
 -> wann Erstellungszeit >= '2017-01-02 10:43:20'
 -> und Erstellungszeit < '2017-01-03 21:26:40'
 -> dann '2017-01-03 21:26:40'
 -> wann Erstellungszeit >= '2017-01-03 21:26:40' 
 -> und Erstellungszeit < '2017-01-05 08:10:00'
 -> dann '2017-01-05 08:10:00'
 -> sonst '2017-01-06 18:53:20'
 -> Ende max_dt,
 -> zählen(*)
 -> von t_source
 -> nach Fall gruppieren, wenn Erstellungszeit >= '2017-01-01' 
 -> und Erstellungszeit < '2017-01-02 10:43:20'
 -> dann '2017-01-01'
 -> wann Erstellungszeit >= '2017-01-02 10:43:20'
 -> und Erstellungszeit < '2017-01-03 21:26:40'
 -> dann '2017-01-02 10:43:20'
 -> wann Erstellungszeit >= '2017-01-03 21:26:40' 
 -> und Erstellungszeit < '2017-01-05 08:10:00'
 -> dann '2017-01-03 21:26:40' 
 -> sonst '2017-01-05 08:10:00'
 -> Ende,
 -> Fall, wenn Erstellungszeit >= '2017-01-01' 
 -> und Erstellungszeit < '2017-01-02 10:43:20'
 -> dann '2017-01-02 10:43:20'
 -> wann Erstellungszeit >= '2017-01-02 10:43:20'
 -> und Erstellungszeit < '2017-01-03 21:26:40'
 -> dann '2017-01-03 21:26:40'
 -> wann Erstellungszeit >= '2017-01-03 21:26:40' 
 -> und Erstellungszeit < '2017-01-05 08:10:00'
 -> dann '2017-01-05 08:10:00'
 -> sonst '2017-01-06 18:53:20'
 -> Ende;
+---------------------+---------+----------+
| min_dt | max_dt | Anzahl(*) |
+---------------------+---------+----------+
| 01.01.2017 | 02.01.2017 10:43:20 | 249999 |
| 02.01.2017 10:43:20 | 03.01.2017 21:26:40 | 250000 |
| 03.01.2017 21:26:40 | 05.01.2017 08:10:00 | 250000 |
| 05.01.2017 08:10:00 | 06.01.2017 18:53:20 | 250002 |
+---------------------+---------+----------+
4 Reihen im Satz (4,86 Sek.)

Die Vereinigung der vier Datensätze sollte den gesamten Quelldatensatz abdecken und es sollte keine Duplikate zwischen den Daten geben. Das heißt, die Erstellungszeit der vier Datenkopien muss kontinuierlich und gegenseitig ausschließend sein. Kontinuität stellt sicher, dass alle Daten verarbeitet werden, und gegenseitiger Ausschluss stellt sicher, dass keine zweite Duplikatsprüfung erforderlich ist. Tatsächlich ähnelt dies dem Konzept der Zeitbereichspartitionierung. Vielleicht ist es besser, eine partitionierte Tabelle zu verwenden, aber der Schritt zum erneuten Erstellen der Tabelle wird hier ausgelassen.

2. Erstellen Sie eine gespeicherte Prozedur zur Überprüfung auf doppelte Inhalte

Mit den obigen Informationen können wir 4 Anweisungen schreiben, um alle Daten zu verarbeiten. Um die aufrufende Schnittstelle so einfach wie möglich zu gestalten, erstellen Sie die folgende gespeicherte Prozedur.

Trennzeichen //
Prozedur sp_unique(i smallint) erstellen 
beginnen 
 setze @a:='1000-01-01 00:00:00'; 
 setze @b:=' '; 
 wenn (i<4) dann
 in t_target einfügen 
 wähle * aus t_source, erzwinge Index (idx_sort) 
  wobei Erstellungszeit >= Datum_Hinzufügen('2017-01-01',Intervall (i-1)*125000 Sekunden) 
  und Erstellungszeit < Datum_Hinzufügen('2017-01-01',Intervall i*125000 Sekunden) 
  und (@a!=Erstellungszeit oder @b!=Artikelname) 
  und (@a:=created_time) ist nicht null 
  und (@b:=item_name) ist nicht null 
  Sortieren nach Erstellungszeit, Artikelname; 
 anders 
 in t_target einfügen 
 wähle * aus t_source, erzwinge Index (idx_sort) 
  wobei Erstellungszeit >= Datumsadd('2017-01-01',Intervall (i-1)*125000 Sekunden) 
  und Erstellungszeit <= Datum_Hinzufügen('2017-01-01',Intervall i*125000 Sekunden) 
  und (@a!=Erstellungszeit oder @b!=Artikelname) 
  und (@a:=created_time) ist nicht null 
  und (@b:=item_name) ist nicht null 
  Sortieren nach Erstellungszeit, Artikelname; 
 Ende wenn; 
Ende 
//

Der Ausführungsplan der Abfrageanweisung lautet wie folgt:

mysql> erklären select * from t_source force index (idx_sort) 
 -> wobei Erstellungszeit >= Datum_Hinzufügen('2017-01-01',Intervall (1-1)*125000 Sekunden) 
 -> und Erstellungszeit < date_add('2017-01-01',Intervall 1*125000 Sekunden) 
 -> und (@a!=Erstellungszeit oder @b!=Artikelname) 
 -> und (@a:=created_time) ist nicht null 
 -> und (@b:=item_name) ist nicht null 
 -> Sortierung nach Erstellungszeit, Artikelname; 
+----+----------+----------+---------+-----------+---------------+-----------+----------+---------+---------+---------+----------+----------+----------+----------------------+
| ID | Auswahltyp | Tabelle | Partitionen | Typ | mögliche Schlüssel | Schlüssel | Schlüssellänge | Ref. | Zeilen | gefiltert | Extra |
+----+----------+----------+---------+-----------+---------------+-----------+----------+---------+---------+---------+----------+----------+----------+----------------------+
| 1 | SIMPLE | t_source | NULL | Bereich | idx_sort | idx_sort | 6 | NULL | 498640 | 100,00 | Indexbedingung wird verwendet |
+----+----------+----------+---------+-----------+---------------+-----------+----------+---------+---------+---------+----------+----------+----------+-----------------------+
1 Zeile im Satz, 3 Warnungen (0,00 Sek.)

Der MySQL-Optimierer führt Indexbereichsscans durch und verwendet Index Condition Pushdown (ICP), um Abfragen zu optimieren.

3. Parallele Ausführung

Im Folgenden werden Shell-Hintergrundprozesse und MySQL Schedule Event verwendet, um Parallelität zu erreichen.

(1) Shell-Hintergrundprozess

•Erstellen Sie die Datei duplicate_removal.sh mit folgendem Inhalt:

#!/bin/bash
mysql -vvv -u root -p123456 test -e "truncate t_target" &>/dev/null 
Datum '+%H:%M:%S'
für y in {1..4}
Tun
 sql="rufe sp_unique($y) auf"
 mysql -vvv -u root -p123456 test -e "$sql" &>par_sql1_$y.log &
Erledigt
Warten
Datum '+%H:%M:%S'

• Ausführen von Skriptdateien

./duplicate_removal.sh

Die Ausführungsausgabe ist wie folgt:

[mysql@hdp2~]$./duplicate_removal.sh
14:27:30
14:27:35

Diese Methode dauert 5 Sekunden und die 4 parallel ausgeführten Prozessaufrufe dauern jeweils 4,87 Sekunden, 4,88 Sekunden, 4,91 Sekunden und 4,73 Sekunden:

[mysql@hdp2~]$cat par_sql1_1.log | sed '/^$/d'
mysql: [Warnung] Die Verwendung eines Passworts in der Befehlszeilenschnittstelle kann unsicher sein.
--------------
rufe sp_unique(1) auf
--------------
Abfrage OK, 124999 Zeilen betroffen (4,87 Sek.)
Tschüss
[mysql@hdp2~]$cat par_sql1_2.log | sed '/^$/d'
mysql: [Warnung] Die Verwendung eines Passworts in der Befehlszeilenschnittstelle kann unsicher sein.
--------------
rufe sp_unique(2) auf
--------------
Abfrage OK, 125.000 Zeilen betroffen (4,88 Sek.)
Tschüss
[mysql@hdp2~]$cat par_sql1_3.log | sed '/^$/d'
mysql: [Warnung] Die Verwendung eines Passworts in der Befehlszeilenschnittstelle kann unsicher sein.
--------------
rufe sp_unique(3) auf
--------------
Abfrage OK, 125.000 Zeilen betroffen (4,91 Sek.)
Tschüss
[mysql@hdp2~]$cat par_sql1_4.log | sed '/^$/d'
mysql: [Warnung] Die Verwendung eines Passworts in der Befehlszeilenschnittstelle kann unsicher sein.
--------------
rufe sp_unique(4) auf
--------------
Abfrage OK, 125001 Zeilen betroffen (4,73 Sek.)
Tschüss
[mysql@hdp2~]$

Es ist ersichtlich, dass die Ausführungszeit jedes Prozesses 4,85 Sekunden beträgt. Da er parallel ausgeführt wird, beträgt die Gesamtausführungszeit des Prozesses die langsamste von 4,91 Sekunden, was 2,5-mal schneller ist als die Single-Thread-Geschwindigkeit.

(2) MySQL-Schedule-Ereignis

• Ereignisverlaufsprotokolltabelle erstellen

-- Wird verwendet, um die Ausführungszeit des Ereignisses und andere Informationen anzuzeigen. Erstellen Sie die Tabelle t_event_history ( 
 dbname varchar(128) nicht null Standard '', 
 eventname varchar(128) nicht null Standard '', 
 Startzeit datetime(3) ungleich null Standard '1000-01-01 00:00:00', 
 Endzeit datetime(3) Standard null, 
 issuccess int(11) Standard null, 
 Dauer int(11) Standard null, 
 Fehlermeldung varchar(512) Standard null, 
 randno int(11) Standard null
);

• Erstellen Sie ein Ereignis für jeden gleichzeitigen Thread

Trennzeichen //
Ereignis ev1 nach Zeitplan erstellen bei current_timestamp + Intervall 1 Stunde nach Abschluss beibehalten deaktivieren do 
beginnen
 Deklarieren Sie r_code char(5) als Standard „00000“; 
 Deklariere r_msg-Text; 
 Deklarieren Sie v_error als Ganzzahl; 
 Deklariere v_starttime datetime jetzt als Standard (3); 
 Deklarieren Sie v_randno als ganzzahligen Standard-Floor (rand()*100001); 
 einfügen in t_event_history (Datenbankname, Ereignisname, Startzeit, Zufallszahl) 
 #Jobnamewerte(Datenbank(),'ev1', v_Startzeit,v_Randnummer); 
 beginnen 
 #Abschnitt zur Ausnahmebehandlung deklariert den Continue-Handler für SQLexeption 
 beginnen 
  setze v_error = 1; 
  Diagnosezustand 1 abrufen, r_code = returned_sqlstate, r_msg = message_text; 
 Ende; 
 #Dies ist die eigentliche Benutzerprogrammprozedur mit dem Namen call sp_unique(1); 
 Ende; 
 Aktualisiere t_event_history, setze Endzeit=jetzt(3),issuccess=isnull(v_error),Dauer=Zeitstempeldiff(Mikrosekunde,Startzeit,jetzt(3)),Fehlernachricht=concat('Fehler=',r_code,',Nachricht=',r_msg),randno=null, wobei Startzeit=v_starttime und Randno=v_randno; 
Ende
// 
erstelle Ereignis ev2 nach Zeitplan bei current_timestamp + Intervall 1 Stunde nach Abschluss beibehalten deaktivieren do 
beginnen
 Deklarieren Sie r_code char(5) als Standard „00000“; 
 Deklariere r_msg-Text; 
 Deklarieren Sie v_error als Ganzzahl; 
 Deklariere v_starttime datetime jetzt als Standard (3); 
 Deklarieren Sie v_randno als ganzzahligen Standard-Floor (rand()*100001); 
 einfügen in t_event_history (Datenbankname, Ereignisname, Startzeit, Zufallszahl) 
 #Jobnamewerte(Datenbank(),'ev2', v_Startzeit, v_Randnummer); 
 beginnen 
 #Abschnitt zur Ausnahmebehandlung deklariert den Continue-Handler für SQLexeption 
 beginnen 
  setze v_error = 1; 
  Diagnosezustand 1 abrufen, r_code = returned_sqlstate, r_msg = message_text; 
 Ende; 
 #Dies ist die eigentliche Benutzerprogrammprozedur mit dem Namen call sp_unique(2); 
 Ende; 
 Aktualisiere t_event_history, setze Endzeit=jetzt(3),issuccess=isnull(v_error),Dauer=Zeitstempeldiff(Mikrosekunde,Startzeit,jetzt(3)),Fehlernachricht=concat('Fehler=',r_code,',Nachricht=',r_msg),randno=null, wobei Startzeit=v_starttime und Randno=v_randno; 
Ende
// 
erstelle Ereignis ev3 nach Zeitplan bei current_timestamp + Intervall 1 Stunde nach Abschluss beibehalten deaktivieren do 
beginnen
 Deklarieren Sie r_code char(5) als Standard „00000“; 
 Deklariere r_msg-Text; 
 Deklarieren Sie v_error als Ganzzahl; 
 Deklariere v_starttime datetime jetzt als Standard (3); 
 Deklarieren Sie v_randno als ganzzahligen Standard-Floor (rand()*100001); 
 einfügen in t_event_history (Datenbankname, Ereignisname, Startzeit, Zufallszahl) 
 #Jobnamewerte(Datenbank(),'ev3', v_Startzeit,v_Randnummer); 
 beginnen 
 #Abschnitt zur Ausnahmebehandlung deklariert den Continue-Handler für SQLexeption 
 beginnen 
  setze v_error = 1; 
  Diagnosezustand 1 abrufen, r_code = returned_sqlstate, r_msg = message_text; 
 Ende; 
 #Dies ist die eigentliche Benutzerprogrammprozedur mit dem Namen call sp_unique(3); 
 Ende; 
 Aktualisiere t_event_history, setze Endzeit=jetzt(3),issuccess=isnull(v_error),Dauer=Zeitstempeldiff(Mikrosekunde,Startzeit,jetzt(3)),Fehlernachricht=concat('Fehler=',r_code,',Nachricht=',r_msg),randno=null, wobei Startzeit=v_starttime und Randno=v_randno; 
Ende
// 
Ereignis ev4 nach Zeitplan erstellen bei current_timestamp + Intervall 1 Stunde nach Abschluss beibehalten deaktivieren do 
beginnen
 Deklarieren Sie r_code char(5) als Standard „00000“; 
 Deklariere r_msg-Text; 
 Deklarieren Sie v_error als Ganzzahl; 
 Deklariere v_starttime datetime jetzt als Standard (3); 
 Deklarieren Sie v_randno als ganzzahligen Standardwert für floor(rand()*100001); 
 einfügen in t_event_history (Datenbankname, Ereignisname, Startzeit, Zufallszahl) 
 #Jobnamewerte(Datenbank(),'ev4', v_Startzeit, v_Randnummer); 
 beginnen 
 #Abschnitt zur Ausnahmebehandlung deklariert den Continue-Handler für SQLexeption 
 beginnen 
  setze v_error = 1; 
  Diagnosezustand 1 abrufen, r_code = returned_sqlstate, r_msg = message_text; 
 Ende; 
 #Dies ist die eigentliche Benutzerprogrammprozedur mit dem Namen call sp_unique(4); 
 Ende; 
 Aktualisiere t_event_history, setze Endzeit=jetzt(3),issuccess=isnull(v_error),Dauer=Zeitstempeldiff(Mikrosekunde,Startzeit,jetzt(3)),Fehlernachricht=concat('Fehler=',r_code,',Nachricht=',r_msg),randno=null, wobei Startzeit=v_starttime und Randno=v_randno; 
Ende
//

Um die Ausführungszeit jedes Ereignisses aufzuzeichnen, wird der Ereignisdefinition die Logik der Operationsprotokolltabelle hinzugefügt. Da in jedem Ereignis nur ein Einfügen und ein Aktualisieren ausgeführt werden, werden für die 4 Ereignisse insgesamt 8 sehr einfache Anweisungen ausgeführt, und die Auswirkungen auf den Test sind vernachlässigbar. Die Ausführungszeit ist auf Millisekunden genau.

• Ausführung von Triggerereignissen

mysql -vvv -u root -p123456 test -e "t_target abschneiden; Ereignis ev1 planmäßig am aktuellen Zeitstempel ändern aktivieren; Ereignis ev2 planmäßig am aktuellen Zeitstempel ändern aktivieren; Ereignis ev3 planmäßig am aktuellen Zeitstempel ändern aktivieren; Ereignis ev4 planmäßig am aktuellen Zeitstempel ändern aktivieren;"

Diese Befehlszeile löst vier Ereignisse nacheinander aus, wartet jedoch nicht, bis das vorherige abgeschlossen ist, bevor das nächste ausgeführt wird. Stattdessen wird das nächste sofort ausgeführt. Dies ist aus der Ausgabe des Befehls deutlich ersichtlich:

[mysql@hdp2~]$mysql -vvv -u root -p123456 test -e "truncate t_target;ändere Ereignis ev1 planmäßig am aktuellen Zeitstempel, aktivieren;ändere Ereignis ev2 planmäßig am aktuellen Zeitstempel, aktivieren;ändere Ereignis ev3 planmäßig am aktuellen Zeitstempel, aktivieren;ändere Ereignis ev4 planmäßig am aktuellen Zeitstempel, aktivieren;"
mysql: [Warnung] Die Verwendung eines Passworts in der Befehlszeilenschnittstelle kann unsicher sein.
--------------
t_target abschneiden
--------------
Abfrage OK, 0 Zeilen betroffen (0,06 Sek.)
--------------
Ändern Sie das Ereignis ev1 planmäßig um current_timestamp, aktivieren Sie
--------------
Abfrage OK, 0 Zeilen betroffen (0,02 Sek.)
--------------
Ereignis ev2 planmäßig um current_timestamp ändern aktivieren
--------------
Abfrage OK, 0 Zeilen betroffen (0,00 Sek.)
--------------
Ereignis ev3 planmäßig um current_timestamp ändern aktivieren
--------------
Abfrage OK, 0 Zeilen betroffen (0,02 Sek.)
--------------
Ereignis ev4 planmäßig am aktuellen Zeitstempel ändern, aktivieren
--------------
Abfrage OK, 0 Zeilen betroffen (0,00 Sek.)
Tschüss
[mysql@hdp2~]$

• Ereignisausführungsprotokoll anzeigen

mysql> wähle * aus test.t_event_history;
+--------+-----------+-------------------------+-------------------------+-----------+---------+-----------+--------------+--------+
|
+--------+-----------+-------------------------+-----------------------------------------------+----------+--------------+--------+
|
|
|
|
+--------+-----------+-------------------------+-----------------------------------------------+----------+--------------+--------+
4 Zeilen im Satz (0,00 Sek.)

Es ist zu erkennen, dass jeder Prozess 4,83 Sekunden dauert, da sie parallel ausgeführt werden, die Gesamtausführungszeit ist die langsamste 5,3 Sekunden.

Zusammenfassen

Die oben genannte Betriebsmethode, die ich Ihnen vorgestellt habe, um den MySQL -Deduplizierungsvorgang auf das Extrem zu optimieren. Ich möchte auch allen für ihre Unterstützung der Website 123WORDPRESS.COM danken!
Wenn Sie diesen Artikel hilfreich finden, können Sie ihn gerne abdrucken und dabei bitte die Quelle angeben. Vielen Dank!

Das könnte Sie auch interessieren:
  • Zusammenfassung der drei Deduplizierungsmethoden in SQL
  • Detailliertes Beispiel zur Verwendung der distinct-Methode in MySQL
  • Eine einfache Methode zum Zusammenführen und Entfernen doppelter MySQL-Tabellen
  • MySQL-Deduplizierungsmethoden
  • Detaillierte Erklärung zweier Methoden zur Deduplizierung in MySQL und Beispielcode
  • SQL-Lernhinweise 5: So entfernen Sie Duplikate und weisen neu hinzugefügten Feldern Werte zu
  • Zusammenfassung der SQL-Deduplizierungsmethoden

<<:  So ändern Sie den Hostnamen in Linux

>>:  Vue realisiert Click-Flip-Effekt

Artikel empfehlen

Eine detaillierte Einführung in den Lade- und Analyseprozess von HTML-Seiten

Die Reihenfolge, in der der Browser HTML lädt und ...

Allgemeine Linux-Befehle chmod zum Ändern der Dateiberechtigungen 777 und 754

Der folgende Befehl wird häufig verwendet: chmod ...

js implementiert einen einfachen Rechner

Verwenden Sie natives JS, um einen einfachen Rech...

So installieren Sie Nginx an einem bestimmten Ort im Centos-System

Wie installiere ich Nginx an einem bestimmten Ort...

Welche Codes sollte ich beherrschen, wenn ich Webdesign lerne?

In diesem Artikel werden einige der Techniken ausf...

Ursachen und Lösungen für MySQL-Deadlocks

Die Datenbank ist wie das Betriebssystem eine gem...

Grafisches Tutorial zur Installation und Konfiguration von MySQL 8.0.19

In diesem Artikel wird das grafische Tutorial zur...

So fragen Sie den minimal verfügbaren ID-Wert in der MySQL-Tabelle ab

Als ich mir heute die Laborprojekte ansah, stieß ...

So ändern Sie das MySQL-Datenbankdateiverzeichnis in Ubuntu

Vorwort Der Ubuntu-Server des Unternehmens platzi...

So beschränken Sie das Eingabefeld in HTML auf die Eingabe reiner Zahlen

Beschränken Sie input Eingabefeld auf reine Zahle...

So zählen Sie die Anzahl bestimmter Zeichen in einer Datei unter Linux

Das Zählen der Nummer einer Zeichenfolge in einer...