2025-10-13
Streaming-Muster mit DuckDB
Guillermo Sanchez
Die Wörter „DuckDB“ und „Streaming“ landen selten im selben Satz. Vielleicht, weil DuckDB als mächtige (aber sehr leichte) OLAP-Datenbank positioniert wurde. Oder weil das Ökosystem der Streaming-Analytics um Namen wie Kafka, Flink und Spark Streaming kreist und zuletzt um Spieler, die das Spiel ändern wollen, wie Materialize oder RisingWave. Aber kann DuckDB im Kontext von Streaming-Analytics genutzt werden? Was ist Streaming-Analytics überhaupt?
Streaming-Analytics-Muster
Die einfachste Definition: Streaming-Analytics ist der Akt, eine analytische Sicht auf Ihre Daten nahezu in Echtzeit zu aktualisieren, sobald neue Daten eintreffen. Wenn zum Beispiel gerade drei neue Sessions auf Ihrer Website gestartet sind, ist das Sammeln dieser Session-Events und das Aktualisieren des Zählers (+3) Streaming-Analytics. Streaming-Analytics ist meiner bescheidenen Meinung nach nicht nur das Einfügen dieser 3 Session-Events in eine Tabelle – das wäre eher eine transaktionale Workload. Streaming-Analytics ist auch nicht, diese Events in ein Kafka-Topic zu schieben und in ein anderes System zu sinken. Wenn Sie die analytische Sicht auf Ihre Daten nicht aktualisieren, würde ich es nicht Streaming-Analytics nennen.
Jetzt, da wir eine Definition haben, schauen wir uns drei gängige Architektur-Muster in der Streaming-Analytics an. Die Namen für diese Muster habe ich selbst gewählt, aber sie helfen, sie voneinander zu unterscheiden.
-
Im Materialized-View-Muster ist es üblich, ein Cloud Data Warehouse mit Unterstützung für materialisierte Sichten zu nutzen (etwa BigQuery oder Snowflake). Der Event-Stream wird in der Regel in eine Roh-Tabelle gesenkt, und darüber wird eine materialisierte Sicht erzeugt. Dieses Muster gilt allgemein als höherlatenter als die nächsten beiden. Es gibt allerdings nicht so viele Benchmarks, um das schlüssig zu belegen.
-
Das Streaming-Engine-Muster nutzt den klassischeren ETL-Ansatz. Ein separater Prozess mit einer Streaming-Engine konsumiert die Nachrichten von der Quelle, Abfragen laufen on the fly, und die Ergebnisse landen in einer persistierten Tabelle. Gängige Engines sind Spark Streaming, Flink, Kafka Streams oder zuletzt Arroyo. Das kam traditionell mit einer Reihe von Komplikationen (Watermarks, State Management, höherer Speicherbedarf für unendliche Queries usw.).
-
Das Streaming-Database-Muster ist vom Latenzverhalten her ähnlich wie das vorherige, vereinfacht die Erfahrung aber drastisch. Streaming-Datenbanken wie RisingWave oder Materialize können direkt von der Streaming-Quelle lesen und Ihre materialisierte Sicht on the fly aktualisieren. Sie zielen auf ACID-Konsistenz und erlauben Clients, Daten über das PostgreSQL-Wire-Protokoll abzufragen.
„Wo passt DuckDB da rein?“ – mögen Sie fragen. Nun, DuckDB passt gut zu Muster eins und zwei. Auch wenn DuckDB materialisierte Sichten (noch) nicht unterstützt, können wir uns um diese Einschränkung herumarbeiten und diese Muster trotzdem mit sehr guten Ergebnissen umsetzen.
Interessanterweise hat die Streaming-Engine-Branche nicht viele offizielle Benchmarks. Der Nexmark-Benchmark scheint der gängigste zu sein, aber es gibt nicht viele veröffentlichte Ergebnisse, die Engines damit vergleichen.
Materialized-View-Muster: Unsere eigene materialisierte Sicht mit DuckDB kochen
Wir wissen, dass DuckDB sehr schnell Daten on the fly aggregiert und sich auch bei transaktionalen Workloads (für ein OLAP-System) gut schlägt. Das gilt auch für DuckLakes Lakehouse-Format dank Data Inlining. In diesem Abschnitt sehen wir DuckDB und DuckLake in Aktion: als Sink für Kafka und beim Berechnen neuer Metrikwerte auf Basis von Deltas.
Alle Muster tun dasselbe auf unterschiedliche Weise: Events aus einem Kafka-Topic lesen und die analytische Sicht aktualisieren, die eine persistierte Tabelle oder eine Sicht über einer Roh-Tabelle sein kann. Was dazwischen passiert, unterscheidet die Muster.
Deltas mit DuckDB abfragen
Die Schlüsselkomponente in diesem Diagramm ist das, was ich „Delta Processor“ nenne. Diese Komponente ist im Wesentlichen eine Funktion, die periodisch eine Schleife dreht und eine Abfrage ausführt, um neue Daten in der Tabelle raw_events zu aggregieren und die analytische Sicht zu aktualisieren, hier eine persistierte Tabelle namens user_clicks. Das ist die Abfrage, die periodisch user_clicks mit dem neuen Delta aktualisiert:
MERGE INTO user_clicks AS destUSING ( SELECT user_id, user_name, count(*) AS count_of_clicks, max(timestamp) AS updated_at FROM raw_events WHERE event_type = 'CLICK' AND (⟨LATEST_UPDATED_AT⟩ IS NULL OR timestamp > ⟨LATEST_UPDATED_AT⟩) GROUP BY user_id, user_name) AS srcON dest.user_id = src.user_idWHEN MATCHED THEN UPDATE SET count_of_clicks = dest.count_of_clicks + src.count_of_clicks, updated_at = src.updated_atWHEN NOT MATCHED THEN INSERT (user_id, user_name, count_of_clicks, updated_at) VALUES (src.user_id, src.user_name, src.count_of_clicks, src.updated_at);Die vollständige Pipeline finden Sie in diesem Repository.
DuckLakes Change Data Feed nutzen
Dieses Muster ist 1.1 sehr ähnlich, mit einigen DuckLake-Besonderheiten:
- Wir nutzen DuckLakes Data Inlining, um Einfügen zu beschleunigen, ohne zu viele kleine Dateien zu schreiben.
- Die Delta-Processor-Komponente kann DuckLakes Data Change Feed nutzen, um unnötiges Scannen zu vermeiden.
- Es gibt eine Extra-Komponente, den „Inline Flusher“, der periodisch inline-Daten aus dem Metadata Catalog in Parquet-Dateien der angegebenen Dateigröße (standardmäßig 512 MB) spült. Das ist eine Wartungsoperation, die DuckLake performant hält.
Die vollständige Pipeline finden Sie in diesem Repository.
Um Filter-Pushdowns und File Pruning besser zu nutzen, empfiehlt sich eine Partitionierung der Daten nach
timestamp
Streaming-Engine-Muster: Streaming-Engines und DuckDB
Die etablierten Streaming-Engines (Spark Streaming, Flink, Kafka Streams) sind JVM-basiert. Sie können daher Daten über das JDBC-Protokoll in DuckDB einfügen. Dieses Muster ist in der Regel etwas schwieriger zu betreiben. Lange laufende Streaming-Queries verbrauchen oft viel Speicher, und unterbrochene Streaming-Queries neu zu starten lässt mich immer einen Herzschlag aussetzen. Es kann aber eine sehr latenzarme Lösung für sehr große Datenströme sein.
Spark Streaming nutzen und in DuckDB sinken
In diesem Diagramm sehen wir, dass die meisten Komponenten von der Spark-Streaming-Runtime verwaltet werden. In Spark Streaming steckt all das in einer Streaming Query. Wenn der Micro-Batching-Modus genutzt wird (wie in diesem Beispiel), können Sie der Writer-Funktion eine eigene Funktion übergeben, mit der Sie jeden Batch so schreiben, wie Sie möchten. In unserem Fall nutzen wir einfach eine JDBC-Verbindung und überschreiben die Zieltabelle (user_clicks).
Wir sehen auch, dass keine Zwischenergebnisse gespeichert werden, das heißt in diesem konkreten Fall gibt es keine Tabelle raw_events. Das ist kein Muster, das ich liebe, weil ich für Audit-Zwecke die Rohdaten speichern würde, um sicherzustellen, dass mein Streaming-Job nichts Seltsames macht. In diesem Fall verlässt sich Spark Streaming auf Checkpoints, um den Zustand zu halten und sicherzustellen, dass Daten genau einmal verarbeitet werden und Queries neu starten können, ohne Daten aus dem Kafka-Topic zu verlieren oder zu duplizieren.
Die vollständige Pipeline finden Sie in diesem Repository.
Bonus: Die DuckDB-Tributary-Erweiterung, um Kafka direkt abzufragen
Dieses Setup kommt dem Streaming-Database-Muster am nächsten, das Sie derzeit mit DuckDB umsetzen können. Getrieben von der tributary-DuckDB-Community-Extension können Sie eine Sicht oder Tabelle anlegen, die direkt aus einem Kafka-Topic liest. Um materialisierte Sichten zu simulieren, nutzen wir in diesem Beispiel Sichten. Die folgende Abfrage zeigt, wie einfach das ist:
CREATE VIEW IF NOT EXISTS raw_events_view AS SELECT * EXCLUDE message, decode(message)::JSON AS message FROM tributary_scan_topic(⟨TOPIC⟩, "bootstrap.servers":="localhost:9092");Derzeit hat diese Erweiterung kein State Management. Jedes Mal, wenn diese Sicht abgefragt wird, würden wir das ganze Topic ab Offset 0 lesen. Das ist nicht ideal, weil Kafka eine begrenzte Retention-Policy hat und irgendwann Nachrichten zu flushen beginnt. Ein Ausweg ist, diese Nachrichten in Tabellen zu materialisieren und den Offset (oder einen Zeitstempel) zu nutzen, um festzuhalten, was bereits ingestiert wurde.
Die vollständige Pipeline finden Sie in diesem Repository.
Das ist eine experimentelle Erweiterung von Query.Farm.
Einige Gedanken
Schlussfolgerungen wirken immer sehr subjektiv, deshalb schreibe ich lieber über ein paar Gedanken zu Streaming-Mustern im Allgemeinen und besonders rund um DuckDB.
Das Materialized-View-Muster ist meistens gut genug. Mein Hot Take: Die meisten Analytics-Use-Cases sind durch das Materialized-View-Muster abgedeckt, ohne die Komplexität der anderen Muster. Ich glaube, DuckDB eignet sich dafür sehr gut, weil es als kleines OLAP unglaublich gut große Mengen Streaming-Inserts verarbeitet. In diesem Artikel wurde DuckDB an die Grenze gebracht und konnte mehr als eine Million Zeilen pro Sekunde aufnehmen. Außerdem stehen materialisierte Sichten auf der langfristigen DuckDB-Roadmap, sodass dieses Muster in naher Zukunft noch einfacher wird.
Wenn Sie in ein Lakehouse streamen, sollten Sie wissen, dass DuckLakes Data-Inlining-Feature genau dafür gebaut wurde: hohe Insert-Durchsätze bei gleichzeitiger Lösung des Small-File-Problems. Das macht DuckLake zu einem starken Kandidaten für dieses Muster, wenn Sie eine lakehouse-artige Architektur haben.
Streaming-Engines und Streaming-Datenbanken können schwer (oder teuer) sein. Im großen Maßstab können Streaming-Engines schwer zu betreiben sein. Das Feld entwickelt sich, und es wird daran gearbeitet, ewig laufende Streaming-Queries leichter zu machen. Apache Fluss etwa wird mit der Idee gebaut, einige der in diesem Beitrag beschriebenen Schwächen zu lösen. Es fügt aber eine weitere Komplexitätsschicht zu einer schon komplexen Streaming-Architektur hinzu.
Streaming-Datenbanken sind eine sehr elegante Lösung und können sehr angenehm zu nutzen sein. Wenn Sie die Lösung selbst hosten wollen, braucht das allerdings Expertise, weil diese Systeme erheblich komplex sind (siehe RisingWaves Architektur). Das drängt Praktiker eher zum Kaufen als zum Hosting und Pflegen dieses komplexen Systems – und das kann teuer werden.
Was immer Sie für Ihre Architektur wählen: Stellen Sie sicher, dass der Aufwand zu Ihren Bedürfnissen passt. Und wenn Sie das nächste Mal an Streaming denken, denken Sie auch an DuckDB.