Wir verwenden DuckDB für zahlreiche der Datenprodukte, die wir für unsere Kunden entwickeln und verwalten. Als „Datenbank im Prozess“ hält sie unsere Systemlandschaft schlank, was es uns ermöglicht, Kundenanforderungen ohne großen Aufwand zu entwickeln.
Der eigentlich interessante Teil ist das, was DuckDB technisch anders macht als klassische Warehouses und verteilte Engines. Ebendarum bietet der Rest dieses Artikels einen technischen Einblick in die Struktur eines der derzeit wohl populärsten Datentools.
DuckDB ermöglicht schnelle und “spaßige” Datenanalyse, ohne dass man große Apache-Spark-Cluster einrichten oder ein Cloud-Data-Warehouse betreiben muss, wenn man nur ein paar hundert Gigabyte Daten verarbeitet. Aber auch für größere Datenmengen eignet es sich ebenso gut.
Das schrieb AWS jüngst zur Übernahme von DuckLabs:
"For almost a decade, the DuckDB team has worked with the idea of building a tool that lets people work with data, with confidence. I find that sentiment—not just making data easy to work with, but actively empowering people to ask questions, build, and really innovate with data—is exactly in line with how AWS and our customers think about data," said Andy Warfield, Amazon vice president and distinguished engineer. "We’ve been working closely with the DuckLabs team for the past two years, and I am so excited at the opportunity to work with them even more closely."
Was „Datenbank im Prozess“ konkret bedeutet
Die überwiegende Mehrheit der Datenbanken, darunter Postgres, MySQL, Snowflake und Spark, teilt Anwendung und Datenbank in zwei verschiedene Prozesse. Der Client einer Anwendung stellt eine Verbindung über einen Socket her, sendet eine Anfrage und wartet auf die Antwort.
Bei DuckDB verhält sich das anders.
Es wird als Bibliothek in den Prozess der Anwendung selbst eingebunden, wobei es egal ist, ob die Anwendung Python, C++, Node.js, R oder direkt als CLI läuft. Es existiert kein Port, kein Verbindungspool, kein Auth-Handshake und kein Server, mit dem man sich verbinden müsste.
Zugegebenermaßen ist das schon recht gewöhnungsbedürftig und seltsam, wenn man es jahrelang anders gewohnt war.
Ein Beispiel für eine Ausführung einer Datenbank in Python:
import duckdb
r1 = duckdb.sql("SELECT 42 AS i")
duckdb.sql("SELECT i * 2 AS k FROM r1").show()Dies führt die Abfrage unter Verwendung einer In-Memory-Datenbank aus, die global innerhalb des Python-Moduls gespeichert ist. Das Ergebnis der Abfrage wird als „Relation“ zurückgegeben. Eine Relation ist eine symbolische Darstellung der Abfrage. Die Abfrage wird erst dann ausgeführt, wenn das Ergebnis abgerufen oder dessen Ausgabe auf dem Bildschirm angefordert wird (lazy execution).
DuckDB basiert in etwa auf dem gleichen Grundgedanken wie SQLite, was sich als Tool für ein flexibles OLTP (Online Transaction Processing) bei Entwicklern großer Beliebtheit erfreut. Nur ist DuckDB für einen anderen Workload gedacht: SQLite ist auf schnelle, punktuelle Transaktionen optimiert (OLTP), DuckDB auf große analytische Scans – OLAP (Online Analytical Processing).
Beide teilen sich das Deployment-Modell – eine einzelne Datei, kein Serverbetrieb –, aber ihre internen Speicher- und Ausführungsmodelle sind komplett unterschiedlich gebaut.
Daten lesen und schreiben: SQL über Systemgrenzen hinweg
Daten können in unterschiedlichen Formaten persistiert oder gelesen werden. Während bei anderen Datenbanksystemen die Speicherung der Daten eng mit der Ausführungsengine verknüpft ist, nutzt hier DuckDB das Prinzip der Trennung von Datenformat und Engine, wie wir es zum Beispiel auch bei Spark und anderen Cloud Warehouses finden.
Native werden zahlreiche Datenformate unterstützt:
import duckdb
duckdb.read_csv("example.csv") # read a CSV file into a Relation
duckdb.read_parquet("example.parquet") # read a Parquet file into a Relation
duckdb.read_json("example.json") # read a JSON file into a Relation
duckdb.sql("SELECT * FROM 'example.csv'") # directly query a CSV file
duckdb.sql("SELECT * FROM 'example.parquet'") # directly query a Parquet file
duckdb.sql("SELECT * FROM 'example.json'") # directly query a JSON fileEine der Eigenschaften von DuckDB ist die Fähigkeit, per Extension direkt gegen fremde Datenquellen zu scannen, ohne die Daten vorher zu materialisieren. D.h., dass hier die Optimierungen bei der Anfrage, wie z. B. die Daten entsprechend zu filtern (Predicate Pushdown), direkt übernommen werden.
Über postgres_scan etwa lässt sich eine produktive Postgres-Tabelle mit einer lokalen CSV-Datei joinen, in einer einzigen Abfrage:
SELECT u.id, u.email, s.zielwert
FROM postgres_scan('host=localhost dbname=prod', 'public', 'users') u
JOIN read_csv_auto('stakeholder_daten.csv') s ON u.id = s.user_id;Das funktioniert nicht nur mit Postgres, sondern über weitere Extensions auch mit MySQL, SQLite, entfernten Parquet- und JSON-Dateien auf S3, sowie über Iceberg- und Delta-Lake-Extensions direkt gegen Lakehouse-Tabellen:
SELECT count(*), min(signup_date)
FROM read_parquet('s3://bucket/rohdaten/*.parquet')
WHERE jahr = 2024;Dabei werden Filter- Predicate-Pushdowns auch auf die entfernte Quelle angewendet, sodass nicht unbedingt alle Spalten und Zeilen über das Netz gezogen werden müssen, um sie dann lokal zu filtern.
DuckDB verfügt über zahlreiche Erweiterungen, unter anderem auch Excel:
import duckdb
duckdb.sql("INSTALL excel;")
duckdb.sql("LOAD excel;")
duckdb.sql("""
SELECT
date_part('year', strptime(time, '%Y-%m-%d %H:%M:%S')) as year,
CAST(temp AS DOUBLE) as temp,
CAST(coco AS INTEGER) as coco
FROM read_xlsx('2026-07-20_meteostat_export.xlsx', all_varchar=True, header=True);
""")Spaltenorientierte Speicherung und Row Groups
Daten werden in DuckDB, so wie eigentlich auch in jeder anderen „Analytical Processing Engine“, spaltenweise, nicht zeilenweise, verarbeitet.
Der Unterschied ist bei analytischen Queries entscheidend. Wenn du zum Beispiel
SELECT avg(betrag) FROM rechnungen WHERE jahr = 2025ausführst, muss die Engine nur die Spalten betrag und jahr lesen und nicht jede einzelne Zeile mit sämtlichen anderen Feldern, die für diese Query irrelevant sind. Bei einer zeilenorientierten Datenbank würden dagegen zwangsläufig alle Spalten mitgelesen werden.
Intern organisiert DuckDB die Daten in sogenannten Row Groups von jeweils rund 120.000 Zeilen. Jede Row Group trägt Metadaten (Min/Max-Werte pro Spalte, Null-Counts), die der Optimizer nutzt, um ganze Blöcke zu überspringen, wenn sie für eine Query offensichtlich irrelevant sind – sogenanntes Zone Mapping bzw. Predicate Pushdown. Bei einer Filterbedingung wie WHERE jahr = 2025 muss die Engine dadurch oft nur einen Bruchteil der physisch gespeicherten Daten überhaupt anfassen.
Das lässt sich in etwa mit der Partitionierung bei Parquet vergleichen, wo eine Gruppeneinteilung von Parquet-Files typischerweise in Ordnern und „Row Groups“ in den Files stattfindet.
DuckDB & Parquet
Der Parquet-Reader von DuckDB kommt ohne externe Abhängigkeiten aus und kann SQL-Abfragen direkt auf Parquet-Dateien ausführen – ganz ohne vorherigen Import- oder Analyseschritt. Dank des spaltenorientierten Formats von Parquet geschieht dies zudem zügig.
DuckDB liest Parquet-Dateien im Streaming-Verfahren ein; dadurch lassen sich auch Abfragen auf großen Dateien durchführen, die nicht vollständig in den Arbeitsspeicher passen.
Spalten und Zeilen, welche für eine bestimmte Abfrage benötigt werden, werden automatisch erkannt. Dies ermöglicht die Analyse weitaus größerer und komplexerer Parquet-Dateien, ohne dass manuelle Optimierungen oder zusätzliche Hardware- erforderlich sind.
Ein weiterer Vorteil: DuckDB erledigt all dies mittels Parallelverarbeitung und kann dank Glob-Syntax mehrere Parquet-Dateien gleichzeitig verarbeiten:
con.execute("""
SELECT *
FROM 'taxi/*.parquet'
LIMIT 5""").df()Vektorisierte Ausführung statt Zeile-für-Zeile
Klassische Datenbank-Engines arbeiten häufig nach dem sogenannten Volcano-Modell: Jeder Operator im Ausführungsplan verarbeitet eine Zeile, reicht sie an den nächsten Operator weiter, dann die nächste Zeile. Das erzeugt massiven Function-Call-Overhead, wenn Millionen Zeilen durch einen mehrstufigen Plan laufen.
DuckDB verarbeitet stattdessen Vektoren - Batches von typischerweise rund 2.048 Werten gleichzeitig. Ein Operator bekommt nicht eine Zeile, sondern einen ganzen Vektor, verarbeitet ihn mit SIMD-freundlichem Code und reicht den Vektor als Batch weiter. Das reduziert nicht nur den Overhead pro Zeile drastisch, es erlaubt der CPU auch, moderne Vektor-Instruktionen tatsächlich auszunutzen, statt ständig zwischen Interpreter-Schritten zu pendeln. Dieses Ausführungsmodell – kombiniert mit einem parallelisierbaren, morsel-artigen Scheduling über mehrere CPU‑Kerne – ist der Hauptgrund, warum DuckDB auf Single-Node-Hardware so nah an die Performance verteilter Systeme herankommt, ohne deren Koordinations-Overhead zu haben.
Out-of-Core-Verarbeitung: größer als der RAM
Der Punkt, an dem viele In-Memory-Tools kapitulieren, ist ein Datensatz, der größer ist als der verfügbare Arbeitsspeicher. DuckDB löst das über Streaming- und Spill-to-Disk-Mechanismen: Aggregationen, Joins und Sortierungen, die den RAM sprengen würden, werden in Teilen verarbeitet und Zwischenergebnisse bei Bedarf temporär auf die Festplatte ausgelagert, statt mit “Out of Memory” abzubrechen.
Ein Vergleichstest von Daniel Beach illustriert, wie belastbar das in der Praxis ist: 650 GB Rohdaten aus einer Delta-Lake-Tabelle auf S3, verarbeitet auf einer EC2-Instanz mit lediglich 32 GB RAM und 16 vCPUs — also knapp 5 % des Datenvolumens an Arbeitsspeicher.
DuckDB kam nach 16 Minuten durch, inklusive korrekter Verarbeitung von Deletion Vectors, mit der Polars zu diesem Zeitpunkt noch Schwierigkeiten hatte.
Ein Single-Node-PySpark-Setup auf vergleichbarer Hardware brauchte über eine Stunde, unter anderem weil Spark ohne manuelles Tuning (etwa an spark.sql.shuffle.partitions) nicht für diese Konstellation ausgelegt ist.
Für die Einordnung ist wichtig zu verstehen, dass das kein kontrollierter Benchmark Test unter Laborbedingungen ist, sondern ein einzelner, transparent dokumentierter Testlauf.
Es zeigt aber ein Muster, das sich in eigenen Projekten regelmäßig bestätigt – auf Datengrößen deutlich unterhalb des Petabyte-Bereichs verliert die Koordination eines verteilten Systems gegen ein gut gebautes Single-Node-Tool.
Zero-Copy-Integration mit dem Python/Arrow-Ökosystem
DuckDB arbeitet eng mit Apache Arrow zusammen. Für Pandas- und Polars-DataFrames sowie Arrow-Tables kann DuckDB SQL direkt auf den In-Memory-Objekten ausführen, ohne die Daten vorher zu serialisieren oder zu kopieren:
import duckdb
import pandas as pd
df = pd.read_parquet("umsatz.parquet")
result = duckdb.sql("SELECT region, sum(umsatz) FROM df GROUP BY region").df()Für diesen Fall arbeitet DuckDB im Zero-Copy-Modus - der Speicher wird nicht dupliziert, sondern DuckDB liest direkt auf dem Arrow-Buffer. Das macht DuckDB zu einem SQL-Layer über bestehenden Python Datenstrukturen, statt zu einer weiteren Datenkopie, die separat zu pflegen ist.
DuckLake: Lakehouse-Features ohne Iceberg-Komplexität
Ein neueres Projekt aus dem DuckDB-Umfeld ist DuckLake, ein offenes Tabellenformat, das Lakehouse-typische Eigenschaften - ACID-Transaktionen, Schema Evolution, Time Travel — bereitstellt, ohne den vollen Funktionsumfang (und die Betriebskomplexität) von Apache Iceberg oder Delta Lake vorauszusetzen.
Der wesentliche Unterschied ist, dass DuckLake seine Metadaten nicht in einer eigenen Katalog-Infrastruktur speichert, sondern in einer regulären SQL-Datenbank, etwa Postgres.
Für Teams, die ohnehin schon eine relationale Datenbank betreiben, entfällt damit ein zusätzlicher Metadaten-Service, den man separat hochverfügbar machen müsste.
Im Browser: DuckDB-WASM
Über eine WebAssembly-Kompilierung läuft DuckDB auch vollständig im Browser, clientseitig, ohne Serveranbindung.
Das wird bereits produktiv genutzt, etwa im Dataset Explorer von Hugging Face, um Nutzern SQL-Abfragen auf öffentlichen Datensätzen zu erlauben, ohne dass jede Filter-Interaktion eine Anfrage an einen Server auslöst.
Dadurch wird die Serverlast für Anbieter von z.B. öffentlichen Datensätzen strukturell reduziert, da die Rechenlast auf die Clients verteilt wird.
Wo die Architektur an ihre Grenzen stößt
Zwei Einschränkungen sind für die hier präsentierte Einordnung wichtig:
Single-Writer Model
DuckDB erlaubt zu jedem Zeitpunkt genau einen schreibenden Prozess auf einer Datenbankdatei. Mehrere Prozesse können gleichzeitig lesen, aber ein paralleles Schreiben mehrerer unabhängiger Prozesse auf dieselbe Datei ist architektonisch nicht vorgesehen. Für Multi-User-Schreibzugriffe über mehrere Anwendungen hinweg braucht es entweder eine Anwendungsschicht, die Schreibzugriffe serialisiert, oder eine Erweiterung wie MotherDuck, die eine Cloud-Koordinationsschicht über DuckDB legt.
Kein OLTP
Die vektorisierte, batch-orientierte Ausführung, die DuckDB bei analytischen Scans so schnell macht, ist für hochfrequente Einzeltransaktionen – ein Bestellsystem mit tausenden kleinen Writes pro Sekunde etwa – nicht die richtige Wahl. Eine klassische OLTP-Datenbank wie Postgres ist hier deutlich besser geeignet. DuckDB und Postgres schließen sich deshalb nicht aus, sie lösen unterschiedliche Probleme – und lassen sich über Extensions wie pg_duckdb sogar kombinieren, sodass Postgres analytische Queries an die DuckDB-Engine delegiert.
Wo das in unseren Datenprodukten landet
Wenn wir für einen Kunden Datenprodukte bauen, ist das meistens eine Kombination aus automatisierter Datenaufbereitung von ein bis zwei angebundenen Quellsystemen und einem Reporting- oder KI‑Layer obendrauf. DuckDB übernimmt dabei bei uns typischerweise die Verarbeitungsschicht zwischen Rohdaten und Auslieferung.
Konkret sieht das in der Regel so aus:
Ingestion & Transformation: Statt einer eigenen Orchestrierungs-Infrastruktur läuft die Kernlogik als DuckDB-Prozess, angestoßen per Cronjob oder Lambda-Funktion, der über Extensions direkt gegen ERP-Exporte, CSV-Uploads oder S3-Buckets liest, transformiert und das Ergebnis als Parquet- oder Delta-Table zurückschreibt. Das ersetzt in vielen Projekten eine dedizierte ETL-Orchestrierung, die für das tatsächliche Datenvolumen überdimensioniert wäre.
Speicherschicht im Produkt selbst: Weil eine DuckDB-Datenbank eine einzelne Datei ist, lässt sie sich sauber versionieren und als Artefakt zwischen Entwicklungs-, Test- und Produktionsumgebung durchreichen, ohne separate Datenbank-Instanz, die wir beim Kunden betreiben oder absichern müssten. Das reduziert die Zahl der Systeme, die nach Go-Live gewartet werden müssen, was bei Kunden ohne eigenes Data-Team der eigentliche Engpass ist, nicht die Rechenleistung.
Speed Layer vor bestehenden Warehouses: Läuft beim Kunden bereits ein Warehouse wie Snowflake, setzen wir DuckDB gelegentlich als vorgelagerte Aggregationsschicht ein, die die für ein Dashboard relevanten Daten einmalig exportiert und lokal vorhält - die interaktive Filterung im Frontend verursacht dann keine laufenden Abfragekosten mehr im Warehouse.
RAG- und KI-Pipelines: In Projekten mit einer Retrieval-Komponente übernimmt DuckDB häufig die Vorverarbeitung und Filterung der Rohdaten, bevor sie in eine Vektordatenbank wandern - die Zero-Copy-Integration mit Arrow/Pandas macht diesen Schritt in der Python-Pipeline günstig, ohne eine zusätzliche Datenkopie zu erzeugen.
Der gemeinsame Nenner: In keinem dieser Fälle betreiben wir für den Kunden eine zusätzliche Server-Komponente, die separat gepatcht, skaliert oder überwacht werden müsste. Das ist ein direkter Ausfluss der oben beschriebenen In-Process-Architektur — nicht ein Feature, das wir DuckDB andichten, sondern die logische Konsequenz aus einem Engine-Design, das für genau diese Klasse von Datenvolumen gebaut ist.





