Parquet in der Produktion: Wie man Daten für Analytics liest, schreibt und optimiert
Parquet ist mehr als nur ein Speicherformat – es ist ein Werkzeug zur architektonischen Optimierung. Seine spaltenorientierte Struktur, die integrierte Filterung zur Laufzeit, die Wörterbuchkodierung und die Metadaten zu den Zeilengruppen reduzieren den I/O-Umfang um Größenordnungen und verringern die Datengröße im Vergleich zu CSV um bis zu 140-fach. Für Data Engineers und Analysten bedeutet dies nicht nur Einsparungen beim Speicherplatz, sondern auch geringere Latenzzeiten bei interaktiven Abfragen, weniger Speicherdruck und vorhersehbare Performance. Dieser Artikel bietet praktische Anleitungen zum Arbeiten mit Parquet in Python auf mittlerem bis fortgeschrittenem Niveau – von der grundlegenden Lektüre über pandas bis hin zur manuellen Kontrolle über Schema, Partitionierung und Kompression.
Warum Parquet CSV in modernen Datenpipelines ablöst
CSV ist ein textbasiertes Format ohne Semantik: Jede Zeile wird als einzelner Block verarbeitet, Trennzeichen werden dynamisch analysiert und Typen werden heuristisch abgeleitet. Parquet hingegen ist binär, streng typisiert und spaltenorientiert, mit eingebetteten Metadaten. Die wichtigsten Unterschiede sind:
- Spaltenorientierte Speicherung: Jede Spalte wird separat gespeichert, sodass Sie nur die benötigten Felder lesen können (z. B.
yearundindicator_value), während der Rest ignoriert wird – ohne die gesamte Zeile zu parsen. - Zeilengruppen: Die Daten werden in Blöcke unterteilt (standardmäßig ~128 MB), von denen jeder Statistiken enthält (
min,max,null_count). Bei einer Filterung aufyear == 2023überspringt das System ganze Zeilengruppen, ohne sie in den Speicher zu laden. - Wörterbuchkodierung: Bei String-Spalten mit hoher Wiederholungsrate (Regionen, Kategorien) speichert Parquet entweder automatisch oder explizit ein Wörterbuch mit eindeutigen Werten und Verweisen darauf – statt „Moskau Region“ × 500.000 Datensätze verwendet es also den Code
1× 500.000. - Typsicherheit: In CSV macht eine
NULLin einem numerischen Feld die gesamte Spalte zufloat64; in Parquet hingegen werden Typen strikt im Schema definiert und beim Schreiben validiert.
Der Unterschied in der Praxis ist enorm: Ein 576-MB-Datensatz zur Krebsinzidenz schrumpft in Parquet auf 4 MB (Kompressionsverhältnis ×144), und die Zeit für das Lesen eines gefilterten Teils sinkt von 12–18 Sekunden auf unter 300 ms.
Lesen: Von pandas bis zur feinabgestimmten Kontrolle mit pyarrow
Grundlegendes Lesen mit pandas
Für den schnellen Einstieg genügt pd.read_parquet():
import pandas as pd
df = pd.read_parquet(
"data_zis_109_v20260126.parquet",
columns=["object_name", "year", "indicator_value"],
filters=[("year", "=", 2023)]
)
Wichtig ist, dass die Filterung bereits beim Lesen erfolgt, nicht erst nach dem Laden des gesamten DataFrames – das ist entscheidend bei Dateien größer als 1 GB.
Direkter Zugriff mit pyarrow: Kontrolle über Schema und Metadaten
Wenn präzise Steuerung erforderlich ist, verwenden Sie direkt pyarrow.parquet:
import pyarrow.parquet as pq
# Ohne Datenladen – nur Schema und Metadaten
schema = pq.read_schema("data_zis_109_v20260126.parquet")
meta = pq.read_metadata("data_zis_109_v20260126.parquet")
print(f"Zeilen: {meta.num_rows:,}, Spalten: {meta.num_columns}")
# Lesen mit Filterung und Spaltenauswahl
table = pq.read_table(
"data_zis_109_v20260126.parquet",
columns=["object_name", "object_oktmo", "year", "indicator_value"],
filters=[("year", "=", 2023)]
)
df = table.to_pandas() # Konvertierung in pandas, falls nötig
Bei extrem großen Dateien empfiehlt sich das Streaming-Lesen in Chargen:
pf = pq.ParquetFile("data_zis_109_v20260126.parquet")
for batch in pf.iter_batches(batch_size=100_000):
df_batch = batch.to_pandas()
# Verarbeitung der Charge
Schreiben: Von einfachen bis hin zu produktionsreifen Serialisierungen
Herausforderungen der automatischen Serialisierung
df.to_parquet() funktioniert zwar, birgt aber Risiken:
- Implizite Typbestimmung:
int64mit NULL →float64,datetime64→string. - Fehlende Wörterbuchkodierung für hochkardinalisierte String-Spalten.
- Unangemessene Zeilengruppengröße (Standard 64 MB), was die Filtereffizienz verringert.
Explizites Schema und kontrollierte Kodierung
import pyarrow as pa
import pyarrow.parquet as pq
schema = pa.schema([
pa.field("object_name", pa.string()),
pa.field("object_oktmo", pa.dictionary(pa.int32(), pa.string())),
pa.field("year", pa.int32()),
pa.field("indicator_value", pa.float64())
])
table = pa.Table.from_pandas(df, schema=schema, preserve_index=False)
pq.write_table(
table,
"data_zis_new_version.parquet",
compression="zstd",
row_group_size=500_000,
version="2.6",
write_statistics=True
)
Warum zstd? Die Kompression ist 30–50 % besser als snappy, bei vergleichbarer Dekompressionsgeschwindigkeit. Sie bietet das optimale Gleichgewicht für analytische Workloads.
Partitionierung und Arbeit mit Dateisätzen
Wenn die Daten logisch aufgeteilt sind (nach Jahr, Region), verwenden Sie write_to_dataset():
pq.write_to_dataset(
table,
root_path="data_zis_partitioned/",
partition_cols=["year"]
)
Das Lesen wird dann effizient:
import pyarrow.dataset as ds
dataset = ds.dataset("data_zis_partitioned/", format="parquet", partitioning="hive")
table = dataset.to_table(filter=ds.field("year") == 2023)
Partitionierung ist sinnvoll bei Datensätzen über 1 GB und wenn der Partitionsschlüssel weniger als 1.000 verschiedene Werte hat. Vermeiden Sie Partitionierung nach hochkardinalisierten Feldern (z. B. indicator_value) – das erzeugt tausende kleiner Dateien und beeinträchtigt die Performance.
Was wirklich wichtig ist
- Parquet ersetzt keine Datenbank: Es eignet sich ideal für append-only, leseintensive Analyse-Szenarien, unterstützt jedoch keine Transaktionen, UPDATEs oder Indizes.
- Die Wörterbuchkodierung bringt maximale Vorteile, wenn die Kardinalität unter 10⁴ liegt und die Wiederholungsrate über 30 % beträgt. Bei Spalten mit einzigartigen Werten (UUIDs, Zeitstempel) ist sie nutzlos.
- Die Größe der Zeilengruppen beeinflusst die Filtergenauigkeit: Zu klein führt zu übermäßiger Metadatenbelastung; zu groß macht das Überspringen unnötiger Daten ineffizient. Der optimale Bereich liegt bei 100–500 Tausend Zeilen.
- Statistiken (
write_statistics=True) sind für die Produktion unerlässlich. Ohne sie verhält sich die Filterung wie ein vollständiger Scan. - Überprüfen Sie immer das Schema vor dem Schreiben:
pa.Table.from_pandas(df, schema=schema)gibt einen Fehler aus, wenn die Typen nicht übereinstimmen – das schützt vor stiller Beschädigung.
— Editorial Team
Noch keine Kommentare.