Datenbank für 9GB Text

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MariaDB/MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
rman
User
Beiträge: 8
Registriert: Freitag 30. Januar 2015, 16:17

Hi Leute,

ich habe ein Tool fertig welches ca. 9 GB Daten verarbeiten muss (12GB in naher Zukunft).
Momentan benutze ich sqlite3. Ob der lahmen Geschwindigkeit versuche ich permanent zu optimieren und komme langsam an meine Wissensgrenzen. Langsam zweifle ich ob die Designentscheidung sqlite3 zu nehmen die richtige war.

Ich möchte euch kurz das Programm beschreiben und euch bitten mir ein paar Tipps zu geben.


Die Eingangsdaten sind auf mehrere, im Moment 8, csv Dateien verteilt.
In jeder Datei gibt es eine uid und weitere Parameter.
Daten mit der gleichen uid gehören zusammen.
Die Daten aus Datei A und B sind im Verhältnis 1:1, d.h. für jede uid in A gibt es eine uid in B.
Die weiteren Dateien können für eine in A und B vorhandene uid Daten bereithalten oder auch nicht. Es kann auch sein, dass zu einer uid in A und B es mehrere Einträge in den anderen Dateien gibt (mit der gleichen uid).
Es gibt keine Reihenfolge der Zeilen. D.h. uid 1 in A kann in Zeile 1 sein und in B in Zeile 100.

Das Ziel ist, aus allen Dateien zugehörige Daten (gleiche uid) harauszusuchen und als ein zusammenhängendes Datum in eine einfache ASCII Datei zu schreiben (nach Konvertierung, aber das ist nicht das Problem).

Im zweiten Schritt müssen die neuen Daten mit den Daten vom Vortag verglichen werden und nur das Delta muss in eine ASCII Datei geschrieben werden (nach Konvertierung)


Also:
Aufgabenstellung 1:
Für jeden Eintrag in A nehme die uid und suche genau einen Eintrag in B mit der gleichen uid, dann durchsuche Dateien C,D,E,F,G,H ob dort diese uid auch vorhanden ist, wenn ja füge die Daten zusammen und verarbeite dieses Paket.


Aufgabenstellung 2:
Jeden Tag gibt es einen neuen Satz von csv Dateien mit (momentan) 9GB Daten. Die Daten von Heute müssen mit den Daten von Gestern verglichen werden. Gibt es Unterschiede so muss für die veränderten Daten ein vollständiges Datenpaket erzeugt werden.
-> Für jedes Datum vom Vortag suche ein Datum mit gleicher uid von Heute und vergleiche diese. Wenn nicht vorhanden oder verändert erzeuge ein neues Datenpaket mit den Daten von Heute.


Das Tool macht entweder Aufgabenstellung 1 oder 2.


Mit der sqlite3 Lösung die ich implementiert habe brauche ich für Aufgabenstellung 1 ca. 1,5h, für Aufgabenstellung 2 ca. 3,5h.
Vorallem Aufgabenstellung 2 muss beschleunigt werden. Ich würde gerne unter 2h bleiben.


Rechner:
Windows 7 64bit
i7 3770
16GB RAM (mehr nicht möglich)
256GB SSD für Daten
Python 2.7.8


Mich interessiert:
1. Wie würdet ihr das Problem lösen?
2. Die meiste Zeit braucht bei Aufgabenstellung 2 der Vergleich der Daten in vom Vortag mit den neuen Daten. Die Daten passen nicht ins RAM. Wie kann man das beschleunigen?



PS: Ich möchte ncht im Detail beschreiben wie ich das implementiert habe, da ich offen für neue Ideen bin und euch nicht in eine bestimmte Denkrichtung lenken möchte.
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Die Zeiten sind wesentlich länger als sowas dauern sollte. An der Hardware, Datenmenge oder SQLite sollte es allerdings nicht liegen. Ich würde mal davon ausgehen, dass es da noch einiges an Verbesserungspotential im Code selbst gibt, ohne diesen lässt sich dazu natürlich nicht viel sagen.

Insofern kann man nur dazu raten, eine Teilmenge der Daten zu nehmen und mal mit einem Profiler zu schauen was soviel Zeit kostet.
BlackJack

Und die üblichen Verdächtigen ausschliessen: Wo es möglich ist `executemany()` verwenden. Nicht jede Schreibaktion einzeln „committen”. Beim ersten importieren der Daten die Indizes vielleicht erst nach dem Import anlegen.
rman
User
Beiträge: 8
Registriert: Freitag 30. Januar 2015, 16:17

Erstmal danke.

Mein Tool liesst die Daten erst ein und verarbeitet diese im zweiten Schritt.
Das Einlesen hat Verbesserungspotential. Euren Tipps folgend habe ich entdeckt dass meine Verarbeitungsroutinen für 75% der Laufzeit verantwortlich sind. Da ist noch einiges an Potential drin.

Der zweite Schritt bei dem die Daten ausgelesen werden ist sehr datenbanklastig ich vermute dass mein Konzept mist ist, deshalb meine Frage an euch wie ihr solche Daten verarbeiten würdet.

Diese 9GB sind aufgeteilt auf 8 Dateien. 2 Dateien haben kaum Einträge.
Datei A und B haben ca. 5M Einträge, wobei Datei B etwa 300 Spalten hat. Alle anderen Dateien begnügen sich mit 10 bis 40 Spalten.

Mein grösstes Problem ist zu verstehen wie ich die Daten aus C,D,E,F,G,H einbinden kann. Nochmals zur Wiederholung:
Datei A und B sind 1:1, d.h. für jeden Eintrag (uid) in A gibt es einen Eintrag in B.
In den anderen Dateien ist das Verhältnis 1:N mit N=0..10 (circa)


Ich melde mich nach dem Profilerlauf aber vielleicht hat jemand einen Tipp für mich bezöglich des Datenbankaufbaus (muss es überhaupt eine Datenbank sein oder gibt es bessere Methoden. Die Beantwortung dieser Frage steht aus noch aus.)

PS: Ein Index auf die uid ist erstellt.

Danke und Grüsse
rman
Zuletzt geändert von rman am Montag 2. Februar 2015, 08:58, insgesamt 1-mal geändert.
Sirius3
User
Beiträge: 18216
Registriert: Sonntag 21. Oktober 2012, 17:20

@rman: Deine Angaben sind zu allgemein um konkrete Tipps geben zu können. Werden wirklich alle 300 Spalten immer gebraucht? Was steht denn da drin? Welche Felder sollen denn verglichen werden?
Für mich hört sich das so an, als ob eine Datenbank hier fehl am Platz ist. Ich würde alle Datensätze zusammensetzen, das ganz als JSON pro Datensatz nach uid sortiert in eine Datei schreiben und die Abweichungen mit einem einfachen "diff" heraussuchen.
rman
User
Beiträge: 8
Registriert: Freitag 30. Januar 2015, 16:17

Hi Sirius3

die Datensätze sind alles Text, manchmal ist es ein True/False, manchmal eine 4 digit Zahl, manchmal bis zu 100 Characters pro Feld. Alles wird gebraucht.

Der entscheidene Parameter ist die uid.
JSON kenn ich nicht muss ich mir anschauen.Danke für den Tipp.

Das Zusammensetzen ist mein Problem. Wären die Daten alle 1:1:1:1... so würde ich aus jeder Datei den Eintrag mit der passenden uid suchen und in die DB eintragen.
Bei 1:1:N:N:N:N:N:N tue ich mir schwer. Wenn ich die Daten in verscheidene Datenbanken eintrage und dan ein JOIN mache bekomme ich ein kartesisches Produkt. Das kann ich nicht gebrauchen.
Die simple Verküpfung von 1:1:N:N:N:N:N:N bekomme ich nicht hin. Deshalb habe ich verschiedene sqlite3 connections erzeugt um unterschiedliche cursors zu haben und habe folgenden Pseudocode:

for data in (select * FROM A_B):
uid = data[1]
output = [data]
for data in (select * FROM C WHERE C.uid==?,uid):
output.append(data)
for data in (select * FROM D WHERE D.uid==?,uid):
output.append(data)

und so weiter


Vielleicht habe ich nur eine Denkblockade oder sehe ein Problem wo es keins gibt.

Danke und Grüsse,
rman
Sirius3
User
Beiträge: 18216
Registriert: Sonntag 21. Oktober 2012, 17:20

@rman: wie man eine 1:N-Verknüpfung macht, ist Grundlagenwissen. Schau Dir mal eine SQL-Einführung an. Damit das einigermaßen schnell geht, muß für C.uid natürlich ein Index existieren.
rman
User
Beiträge: 8
Registriert: Freitag 30. Januar 2015, 16:17

Hi,

Index ist vorhanden, ohne ist das Tool nicht mal am Wochenende durchgelaufen.

Mein Problem scheinen andere auch zu haben. Nur leider habe ich nichts gelesen in dieser Grössenordnung.
Alle haben entweder nur ein paar Einträge oder nur ganz wenige Spalten. Bei mir sind es etwa 460 Spalten und 5M Zeilen.

Packe ich alles in eine Datenbank in veschiedene Tabellen und habe eine Beziehung 1:1:2:4:2:1:1:1 dann bekomme ich 16 Ergenisse bei einem JOIN und müsste diese Ergebnisse nachverarbeiten um das gewünschte Ergebnis zu bekommen.

Es wäre zu untersuchen ob das schneller ist als die Mehrfachabfragen in einer Schleife die ich im Moment mache.

Vielleicht kommt jemand noch mit einer brillianten Idee wie ich es besser machen könnte?



Danke und Grüsse
rman
Benutzeravatar
sparrow
User
Beiträge: 4504
Registriert: Freitag 17. April 2009, 10:28

Du solltest dir Beispieldateien mit einem Bruchteil der Daten erzeugen.
Also 9 csv-Dateien, die insgesamt vielleicht 10-20 Datensätze enthalten, die aber so verteilt sind wie die Echtdaten. Also uid unique, Reihenfolge durcheinander.

Anhand der Daten könntest du dann ausprobieren welche Möglichkeiten wie lange dauern. Mit den Daten bist du ja in Sekundenbruhteilen durch, kannst an den Schrauben drehen und musst dann nur noch bei der Arbeit mit den echten Daten aufgrund der Größe ausprobieren ob deine Lösung skaliert.

Je nach Einsatzgebiet könnte es auch sinnvoll sein einen Hash über den Wert der Daten hinter einer UID zu ermitteln und beim "schauen ob sich was geändert hat" nur diese zu vergleichen. Das würde aber zum Ermitteln des Hashes ebenfalls Zeit kosten. Je nachdem wie oft sich Daten tatsächlich ändern im Verhältnis wie oft sie verglichen werden, kann das aber durchaus sinnvoll sein,.
rman
User
Beiträge: 8
Registriert: Freitag 30. Januar 2015, 16:17

Danke sparrow.

Diese Idee habe ich anfangs verfolgt, alles schön im RAM verarbeitet und war begeistert. Jetzt wo ich mit den grossen Daten arbeite und der RAM nicht mehr reicht bin ich am verzweifeln.

Aber das Gespräch mit euch hilft mir weiter. Jetzt habe ich am Profiler entdeckt dass ein grosser Teil der Zeit für das verarbeiten der csv Datei draufgeht.

Folgenden code benutze ich (pseudo code):

Code: Alles auswählen

with open(fileName,'rb') as csvfile:
    csvreader = csv.reader(csvfile,delimiter=',')    
    for row in csvreader:
        if not row[5].startswith(uid_range):  # test ob uid in erlaubter range
            continue
        dataList.append(row)
        totallines += 1
        if not totallines%100:
            cursor.executemany('INSERT INTO tableName VALUES (%s)')%dataList
alternativ und etwas schneller:

Code: Alles auswählen

with open(fileName,'rb') as csvfile:
    for line in csvfile:
        row = line.strip().split(',')    # hier geht meiste Zeit drauf
        if not row[5].startswith(uid_range):  # test ob uid in erlaubter range
            continue
        dataList.append(row)
        totallines += 1
        if not totallines%100:
            cursor.executemany('INSERT INTO tableName VALUES (%s)')%dataList
Leider kann ich nicht die csv direkt in sqlite3 einlesen, weil ich noch den ein oder anderen Test am row machen muss bevor der row in die Datenbank darf.
Ich hätte nicht gedacht dass split() so viel Zeit braucht. Vielleicht finde ich dafür noch eine schnellere Lösung.

Mit der vorhandenen HW komme ich auf Leseraten von den Inputdateien um 45-55MB/s . Etwa 2/3 aller Einträge in der Inputdatei werden verworfen da nicht im gewünschten range.

Damit habe ich jetzt eine weitere Baustelle. Ich muss das Einlesen der csv Dateien beschleunigen. Das "INSERT INTO" scheint sehr schnell zu sein. Das hätte ich nie gedacht dass eine einfache Operation wie .split() mehr Zeit braucht als executemany auf eine Datenbank.

Nochmals vielen Dank für den Hinweis auf den Profiler.


Grüsse
rman
Zuletzt geändert von Anonymous am Montag 2. Februar 2015, 15:49, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Code-Tags gesetzt.
Sirius3
User
Beiträge: 18216
Registriert: Sonntag 21. Oktober 2012, 17:20

@rman: benutz doch bitte im Forum die für Code gedachten Tags. Dann kann man auch lesen, was Du da an Code schreibst. Zum Lesen von csv-Dateien nimmt man das csv-Modul, wenn des da wirklich einen Zeitunterschied gibt, dann ist der auf die gesamte Laufzeit bezogen marginal. Dein INSERT tut weder in der einen, noch in der anderen Variante, bei mir kommt da "function takes exactly 2 arguments (1 given)". Idealerweise solltest Du dataList auch irgendwo wieder leeren.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Anstatt mit einem manuellen Zähler könntest (und imho solltest!) Du mit ``enumerate`` arbeiten. Damit wird Dir ein küsntlicher Index automatisch erzeugt.

Sofern das ``split`` wirklich so böse ist und Du *viele* Zeilen verwirfst, weil sie nicht im "uid-range" liegen, könntest Du ggf. versuchen, diese Bestimmung zu vereinfachen. Evtl. könnte man das CSV-Format umstellen, so dass das direkt in der 0. Spalte steht und man *direkt* auf den Rohdaten ein ``startswith`` ausführen kann.

Ansonsten erscheinen mir 100 Zeilen irgend wie auch ziemlich wenig für einen Bulk-Vorgang! Da würde ich mal eher auf 10000 gehen...

Ach so, es kann sich lohnen, Indizes und Trigger usw. von einer Tabelle für den Bulk-Vorgang zu entfernen und *im Anschluss* wieder zu setzen.

Allgemein finde ich die negative Logik für das Verwerfen doof und unleserlich. Wieso nicht einfach nur dann etwas tun, *sofern* eine Bedingung *erfüllt* ist? ;-)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
BlackJack

@rman: Wenn das `split()` am meisten Zeit verbraucht kann man da nicht viel machen. Wobei man da auch sicher sein sollte das `split()` tatsächlich funktioniert denn das CSV ist nicht so einfach wie man vielleicht auf den ersten Blick denken mag.

Falls Du das jetzt mit einer kleineren Datei gemacht hast, solltest Du die Zeiten hochrechnen und schauen ob das ungefähr hinkommt. Nicht das das Problem nur bei grossen Datenmengen auftaucht, zum Beispiel weil sich irgendwo doch mehr im Speicher ansammelt als in den physikalisch vorhandenen Speicher passt. Im Beispielcode ist zum Beispiel kein `commit()`.
rman
User
Beiträge: 8
Registriert: Freitag 30. Januar 2015, 16:17

Hi danke für das Edit.

Das was pseudocode. Grob abgetippt.
Das csv Modul ist etwas langsamer als ein split(). Ich kenne meine Daten und weiss dass ein split() reicht.
Einträge wie:
a,"bla,bla,bla",c
habe ich nicht. Sonst könnte ich .split() nicht verwenden.
Mir ist gerade die Idee gekommen das Einlesen der Daten auf mehrere Threads zu verteilen. Mal sehen ob es funktioniert.
Wie man multithreadingmässig auf sqlite3 zugreift ist mir noch nicht klar. Das muss ich noch herausfinden.
Das Einlesen der Daten in die Datenbank dauert 29 Minuten. Ziel ist unter 20 Minuten, besser 15.
Weniger als 10 Minuten wird wohl kaum gehen da die Eingansdaten (csv) 75GB gross sind. Allein das pure Lesen der Daten von SSD dauert 12 Minuten.
Die sqlite Datenbasis ist 9GB gross.
(Sorry, ich habe anfangs die Dateigrösse der Datenbank angegeben aber geschrieben es wären 9GB Text)


@Hyperion: Ich habe keinen Unterschied gesehen zwischen 100 und 10000 Bulkeinträge.

Die Datenbank ist am Anfang leer. Im Anschluss mache ich einen:

Code: Alles auswählen

CREATE INDEX uid_index on A_B (uid)


Danke
rman
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

rman hat geschrieben:Das was pseudocode. Grob abgetippt.
Dann solltest du vielleicht den echten Code zeigen, denn nur darin lassen sich die echten Fehler finden. Sirius3 hat dich ja auf den riesigen Schnitzer mit der fehlenden Leerung von dataList (was übrigens ein unglaublich schlechter Name ist) hingewiesen. Sollte das wirklich so im Code stehen, dann hättest du aus einem linearen Problem ein quadratisches gemacht.
Das Leben ist wie ein Tennisball.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@rman:
Das mit dem .split ist doch Unsinn, selbst 5min hin oder her machen die Funktionseinbuse nicht wett. Das sind Mikrooptimierungen, auch schreibst Du jetzt von ganz anderen Werten als noch oben. Deine neuen Werte dürften so ziemlich Oberkante des überhaupt auf einem Rechner mit Python Möglichen sein, danach hilft nur noch sharding über mehrere Systeme (wobei dann das Kopieren der Rohdaten selbst wieder längher dauern dürfte).
rman
User
Beiträge: 8
Registriert: Freitag 30. Januar 2015, 16:17

Danke für die Hinweise.

Gehofft habe ich dass ich irgendwo einen Gedankenschnitzer habe und mein Konzept im Ar*** ist.

Ich werde die eine oder andere Optimierung in Cython machen.
Benutzeravatar
sparrow
User
Beiträge: 4504
Registriert: Freitag 17. April 2009, 10:28

Ohne jetzt im Detail deine Abläufe zu kennen:

Wenn das Lesen der Daten schon eine Stunde dauert, dann dauert das reine Schreiben der Datebank ja auch noch eine Weile.
Und da sind Verarbeitungsschritte noch gar nicht berücksichtigt. Ich nehme nicht an, dass du alle cvs-Dateien gleichzeitig im Speicher hälst, sondern je nach Auftreten der uid in der Datei diese Daten in der Datenbank an den bereits vorhandenen Datensatz anfügst. Da geht natürlich aus Zeit drauf, den Datensatz zu finden und Spalten zu aktualisieren.

Ich finde die Laufzeit jetzt gar nicht sooo schlecht ;)
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@rman:
Die Obergrenze scheint ja ~100MB/s zu sein, da die Platte die Daten nicht schneller liefern kann. Derzeit schaffst Du ~40MB/s. Da Du sagst, dass die meiste Zeit fürs Parsen der Dateien drauf geht, könntest Du das evtl. durch parallele Verarbeitung beschleunigen (z.B. mit multiprocessing). Allerdings brauchst Du dafür wahrscheinlich ein anderes DBMS - wenn ich miche recht erinnere, lockt Sqlite die DB-Dateien global für Schreibzugriffe.
Du könntest z.B. A und B partiell verarbeiten lassen - mit `seek` reinspringen, erste Zeile verwerfen (wahrscheinlich kaputt, da mitten in der Zeile), UID der nächsten Zeile an andere Prozesse melden, stoppen an bereits verarbeiteter Zeile aus anderem Prozess (Achtung: `seek` ist tricky mit Textdateien, solltest Du nur vorm ersten Lesen nutzen). C,D,E,F,G,H kannst Du wahrscheinlich direkt auf mehrere Prozesse verteilen.
Sirius3
User
Beiträge: 18216
Registriert: Sonntag 21. Oktober 2012, 17:20

@rman: ob Du irgendwo einen Gedankenschnitzer hast, kann hier niemand sagen, weil niemand Deine Gedanken, respektive Code, lesen kann. Reine Text-Verarbeitung ist mit Python sehr effizient, da kann man meistens kaum noch was optimieren. Mein kleiner Test mit 7GB an Daten und 500Mio Zellen einlesen braucht knapp 1 min, das stimmt mit Deinen 12 min gut überein.
Warum die Daten in der Datenbank plötzlich nur noch 9GB sind, versteh ich nicht, weil Datenbanken sind nicht für ihre Speichersparsamkeit bekannt.
Antworten