Update 02.02.26 – Nach hilfreichen Hinweisen des Polars-Teams auf LinkedIn haben wir unser Benchmark-Setup um eine Polars-Konfiguration erweitert, bei der Async erzwungen wird. Dies wird im Artikel näher erläutert.
Unser vorheriger Benchmark verglich DuckDB, Polars und Pandas anhand eines 9-GB-CSV-Datensatzes. Das ist ein Problem im Laptop-Maßstab. Was passiert jedoch, wenn analytische Workloads Hunderte von Gigabyte oder sogar Terabytes erreichen – Größenordnungen, bei denen Speicherbeschränkungen kritisch werden und Entscheidungen zur Dateistruktur eine große Rolle spielen?
Dieser Artikel unterzieht DuckDB und Polars einem Stresstest mit Datensätzen von bis zu 2 TB und untersucht dabei bewusst adversariale Szenarien: sehr große Parquet-Dateien (je 140 GB), Sammlungen vieler kleiner Dateien (72 × 2 GB) sowie Kombinationen aus beidem. Anstatt einen universellen Sieger zu bestimmen, analysieren wir, wie unterschiedliche Ausführungsmodelle auf extremen Speicherdruck und verschiedene Entscheidungen zur Dateiorganisation reagieren.
Indem wir das logische Abfragemuster konstant halten und ausschließlich Datensatzgröße und physisches Layout variieren, isolieren wir die Auswirkungen von Dateiorganisation und Skalierung auf die Performance. Dadurch wird ein fokussierter Vergleich von Ausführungsstrategien unter stark scan-lastigen Workloads möglich, bei denen das Speichermanagement zum entscheidenden Engpass wird.
Methodik
Benchmark-Setup
Wir verwenden die TPC-H-Tabelle lineitem, einen Industriestandard-Benchmark, der Bestellpositionen mit 16 Spalten repräsentiert, darunter Datumsangaben, Mengen, Preise und kategoriale Felder. Sie bietet eine reproduzierbare Basis mit realistischer Schema-Komplexität und Datenverteilung. Die einzelnen Parquet-Dateien reichen von etwa 2 GB bis 140 GB Größe.
Alle Abfragen folgen demselben logischen Muster: ein Parquet-Scan mit Projektion auf drei Spalten, selektiven Filtern und einer abschließenden Zeilenzählung. Der Workload ist stark scan-lastig sowie filter- und aggregationsorientiert. Join-lastige oder shuffle-intensive Abfragen werden nicht betrachtet. Beide Systeme verwenden ihre Standard-Multithreading-Einstellungen. Die Filterselektivität wurde in allen Experimenten konstant gehalten, um Vergleichbarkeit zu gewährleisten.
Experimentelle Umgebung
Alle Tests wurden auf einem MacBook Pro (2021, M1 Max, 32 GB RAM) unter Verwendung von Python durchgeführt. Wir entwickelten ein eigenes Benchmark-Tool, um eine konsistente und reproduzierbare Ausführung über die Kommandozeile sicherzustellen, wobei Nutzer sowohl die Engine als auch die zu testende Operation auswählen können. Die Ergebnisse wurden mit matplotlib visualisiert, zentrale statistische Kennzahlen wurden berichtet.
Speichermessung
In ersten Experimenten wurde der Speicherverbrauch als Differenz der Resident Set Size (RSS) zwischen Beginn und Ende jeder Abfrage gemessen. Dieser Ansatz erwies sich als irreführend, da der maximale Speicherverbrauch häufig während der Ausführung auftritt, temporäre Puffer vor Abschluss freigegeben werden können und Betriebssystem-Caching die finale RSS verfälscht.
In diesem Benchmark definieren wir den Speicherverbrauch als das maximale RSS, das während der Abfrageausführung beobachtet wird, relativ zur Ausgangs-RSS. Dadurch erfassen wir den maximalen inkrementellen Working-Set-Bedarf der Abfrage und nicht den gesamten Prozess-Footprint.
Wir haben den Prozessspeicher in kurzen Intervallen von 0,05 Sekunden gesampelt und den höchsten beobachteten Wert aufgezeichnet. Jeder Test lief in einem frischen Prozess, um eine konsistente Basislinie sicherzustellen. Obwohl sich OS-seitiges Page-Caching nicht vollständig eliminieren ließ, reduziert dieses Setup dauerhafte In-Process-Caching-Effekte. Jede Konfiguration wurde zehnmal ausgeführt; die berichteten Ergebnisse sind Durchschnittswerte.
Nuancen der Speichermessung: der mmap-Effekt
Polars verwendet standardmäßig speicherabbildendes I/O (mmap) für Parquet-Dateien auf lokalem Speicher. Mit mmap lädt das Betriebssystem Dateiseiten bedarfsgesteuert (Lazy Loading) in den Speicher und kann sie freigeben, wenn andere Prozesse Speicher benötigen. Dies erscheint in Monitoring-Tools als hoher Speicherverbrauch, unterscheidet sich jedoch von dauerhaft allokiertem Speicher, der nicht zurückgewonnen werden kann.
Unsere Messungen erfassen die Resident Set Size (RSS), die auch mmap-basierte Seiten umfasst. Diese repräsentiert den vom Prozess belegten Speicher, nicht unbedingt exklusiv besessenen Speicher – das Betriebssystem kann diese Seiten bei Bedarf auslagern. Für Benchmark-Zwecke berichten wir die maximale RSS, da sie den realen Speicherdruck auf das System widerspiegelt, weisen jedoch explizit darauf hin, wenn mmap wesentlich zum beobachteten Verbrauch beiträgt.
Datensatzerzeugung
Der in diesem Benchmark verwendete Datensatz wurde mit der tpch-Erweiterung von DuckDB erzeugt, die sowohl den Datengenerator als auch die Referenzabfragen für den TPC-H-Benchmark implementiert. Die Erweiterung ist standardmäßig in den meisten DuckDB-Builds enthalten und unterstützt die einfache Datengenerierung über Skalierungsfaktoren. Aus dem generierten Datensatz wurde die Tabelle lineitem extrahiert und mit DuckDB als Parquet-Datei exportiert.
Testdesign
Drei Experimente isolieren unterschiedliche Skalierungsdimensionen:
Skalierung einer einzelnen Tabelle (2 GB → 140 GB): Testet, wie die Engines mit zunehmendem Datenvolumen in einer einzelnen Datei umgehen. Isoliert Speicherverwaltung und Scan-Strategien ohne Einfluss der Dateianzahl.
Viele kleine Dateien (bis zu 72 × 2 GB): Testet Datei-I/O-Overhead und Koordination paralleler Scans. Reale Data Lakes enthalten oft Tausende partitionierter Dateien; dieses Szenario bewertet den Umgang der Engines mit Dateimetadaten und parallelen Lesevorgängen.
Wenige sehr große Dateien (mehrere 140-GB-Dateien → 2 TB): Kombiniert beide Herausforderungen – extreme Dateigrößen und Koordination mehrerer Dateien. Dies repräsentiert Worst-Case-Szenarien für speicherbeschränkte Umgebungen.
In den letzten beiden Tests stellt UNION ALL sicher, dass alle Eingabedaten verarbeitet werden, ohne Deduplikation zu erzwingen, und verhindert metadatenbasierte Optimierungen für reine Zeilenzählungen.
Kontext: Aufbauend auf früheren Benchmarks
Unser vorheriger Artikel verglich diese Engines anhand eines 9-GB-CSV-Datensatzes. Zentrale Ergebnisse waren:
- DuckDB: ~300 MB Spitzenspeicher, schnellste Ausführung
- Polars (lazy + streaming): ~0,5 GB Spitzenspeicher, wettbewerbsfähige Geschwindigkeit
- Pandas: ~10 GB Spitzenspeicher, langsamste Ausführung
Diese Ergebnisse deuteten darauf hin, dass Polars bei moderaten Größenordnungen mit DuckDB gleichziehen kann. Der vorliegende Stresstest untersucht, ob diese Parität auch bei 10- bis 100-fach größeren Datenmengen und mit anderen Dateiformaten (Parquet vs. CSV) bestehen bleibt.
Ergebnisse
Über alle Experimente hinweg blieb die logische Abfrage identisch, während sich physisches Layout und Gesamtdatensatzgröße von wenigen Gigabyte bis nahezu 2 TB änderten. Dadurch lassen sich die Effekte von Dateigröße, Dateianzahl und Ausführungsstrategie auf Laufzeit und inkrementellen Spitzenspeicher isoliert betrachten.
1. Skalierung einer einzelnen Tabelle
Wenn eine einzelne Parquet-Datei von etwa 2 GB auf 140 GB anwächst, zeigen Polars und DuckDB sehr ähnliche Ausführungszeiten, wobei DuckDB im größten Maßstab einen leichten Vorteil von etwa einer Sekunde behält.
DuckDB weist einen langsamen, gleichmäßigen Anstieg des Speicherverbrauchs auf und erreicht einen Spitzenwert von etwa 1,3 GB. Polars hingegen zeigt einen deutlich aggressiveren Anstieg: Bereits bei etwa 8 GB Datenvolumen überschreitet der Speicherverbrauch 1 GB und erreicht für den ~140-GB-Datensatz rund 17 GB.
Dieser 13-fache Speicherunterschied ist in speicherbeschränkten Umgebungen kritisch. Auf einer Maschine mit 32 GB RAM lässt DuckDB bei der Verarbeitung des 140-GB-Datensatzes etwa 30 GB für Betriebssystem und andere Prozesse frei, während Polars mehr als die Hälfte des verfügbaren RAMs belegt. Auf kleineren Maschinen (16 GB) würde Polars Swap oder Out-of-Memory-Fehler auslösen.
Dieses Ergebnis überraschte uns, insbesondere angesichts der starken Performance von Polars im vorherigen 9-GB-Benchmark. Mit Unterstützung des Polars-Teams untersuchten wir dies weiter und gewannen zusätzliche Einblicke in die internen Mechanismen von Polars.
Der hohe beobachtete Speicherverbrauch von Polars resultiert hauptsächlich aus dem Laden von mmap-Seiten. Auf Systemen mit weniger verfügbarem RAM würden weniger Seiten geladen, wodurch der scheinbare Speicherverbrauch geringer wäre. Wichtig ist: Dieser Speicher wird nicht dauerhaft von der Engine besessen; wenn das Betriebssystem ihn benötigt, kann er zurückgewonnen werden.
Für typische Szenarien ist dieses Verhalten vollkommen sinnvoll. Für unser spezifisches Benchmark-Setup lässt sich Polars jedoch so konfigurieren, dass es sich eher wie DuckDB verhält. Durch Setzen der Umgebungsvariable POLARS_FORCE_ASYNC=1 wechselt Polars bei Parquet- und IPC-Readern von mmap auf ObjectStore. Dadurch werden Datenbytes eager in den Speicher gelesen, anstatt auf Lazy Page Faults via mmap zu setzen. Diese Einstellungen sind für Object Storage optimiert und noch nicht vollständig für lokalen Speicher, der Performance-Impact wird jedoch als begrenzt eingeschätzt.
Mit erzwungenem Async ist Polars spürbar langsamer als sowohl DuckDB als auch das Standard-Polars und benötigt für den größten Workload fast drei zusätzliche Sekunden.
Diese langsamere Ausführung tauscht Performance gegen verbessertes Speicherverhalten. Mit erzwungenem Async bleibt der Speicherverbrauch von Polars vergleichsweise niedrig und wächst nur moderat mit größeren Workloads – etwa 350 MB für den kleinsten Datensatz und rund 750 MB für den größten. Dies steht in starkem Kontrast zum dramatischen Anstieg im Standard-Setup.
2. Viele kleine Dateien
Die Kombination von bis zu 72 Parquet-Dateien mit jeweils etwa 2 GB führt zu ähnlichen Ausführungszeiten wie im Einzelfall, wobei DuckDB einen etwas deutlicheren Vorteil zeigt. Interessanterweise gibt es hier keinen signifikanten Laufzeitunterschied zwischen Standard-Polars und Polars mit erzwungenem Async.
Der Speicherverbrauch von DuckDB bleibt konstant unter 70 MB und steigt nur minimal mit wachsendem Datenvolumen. Polars hingegen zeigt einen starken Anstieg – von etwa 400 MB bei 2 GB auf rund 4,3 GB bei 40 GB – und flacht danach weitgehend ab. Beide Polars-Konfigurationen verhalten sich hier ähnlicher als in den anderen Tests und kreuzen sich bei etwa 100 GB Datenvolumen, ab dem das Standard-Polars geringfügig speichereffizienter wird.
3. Wenige sehr große Dateien
Die Laufzeittrends ähneln denen des zweiten Tests. Für den ~2-TB-Datensatz benötigt Polars etwa eine Minute, während DuckDB in rund 45 Sekunden fertig ist. Polars mit erzwungenem Async benötigt erneut deutlich mehr Zeit und nähert sich für den größten Workload 100 Sekunden.
Der Speicherverbrauch von Polars skaliert von etwa 1,7 GB auf 23 GB. DuckDB steigt dagegen von ungefähr 500 MB auf 2,4 GB und behält durchgehend einen deutlich geringeren Spitzenverbrauch. Mit erzwungenem Async bleibt Polars näher an DuckDB: etwa 200 MB Unterschied beim kleinsten und rund 5 GB beim größten Workload. Für den größten Datensatz reduziert erzwungenes Async den Spitzenverbrauch von Polars um bis zu den Faktor 2,8.
4. Einfluss des Dateilayouts: Warum Partitionierung wichtig ist
Der Vergleich identischer Datenvolumina mit unterschiedlichen Dateilayouts zeigt ein zentrales Ergebnis: Partitionierung reduziert den Speicherverbrauch bei beiden Engines drastisch. Wir vergleichen das Einzelfall-Szenario mit dem Szenario vieler kleiner Dateien – beide repräsentieren ähnliche Datenmengen, unterscheiden sich jedoch im Layout.
Abgesehen von Polars mit erzwungenem Async zeigt keine Engine signifikante Laufzeitverbesserungen durch Partitionierung.
Beide Engines profitieren jedoch stark beim Speicherverbrauch. Bei partitionierten Workloads reduziert DuckDB seinen Spitzenverbrauch um bis zu den Faktor 8, Polars um bis zu den Faktor 4. Polars mit erzwungenem Async schneidet bei einzelnen großen Dateien deutlich besser ab – teilweise nahe an DuckDB – als bei vielen kleinen Dateien.
Praktische Implikationen
Für dieselben ~140 GB Daten:
- DuckDB: 1,3 GB Spitze (eine Datei) → 160 MB Spitze (72 kleine Dateien) – 8× Reduktion
- Polars: 17 GB Spitze (eine Datei) → 4,3 GB Spitze (72 kleine Dateien) – 4× Reduktion
Dateiorganisation ist nicht nur eine Performance-, sondern eine Speichersicherheitsfrage. Data-Engineering-Teams sollten Partitionierungsstrategien auch dann berücksichtigen, wenn keine verteilte Verarbeitung erforderlich ist. Eine monolithische 140-GB-Parquet-Datei kann in DuckDB problemlos funktionieren, bei Polars jedoch auf typischen Entwickler-Laptops zu OOM-Fehlern führen.
Diskussion
Speicherverhalten
Über alle Konfigurationen hinweg blieb der inkrementelle Spitzenspeicher von DuckDB unter etwa 2,5 GB, selbst bei der Verarbeitung von nahezu 2 TB Parquet-Daten. Polars zeigte dagegen bei sehr großen Eingabedateien deutlich höhere Spitzenwerte von bis zu rund 20 GB. Wurde derselbe Datensatz in viele kleinere Dateien partitioniert, sank der Spitzenverbrauch beider Engines erheblich.
Diese Ergebnisse decken sich mit der DuckDB-Dokumentation, die Parquet-Dateigrößen zwischen 100 MB und 10 GB empfiehlt. Interessanterweise profitierte Polars mit erzwungenem Async in diesem Setup nicht von der Partitionierung, sondern verlor relativ zu DuckDB sogar an Wettbewerbsfähigkeit.
Polars und große Dateien
Polars erreicht wettbewerbsfähige Laufzeiten, doch der Spitzenspeicher steigt bei großen Dateien stark an. Derselbe Workload wird deutlich speichereffizienter, wenn er in viele kleinere Dateien aufgeteilt wird, was darauf hindeutet, dass der Engpass in Polars bei Row-Group- und Seiten-Pufferung in Kombination mit multithreaded Scans und Dekompression liegt.
Bei Arrow-basiertem Parquet-Lesen erfolgt die Dekompression auf Seiten- und Column-Chunk-Ebene, wobei die Puffer beträchtlich sein können. Durch Partitionierung werden weniger Row Groups gleichzeitig verarbeitet, wodurch sich die maximale Summe aus Decode-, Maskierungs- und Materialisierungspuffern reduziert. Erzwungenes Async kann dieses Verhalten abmildern, indem es Geschwindigkeit gegen geringeren Speicherdruck eintauscht.
DuckDB erzwingt hingegen durch seinen Buffer Manager strikte Speichergrenzen. Obwohl auch DuckDB Parquet-Seiten dekodiert, ist sein Ausführungsmodell darauf ausgelegt, den Working Set begrenzt zu halten und Daten aggressiv zu streamen oder zu verdrängen, wodurch große temporäre Speicher-Spitzen vermieden werden.
Spitzen- vs. Endspeicherverbrauch
Polars allokiert während der Ausführung beträchtlichen temporären Speicher – vor allem für Scans, Dekompression und Zwischenpuffer – gibt diesen jedoch danach weitgehend wieder frei, sodass der Speicherverbrauch nach der Ausführung nahe am Ausgangsniveau liegt. DuckDB hält den Speicherverbrauch während der gesamten Ausführung enger begrenzt. In speicherbeschränkten Umgebungen ist der Spitzenverbrauch entscheidender als der Endverbrauch, da er das Risiko von Out-of-Memory-Fehlern bestimmt. Dieses Verhalten ist maßgeblich auf mmap zurückzuführen.
Polars mit erzwungenem Async verhält sich hier eher wie DuckDB, da es auf klassisches Read/Write-I/O setzt, das besser für Streaming-Workloads geeignet ist.
Row-Group-Größe
Die Row-Group-Größe spielt eine entscheidende Rolle für die Performance von DuckDB. DuckDB parallelisiert Parquet-Scans auf Row-Group-Ebene; Dateien mit nur einer großen Row Group können daher nur von einem Thread verarbeitet werden. DuckDB empfiehlt Row-Group-Größen von 100 000 bis 1 000 000 Zeilen und warnt, dass Größen unter 5 000 Zeilen die Performance um den Faktor 5–10 verschlechtern können.
Die Standard-Row-Group-Größe von Polars liegt bei etwa 250 000 Zeilen. Um dies zu berücksichtigen, haben wir Parquet-Dateien explizit mit einer Row-Group-Größe von 300 000 Zeilen erzeugt.
Optimierte Row Groups verbesserten die Laufzeit beider Engines deutlich:
Einfluss der Row-Group-Optimierung (2-TB-Datensatz):
| Konfiguration | Polars | DuckDB |
|---|---|---|
| Kleine Row Groups (Standard) | 170 s | 70 s |
| Optimierte Row Groups (300 k) | 60 s | 44 s |
| Verbesserung | 2,8× schneller | 1,6× schneller |
Auch der Speicherverbrauch von DuckDB sank deutlich, während er bei Polars relativ stabil blieb. Dies zeigt, dass selbst bei engine-agnostischen Formaten wie Parquet physische Layout-Entscheidungen bestimmte Ausführungs-Engines implizit begünstigen. Die Row-Group-Größe ist nicht nur ein Tuning-Parameter, sondern ein zentraler Performance-Faktor.
Speicherverbrauch ist nicht gleich „Speicherverbrauch“
Da Polars standardmäßig stark auf mmap setzt, ist es nicht vollständig korrekt, den beobachteten Speicherverbrauch ausschließlich der Engine selbst zuzuschreiben – ein großer Teil wird vom Betriebssystem verwaltet. Aus Anwendersicht ist diese Unterscheidung jedoch meist akademisch: Wenn ein Prozess eine bestimmte Menge Speicher belegt und dies zu einer Systembeschränkung wird, spielt es kaum eine Rolle, wo dieser Speicher technisch allokiert ist.
Der Einsatz von mmap in Polars ist kein Fehler und kein Argument gegen die Engine. Sowohl Polars als auch mmap sind etablierte Industriestandards mit guten Gründen für ihren Einsatz. Unser Benchmark macht diese Designentscheidung lediglich in einem sehr spezifischen Szenario als Nachteil sichtbar. Wie immer stellen Benchmarks nur einen Ausschnitt eines deutlich größeren Gesamtbildes dar und sollten mit Vorsicht interpretiert werden.
Fazit
DuckDB und Polars liefern beide starke Performance bei analytischen Workloads im Terabyte-Maßstab, bieten jedoch unterschiedliche Konfigurationen mit klaren Trade-offs zwischen Speichereffizienz und Durchsatz.
DuckDB priorisiert Speichereffizienz. Sein Buffer Manager erzwingt strikte Grenzen und hält den Spitzenspeicher selbst bei nahezu 2 TB Daten unter 2,5 GB. Das macht DuckDB zur sichereren Wahl für speicherbeschränkte Umgebungen, unvorhersehbare Workloads und produktive ETL-Pipelines, bei denen Out-of-Memory-Fehler inakzeptabel sind.
Polars (Standard) priorisiert Durchsatz. Die mmap-Strategie nutzt OS-seitiges Page-Caching für schnelle Lesezugriffe, tauscht jedoch Speicherverbrauch gegen Geschwindigkeit. Beim 140-GB-Einzelfall benötigte Polars 17 GB Spitzenspeicher, blieb aber laufzeitlich wettbewerbsfähig. Bei gut partitionierten Daten sinkt der Verbrauch auf 4,3 GB – immer noch höher als bei DuckDB, aber akzeptabel bei ausreichendem RAM. Diese Konfiguration eignet sich für DataFrame-orientierte Workflows auf gut partitionierten Datensätzen mit 64 GB+ RAM.
Polars mit erzwungenem Async bietet eine dritte Option. Durch Setzen von POLARS_FORCE_ASYNC=1 wechselt Polars von mmap zu klassischem I/O und erreicht eine Speichernutzung vergleichbar mit DuckDB (750 MB bei 140-GB-Dateien). Dies geht jedoch zulasten der Geschwindigkeit – der 2-TB-Workload benötigt 100 Sekunden gegenüber 60 Sekunden bei Standard-Polars und 44 Sekunden bei DuckDB. Paradoxerweise schneidet erzwungenes Async bei einzelnen großen Dateien besser ab als bei vielen kleinen und eignet sich für Szenarien, in denen große monolithische Dateien nicht repartitioniert werden können und der Speicher knapp ist.
Die Wahl der Konfiguration hängt von den Randbedingungen ab:
| Konfiguration | Spitzenspeicher (140 GB Datei) | Ausführung | Geeignet für |
|---|---|---|---|
| DuckDB | 1,3 GB | Am schnellsten | Speicherbeschränkt (16–32 GB), Produktionsstabilität |
| Polars (Standard) | 17 GB → 4,3 GB (partitioniert) | Schnell | Speicherreich (64 GB+), gut partitionierte Daten |
| Polars (Forced Async) | 750 MB | Am langsamsten | Speicherknapp, große Dateien, keine Repartitionierung möglich |
Die überraschendste Erkenntnis: Das Dateilayout beeinflusst den Speicherverbrauch stärker als die Wahl der Engine. Die Partitionierung desselben 140-GB-Datensatzes in 72 kleinere Dateien reduzierte den Speicherverbrauch von DuckDB um den Faktor 8 und von Standard-Polars um den Faktor 4. Data Engineers sollten die Organisation von Parquet-Dateien als erstklassige Architekturentscheidung betrachten – nicht als bloßes Speicherdetail.
Obwohl Parquet ein offenes, engine-agnostisches Format ist, beeinflussen physische Layout-Entscheidungen – Dateigröße, Row-Group-Größe, Kompression – das Ausführungsverhalten erheblich. Die Abstraktion ist durchlässiger, als oft angenommen. In der Praxis bleiben Storage und Compute eng gekoppelt.
Wenn Sie DuckDB-Funktionen in der Cloud nutzen möchten, empfehlen wir unseren On-Demand-Workshop „Hands-on Workshop: Introduction to MotherDuck“ für einen vollständigen praxisnahen Einstieg.
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Blog-Autor*in
Niklas Niggemann
Werkstudent Data & AI
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.