Ein bißchen Nerdstuff am Donnerstag:
Hmm... Eigenartig, dass sie als Windows-Familie auf dem Bett ein MacBook benutzen.
Was macht das Programm?
Es werden also kontinuierlich SQL-Statements abgesetzt.
41 Minuten und 36 Sekunden
Was macht das Programm?
Das Programm erzeugt also zuerst 2 Dateien und greift erst zum Schluss auf die Datenbank zu.
30 Minuten und 9 Sekunden
Wenn wir annehmen, dass die 2 CSV-Dateien schon vorhanden sind und nur noch die LOAD DATA INFILE-Befehle ausgeführt werden müssen, so sind die immerhin 2.218.540 Datensätze innerhalb von
2 Minuten und 23 Sekunden
importiert.
Prepared Statements sollten nicht nur aus sicherheitsrelevanten Überlegungen eingesetzt werden, sondern bringen - richtig implementiert - auch einen Performance-Vorteil. Dieser Vorteil lässt sich so erklären, dass 1x das SQL-Statement mit "?" als Platzhalter an das DBMS übertragen wird und danach werden für jeden Datensatz nur noch die Parameter übertragen, die in die entsprechenden Platzhalter durch das DBMS eingefügt werden. Es fallen also weniger zu übertragene Daten an, aber vor allem können die Daten so übertragen und verarbeitet werden, dass sie nicht mehr durch das DB-System interpretiert bzw. konvertiert werden müssen. Das Ergebnis kann sich sehen lassen:
Das Programm ist identisch mit dem Programm, welches kontinuierlich, einzelne SQL-Statements absetzt. Allerdings ist es so programmiert, dass es die Performance-Vorteile der Prepared Statements nutzt.
28 Minuten und 30 Sekunden
Bei so großen Datenmengen, die sequentiell eingelesen werden, kommt es auf die richtige Wahl der Methode an:
Vorteile Prepared Statements:
Nachteile Prepared Statements:
Vorteile Generierung von CSV & LOAD DATA INFILE:
Nachteile Generierung von CSV & LOAD DATA INFILE:
Anstatt Millionen von INSERT oder UPDATE-Befehlen an die Datenbank zu schicken, um eine Tabelle zu aktualisieren, bietet es sich bei großen Datenmengen an, eine CSV-Datei zu erstellen, welche dann über LOAD DATA INFILE mit extrem hoher Geschwindigkeit eingelesen wird.
In Java bietet sich zum Erstellen von CSV-Dateien die Klasse Super CSV an. Um eine Datei zu erzeugen, benötigt man als erstes einen CSVMapWriter:
CsvMapWriter writer = new CsvMapWriter(new OutputStreamWriter(new FileOutputStream("C:\meineDatei.csv", true),"UTF-8"), CsvPreference.EXCEL_PREFERENCE);
Anstatt mit einem OutputStreamWriter zu arbeiten, kann man auch einen FileWriter verwenden. Da man bei letzterem aber kein Encoding ("UTF-8") angeben kann und in der Datenbank die Zeichen UTF-8-kodiert sind, müssen wir den Stream verwenden.
Interessant ist der zweite Parameter des FileOutputStream, der angibt, dass die Daten, die eingefügt werden, angehängt werden und nicht vorher gelöscht werden.
Die Einstellung CsvPreference.EXCEL_PREFERENCE legt fest, welche Trennzeichen in der CSV-Datei verwendet werden - wenn man andere Trennzeichen verwenden möchte, muss der später behandelte LOAD DATA INFILE-Befehl angepasst werden.
Als nächstes werden die Textfelder für die CSV-Datei definiert:
final String[] header = new String[] { "id", "name", "adressfeld" };
Unsere Datensätze bestehen also aus 3 Feldern.
Um die CSV-Datei mit Daten zu füllen sind folgende Zeilen notwendig:
final HashMap<String, ? super Object> data = new HashMap<String, Object>();
data.put(header[0], this.id);
data.put(header[1], this.name);
this.adressfeld = this.adressfeld.replace("\\", "\\\\");
data.put(header[2], this.adressfeld);
Die Daten werden in einer HashMap gespeichert und per data.put eingefügt. Was mich etwas überraschte war, dass man das Backslash maskieren muss, ansonsten geht es in der Verarbeitung verloren. Eigenartig, weil man diese Funktionalität sicherlich problemlos in Super CSV hätte integrieren können.
Abschließend werden die Daten dem CSVMapWriter übergeben und dieser wieder geschlossen:
writer.write(data, header); writer.close();
Obiges Beispiel ist für einen einzelnen Datensatz, der in einer CSV-Datei gespeichert wird. Wenn man mehrere Datensätze einfügen möchte, müssen sämtliche Schritte (Anlegen eines CSVMapWriter, Daten vorbereiten, Daten einfügen) für jeden Datensatz wiederholt werden.
Den CSVMapWriter 1x anzulegen und am Ende per .close zu schließen, funktioniert nicht - meine Versuche mit der Super CSV-Klasse haben gezeigt, dass dann Datensätze verloren gehen bzw. abgeschnitten werden.
Um die so erstellte Datei mit LOAD DATA INFILE in die Datenbank zu importieren, muss folgender Befehl eingetragen werden:
LOAD DATA [LOCAL] INFILE 'C:\\meineDatei.csv' [REPLACE] INTO TABLE meine_tabelle FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' LINES TERMINATED BY '\n' (id, name, adressfeld);
Vorab: Der Befehl wird so nicht funktionieren, da die Bestandteile innerhalb der eckigen Klammern optional sind:
Alle/Weitere Optionen zu dem LOAD DATA INFILE-Befehl gibt es im MySQL-Handbuch.
Ein Performancevergleich ist hier zu finden.
Ich bin ein Gänseblümchen im Sonnenschein
und durch meine Blüte fließt die Sonne in mich rein
Ich bin ein Gänseblümchen und mir wird ganz warm
ich könnt die ganze Welt und dann mich selbst umarmen.
Ich bin ein Gänseblümchen ohne Aggression
Wut, Ärger - was bringt das schon?
Durchatmen, Freakcommander! Momentan gehst du durch die harte Schule einer dir immer noch relativ unbekannten Sprache. Da fällt man mal hin und muss wieder aufstehen, ansonsten wird das nichts.
Aber man muss zugegeben, dass der heutige Fehler nicht ganz so leicht zu finden war:
In der Methode .getFromDB(), die einfach nur per DB-Query Daten einliest, gab es folgende 3 Zeilen Code, die Grund für das Memory Leak waren:
this.setM_swap(rs.getInt(16)); this.setD_total(rs.getInt(17)); this.setD_free(rs.getInt(18));
Aus dem ResultSet der DB-Abfrage werden per Setter-Methoden die Attribute gesetzt. Das Problem? Da die Java-Datentypen und die DB-Felder für m_swap, d_total und d_free mit Integer bzw. int nicht ausreichend lang waren, habe ich sie nachträglich auf long bzw. Bigint (DB) geändert. Dabei habe ich auch die setter wie setM_swap() so geändert, dass sie als Parameter long erwarten. Normalerweise schreit ja gleich das IDE los, wenn ein Typ nicht passt, aber Java findet (und in diesem Fall völlig zu Recht), dass Methoden, die long als Parameter erwarten, auch mit int gefüttert werden können und hat deswegen nicht gemeckert.
Das Memory Leak kann ich mir nur so erklären, dass mit rs.getInt auf eine Spalte zugegriffen wurde, welche in der Datenbank als BigInteger definiert ist.
Richtigerweise hätte man mit rs.getLong() arbeiten müssen, dann gibt es auch keine Probleme mit dem Speicher. ;)
Das Leben ist hart, grausam und gemein! Und heute ist es noch ein bißchen fieser als sonst.
Ich wage mich gerade in die Tiefen von Java vor. Zwar hatte ich in der Uni etliche Kurse zur Java-Programmierung, aber man ist erst in einer Programmiersprache richtig drin, wenn man etwas größeres damit erstellen muss. Das Problem ist nur, dass man immer wieder an seine sprachlichen Grenzen stößt, viel nachschlagen muss und sich auch mit der Komplilierung & Ausführung von Java nicht wirklich auskennt. Theoretisch ist alles klar, aber das Praktische ist meist etwas komplizierter als gedacht.
Nachdem ich die letzten Tage etliche Zeilen Java-Code geschrieben hatte, wollte ich mich heute ran setzen und das Ganze von meinem lokalen Eclipse auf den Server bringen. Ich machte mir ein wenig Sorgen, ob ich die DB-Verbindung auf einer Linux-Maschine zum Laufen kriegen würde, zudem nutzte ich ein paar externe Java-Bibliotheken (jar), deren Pfade ja schließlich auch angepasst werden mussten.
ResultSet rs = stmt.executeQuery("SELECT idprojects FROM projects WHERE download_url IS NOT NULL AND short_name IS NOT NULL AND (status != 1 OR status != 6)");
while (rs.next()){
Project pro = new Project(rs.getInt(1));
pro.update();
}Ich hatte also den ganzen Tag damit zugebracht, einen Fehler zu finden, den es gar nicht gibt.