Nachdem mein Kollege Ralph Straumann seine zweiteilige Serie zu CSV und dessen designierten Nachfolger Parquet verfasst hat, gab es einige Fragen, wie denn eine «gute» Parquet-Datei erstellt werden soll. Aus diesem Grund möchte ich hier gerne einige Überlegungen dazu teilen, was es zu beachten gilt.
Einige der hier vorgestellten Optimierungen kommen naturgemäss erst ab einer gewissen Datenmenge so richtig zum Tragen. Sie im Sinne von «Best Practices» aber dennoch zu beachten schadet nicht und ist im Normalfall nicht mit viel Aufwand verbunden.
Datentypen definieren…
Der offensichtlichste Nutzen von Parquet ist es, den Datentyp für jede Spalte explizit und richtig zu definieren. Wenn das korrekt gemacht worden ist, unterscheidet sich der typische Aufruf um eine Parquet- oder eine CSV-Datei zu lesen offensichtlich: Beim CSV lässt man den Parser Datentypen raten (und bereinigt potenziell im Nachhinein) oder – Best Practice – man instruiert den CSV-Reader manuell, welchen Datentyp welche Spalte aufweist. Wir schauen uns das unten mal an einem echten Beispiel an, denn Open Data Zürich bietet einige seiner Datensätze neu auch als Parquet an:

Pandas ist zwar nicht schlecht darin, die Datentypen von CSV-Spalten zu erraten. Bei grossen Datenmengen kann es aber vorkommen, dass entweder der Datentyp nicht passt oder es extrem lange dauert, bis die Daten gelesen sind. Wenn man keine Überraschungen erleben bzw. mehr Kontrolle über die Daten haben möchte, sollte man wie im obigen Code-Schnipsel explizit die gewünschten Datentypen an beim Lesen des CSVs angeben.
Die Methode read_csv()
von Pandas hat extra einen Parameter low_memory
(standardmässig auf True
gesetzt), der entscheidet, wie viel Daten gelesen werden, um den Datentyp einer Spalte zu bestimmen. Bei Parquet entfällt das komplett, da in den Metadaten der Datei die Datentypen der Spalten bereits definiert sind.
Beim Aufbereiten und Exportieren von Daten im Parquet-Format (z.B. von Pandas mit to_parquet()
) kann es sich dennoch lohnen, die Datentypen explizit zu setzen. Damit können Daten möglichst platzsparend gespeichert werden (z.B. als int8
anstatt als int64
bei entsprechend kleinen Ganzzahlen). Insbesondere das Definieren einer Spalte als category
führt dazu, dass Parquet diese als Dictionary abspeichert, d.h. die einzelnen kategorischen Werte werden nur einmal physisch gespeichert und ansonsten mit einem Index referenziert:

Das spart unter Umständen viel Speicherplatz und macht das Schreiben und Lesen der Daten schneller.
…und sortieren!
Neben der Definition der Datentypen ist der grösste Hebel für die Performance von Parquet-Dateien die Sortierung der Daten. Als Faustregel gilt: Die Daten sollten nach derjenigen Spalte sortiert sein, nach der sie am häufigsten gefiltert werden.
Übrigens: Wenn man nicht den gesamten Informationsgehalt aus einer Parquet-Datei benötigt, kann mit den Parametern filters
und columns
der Methode read_parquet()
von Pandas bereits beim Einlesen einer Parquet-Datei ein grosser Performance-Boost erreicht werden. In der Dokumentation steht:
filters: To filter out data. […] filtering is […] performed at the partition level, that is, to prevent the loading of some row-groups and/or files.
Pandas 2.2.1 Documentation
columns: If not None, only these columns will be read from the file.
Schauen wir uns doch gleich ein Beispiel an und laden die Fahrgastzahlen der VBZ von 2023:

Wir sehen also eine massive Verbesserung der Lesegeschwindigkeit (über 20mal schneller), wenn wir bereits beim Lesen der Parquet-Datei die entsprechenden Prädikate als Filter angeben. Entscheidend ist hier die Sortierung der Datei: dadurch werden intern Metadaten erstellt, die es ermöglichen ganze Blöcke von Daten auszulassen und so zur Steigerung der Lesegeschwindigkeit beitragen. Und auch die Angabe der verwendeten Spalten (für die Projektion, d.h. welche Spalten ausgewählt werden) erhöht die Lesegeschwindigkeit nochmals ein bisschen.
Um zu verstehen weshalb das so ist und wieso die Sortierung im obigen Beispiel so viel hilft, müssen wir uns kurz mit den dem Parquet-Datenformat auseinander setzen.
Aufbau des Parquet-Datenformats
Schauen wir uns mal ein Beispiel an. Hier eine Tabelle mit Messdaten:

Ganz grob gibt es zwei Arten, wie wir solche tabellarische Daten in einer Datei speichern können:
- Zeilenweise, wie zum Beispiel im Format CSV
- Spaltenweise, wie zum Beispiel im Format Apache ORC
Wenn man analytische Abfragen auf den Daten laufen lassen will, sind die zwei wichtigsten Faktoren, die sich auf die Performance aufwirken, die Projektion (welche Spalten ausgewählt werden – entspricht dem SELECT-Statement in einem SQL-Query) und die Prädikate (welche Zeilen ausgewählt werden – entspricht der WHERE-Klausel in einem SQL-Query).
In beiden Fällen versuchen wir so wenig wie möglich von der Datei zu lesen, um Zeit zu sparen. Und weil wir in Abfragen meist Projektion und Prädikate brauchen, sollte das Dateiformat beides performant unterstützen.
Wenn wir die Datenspeicherung naiv betrachten, dann sehen wir, dass Spalten-basierte Dateiformate optimal geeignet sind, nur minimal viele Daten zu lesen um die gewünschte Projektion zu erhalten: Möchten wir die erste Spalte haben, lesen wir einfach die ersten x Bytes in der Datei (wobei x der Grösse der ersten Spalte entspricht). Umgekehrt können wir bei Zeilen-basierten Dateien wunderbar auf die Prädikate prüfen: um alle Resultate mit ID > 5
zu erhalten, müssen wir «lediglich» die Datei aufsteigend nach der ID sortieren und können dann von Beginn an alle Zeilen lesen, bis wir eine ID > 5
erreicht haben. Dann sind wir fertig: alle nachfolgenden Zeilen sind das Resultat der Abfrage.
Parquet nutzt ein hybrides Modell. Parquet wird deshalb auch als «column-oriented» (im Gegensatz zu «column-based») bezeichnet. Intern werden die Daten im Parquet-Format in sogenannten Row Groups gespeichert. Und zu jeder dieser Groups gibt es Metadaten, die helfen zu entscheiden, ob eine Gruppe gelesen werden muss oder übersprungen werden kann. In diesen Metadaten sind beispielsweise die minimalen und maximalen Werte jeder Spalte in der der Row Group. So kann Parquet eine Abfrage schnell auflösen und nur jene Row Groups lesen, die passende Daten enthalten.

Dieses Konzept nennt sich Predicate Pushdown. Es beschreibt eine Optimierung von Datenbanken bzw. in der Prozessierung von Daten um die Query-Performance zu verbessern: Die Filterkriterien (Prädikate) werden so nah wie möglich an die Datenquelle geschoben, anstatt viele Daten zu lesen und dann erst in einem zweiten Schritt unpassende Daten per Filterung zu entfernen.
Schauen wir uns nun dieselbe Abfrage wie oben in Parquet an mit seinen Row Groups und den zugehörigen Metadaten. Wenn wir eine Abfrage mit dem Prädikat ID > 5
laufen lassen wollen, prüft Parquet seine Metadaten und entscheidet, ob eine Row Group gelesen oder übersprungen werden kann:
Row Group | min | max | Prüfung | Resultat |
1 | 1 | 3 | max <= 5 | Überspringen |
2 | 4 | 5 | max <= 5 | Überspringen |
3 | 6 | 8 | min > 5 | Lesen |
Dieser Mechanismus ermöglicht es, nur wenige Daten zu lesen, wodurch sich die Zeit für eine Abfrage massiv verbessern kann.
Mein Fazit: Parquet hat viele Vorteile. Die Dateien werden schön klein (dank effizienter Kompression), sie lassen sich schnell lesen und filtern (dank dem hybriden Datenmodell und einer geeigneten Sortierung) und sie sind sehr anwendungsfreundlich, da nach dem Einlesen die Daten direkt mit den richtigen Datentypen vorhanden sind. Wenn man obige Best Practice-Hinweise beachtet, ist man schon auf gutem Weg, Daten in guter Form im Parquet-Format abzulegen.
Haben Sie Data Engineering- oder Data Science-Herausforderungen, bei denen ein effizientes Datenformat nützlich sein könnte? Oder wo das Problem etwas komplizierter ist und Sie eventuell froh um einen Austausch wären? Kontaktieren Sie mich unverbindlich.
Headerbild © ods