DuckDB vs. PySpark: Wann reicht die „kleine" Engine – und wie beide mit Airflow glänzen
Nicht jedes Datenproblem braucht einen Spark-Cluster. DuckDB erledigt überraschend viel auf einer einzelnen Maschine – schneller, günstiger und mit weniger Ops-Overhead. Eine Entscheidungshilfe für DataOps-Teams.
DuckDB vs. PySpark: Wann reicht die „kleine” Engine – und wie beide mit Airflow glänzen
TL;DR: Nicht jedes Datenproblem braucht einen Spark-Cluster. DuckDB erledigt überraschend viel auf einer einzelnen Maschine – schneller, günstiger und mit weniger Ops-Overhead. Dieser Artikel zeigt, wo die Grenze liegt, wie beide Engines in Airflow-Pipelines passen und warum die Antwort oft „beides” lautet.
Das Problem: Over-Engineering in Data Pipelines
In vielen Unternehmen ist PySpark der Default für alles, was nach „Datenverarbeitung” klingt. Ein 500-MB-CSV wird über einen Spark-Cluster mit 8 Nodes gejagt, weil „wir das halt so machen”. Das Ergebnis: hohe Infrastrukturkosten, langsame Entwicklungszyklen und ein Ops-Team, das mehr Zeit mit YARN-Konfiguration verbringt als mit eigentlichen Datenproblemen.
Die Gegenbewegung heißt DuckDB – eine eingebettete, analytische SQL-Datenbank, die direkt im Python-Prozess läuft. Kein Cluster, kein JVM-Startup, keine Shuffle-Partitionen. Aber: Ist das wirklich genug für produktive Data Pipelines?
DuckDB in 30 Sekunden
DuckDB ist eine spaltenorientierte OLAP-Datenbank, die In-Process läuft – vergleichbar mit SQLite, aber für Analytics. Die wichtigsten Eigenschaften:
- Kein Server nötig:
pip install duckdbund los. Keine Ports, keine Konfiguration, kein Cluster-Management. - Direkt auf Files arbeiten: Parquet, CSV, JSON, sogar S3-Pfade – alles ohne vorheriges Laden in eine Datenbank.
- Überraschend schnell: Durch vektorisierte Ausführung und intelligentes Memory-Management schlägt DuckDB auf Single-Node-Workloads regelmäßig Spark.
- SQL-first: Wer SQL kann, kann DuckDB. Kein neues API, keine DataFrame-Abstraktion notwendig (aber optional mit dem Python-Relational-API möglich).
import duckdb
# Direkt auf Parquet in S3 aggregieren – eine Zeile
result = duckdb.sql("""
SELECT
kunde_id,
DATE_TRUNC('month', bestelldatum) AS monat,
SUM(umsatz) AS monatsumsatz
FROM read_parquet('s3://data-lake/orders/*.parquet')
WHERE bestelldatum >= '2024-01-01'
GROUP BY ALL
ORDER BY monatsumsatz DESC
""").df()
PySpark: Wann es unverzichtbar bleibt
PySpark hat seinen Platz – und der ist klar definiert:
Datenvolumen jenseits von ~50–100 GB pro Job. DuckDB arbeitet auf einer einzelnen Maschine. Auch mit 64 GB RAM und NVMe-Spilling kommt irgendwann die physische Grenze. Spark verteilt die Last horizontal über beliebig viele Nodes.
Streaming und Near-Realtime. Structured Streaming in Spark ist ein ausgereiftes Framework für Kafka-Consumption, Windowed Aggregations und Event-Time-Processing. DuckDB hat hier schlicht kein Äquivalent.
Bestehende Spark-Ökosysteme. Wer Delta Lake, Unity Catalog oder ein Databricks-Setup betreibt, hat Spark tief integriert. Ein Wechsel auf DuckDB für einzelne Jobs kann sinnvoll sein, ein vollständiger Ersatz selten.
ML-Pipelines mit MLlib. Wenn Feature Engineering und Model Training in einem Spark-Job stattfinden, macht ein Bruch in der Engine wenig Sinn.
Die Entscheidungsmatrix
| Kriterium | DuckDB ✅ | PySpark ✅ |
|---|---|---|
| Datenmenge pro Job | < 50 GB | > 50 GB |
| Latenz-Anforderung | Batch (Minuten) | Streaming / Near-Realtime |
| Infrastruktur | Single Node / Container | Cluster (K8s, YARN, Databricks) |
| Team-Skillset | SQL-heavy | Python/Scala + Spark API |
| Ops-Overhead | Minimal | Erheblich (Cluster, JVM-Tuning) |
| Kosten pro Job | Cent-Bereich | Euro-Bereich |
| Entwicklungsgeschwindigkeit | Sehr hoch (lokales Testen) | Mittel (Cluster-Deployment) |
Die 50-GB-Grenze ist natürlich keine harte Zahl. Auf einer Maschine mit 128 GB RAM und schneller SSD verarbeitet DuckDB auch 200 GB – es kommt auf den Workload an. Aggregationen und Joins auf wenigen Spalten? DuckDB. Full Table Scans über hunderte Spalten mit komplexen UDFs? Spark.
Integration mit Apache Airflow
Hier wird es für DataOps-Teams interessant: Beide Engines lassen sich hervorragend in Airflow orchestrieren, aber auf unterschiedliche Weise.
DuckDB in Airflow: Leichtgewichtig und schnell
DuckDB läuft direkt im Airflow-Worker-Prozess. Kein externer Service, kein Warten auf Cluster-Startup. Das macht es ideal für die vielen „kleinen” Transformationen in einer Pipeline.
from airflow.decorators import dag, task
from pendulum import datetime
@dag(schedule="@daily", start_date=datetime(2024, 1, 1), catchup=False)
def sales_pipeline():
@task()
def transform_daily_sales(ds=None):
import duckdb
con = duckdb.connect()
# Direkt aus S3 lesen, transformieren, zurückschreiben
con.execute(f"""
COPY (
SELECT
kunde_id,
region,
SUM(umsatz) AS tagesumsatz,
COUNT(*) AS anzahl_bestellungen
FROM read_parquet('s3://lake/raw/orders/dt={ds}/*.parquet')
GROUP BY kunde_id, region
) TO 's3://lake/curated/daily_sales/dt={ds}/data.parquet'
(FORMAT PARQUET, COMPRESSION ZSTD)
""")
return {"status": "success", "date": ds}
@task()
def quality_check(ds=None):
import duckdb
result = duckdb.sql(f"""
SELECT
COUNT(*) AS rows,
COUNT(DISTINCT kunde_id) AS unique_kunden,
SUM(CASE WHEN tagesumsatz < 0 THEN 1 ELSE 0 END) AS negative_umsaetze
FROM read_parquet('s3://lake/curated/daily_sales/dt={ds}/*.parquet')
""").fetchone()
rows, unique, negatives = result
assert rows > 0, "Keine Daten vorhanden"
assert negatives == 0, f"{negatives} negative Umsätze gefunden"
return {"rows": rows, "unique_kunden": unique}
sales = transform_daily_sales()
quality_check()
sales_pipeline()
Vorteile dieser Kombination:
- Kein Cluster-Startup: Der Task startet sofort. Bei Spark-Jobs gehen oft 30–90 Sekunden allein für die Session-Initialisierung drauf.
- Geringer Ressourcenverbrauch: Ein DuckDB-Task braucht wenige hundert MB RAM. Auf Kubernetes lässt sich das Resource-Request entsprechend klein halten.
- Einfaches Debugging:
duckdb.sql()lokal ausführen, Ergebnis prüfen, deployen. Kein Remote-Debugging auf einem Cluster. - Perfekt für Data Quality Checks: Schnelle Validierungen zwischen Pipeline-Steps, ohne einen Spark-Job hochzufahren.
PySpark in Airflow: Für die schweren Jobs
Für große Transformationen bleibt PySpark die richtige Wahl – aber die Integration in Airflow ist aufwändiger:
from airflow.providers.apache.spark.operators.spark_submit import SparkSubmitOperator
spark_transform = SparkSubmitOperator(
task_id="heavy_aggregation",
application="/opt/spark-jobs/aggregate_clickstream.py",
conn_id="spark_k8s",
conf={
"spark.executor.memory": "8g",
"spark.executor.instances": "4",
"spark.sql.shuffle.partitions": "200",
},
dag=dag,
)
Das funktioniert, bringt aber Ops-Komplexität: Spark-Operator auf Kubernetes, Driver-Pod-Konfiguration, Dynamic Allocation Tuning, Log-Aggregation. Alles lösbar, aber alles Aufwand.
Das Best-of-Both-Worlds-Pattern
In der Praxis bewährt sich ein hybrider Ansatz, der die Stärken beider Engines nutzt:
[S3 Raw Data]
│
▼
┌─────────────┐ Kleine/mittlere Dateien
│ DuckDB │◄─── Validierung, Bereinigung,
│ Task │ einfache Aggregationen
└──────┬──────┘
│
▼
┌─────────────┐ Große Joins, komplexe
│ PySpark │◄─── Transformationen über
│ Job │ das gesamte Data Lake
└──────┬──────┘
│
▼
┌─────────────┐ Finale Aggregationen,
│ DuckDB │◄─── Reports, Data Quality,
│ Task │ Export nach PostgreSQL
└─────────────┘
DuckDB übernimmt die schnellen, kleinen Schritte – Validierung, Datenbereinigung, finale Aggregationen, Export in die Serving Layer. PySpark wird nur für die Tasks eingesetzt, die tatsächlich verteilte Verarbeitung brauchen.
Das reduziert die Cluster-Laufzeit (und damit die Kosten) erheblich, weil der Spark-Cluster nur für die wirklich rechenintensiven Schritte hochgefahren wird.
Kostenvergleich: Ein realistisches Beispiel
Szenario: Tägliche Pipeline, die 10 GB Rohdaten transformiert, aggregiert und in PostgreSQL lädt.
Variante A – Alles auf Spark (selbst gehostet auf K8s):
- 4 Executor Pods à 4 CPU / 8 GB RAM, ~15 Minuten Laufzeit
- Driver Pod: 2 CPU / 4 GB RAM
- Geschätzte Compute-Kosten: ~0,80 € pro Run
- Ops-Aufwand: Spark Operator, Monitoring, JVM-Tuning
Variante B – DuckDB im Airflow-Worker:
- 1 Pod mit 4 CPU / 16 GB RAM, ~8 Minuten Laufzeit
- Geschätzte Compute-Kosten: ~0,15 € pro Run
- Ops-Aufwand: Praktisch keiner über den Airflow-Worker hinaus
Ersparnis über ein Jahr: ~235 € Compute + erheblich reduzierter Ops-Aufwand. Bei 20 solcher Pipelines summiert sich das schnell auf einen fünfstelligen Betrag – plus die gewonnene Engineering-Zeit.
Stolperfallen und Lessons Learned
DuckDB ist nicht thread-safe im Write-Modus. Eine Connection kann nur von einem Thread geschrieben werden. In Airflow mit dem @task-Decorator ist das kein Problem (jeder Task ist ein eigener Prozess), aber bei Custom Operators aufpassen.
Memory-Management aktiv steuern. DuckDB versucht, möglichst viel in den RAM zu laden. Bei knappen Worker-Ressourcen das Memory-Limit explizit setzen:
con = duckdb.connect()
con.execute("SET memory_limit = '8GB'")
con.execute("SET temp_directory = '/tmp/duckdb_spill'")
S3-Credentials in DuckDB. DuckDB nutzt die httpfs-Extension für S3-Zugriff. In einer Kubernetes-Umgebung mit IAM Roles for Service Accounts (IRSA) funktioniert das out-of-the-box. Alternativ:
con.execute("""
SET s3_region = 'eu-central-1';
SET s3_endpoint = 'minio.internal:9000';
SET s3_use_ssl = false;
SET s3_url_style = 'path';
""")
Parquet als gemeinsames Format. Beide Engines arbeiten exzellent mit Parquet. Wer den hybriden Ansatz fährt, sollte Parquet (oder Iceberg/Delta) als Zwischenformat standardisieren – das macht den Wechsel zwischen Engines nahtlos.
Quiz: Welche Engine würdest du einsetzen?
Teste dein Wissen – 10 reale Szenarien aus dem DataOps-Alltag:
DuckDB vs. PySpark
Euer Team muss täglich 8 GB CSV-Dateien aus einem S3 Bucket aggregieren und das Ergebnis als Parquet zurückschreiben.
Fazit: Die richtige Engine für den richtigen Job
Die Frage ist nicht „DuckDB oder PySpark” – es ist „DuckDB und PySpark, jeweils dort, wo sie hingehören”. In einer gut designten Airflow-Pipeline ergänzen sich beide Engines:
DuckDB für alles unter ~50 GB, für Data Quality Checks, für schnelle Aggregationen und für den letzten Schritt in die Serving Layer. PySpark für die schweren Batch-Jobs, für Streaming und für Workloads, die horizontal skalieren müssen.
Der größte Gewinn liegt oft nicht in der reinen Compute-Ersparnis, sondern in der reduzierten Ops-Komplexität. Ein DuckDB-Task in Airflow ist ein Python-Script. Ein Spark-Job ist ein verteiltes System. Diesen Unterschied im Ops-Aufwand unterschätzen die meisten Teams.
Martin Stagl ist Systems Engineer und Data Scientist. Er betreibt On-Premises-Infrastruktur auf Kubernetes und berät Unternehmen zu DSGVO-konformen Datenlösungen. Mehr unter stagl.systems.