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.

Streaming patterns Streaming patterns

„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

pattern_1_1 pattern_1_1

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 dest
USING (
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 src
ON dest.user_id = src.user_id
WHEN MATCHED THEN
UPDATE SET
count_of_clicks = dest.count_of_clicks + src.count_of_clicks,
updated_at = src.updated_at
WHEN 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

pattern_1_2 pattern_1_2

Dieses Muster ist 1.1 sehr ähnlich, mit einigen DuckLake-Besonderheiten:

  1. Wir nutzen DuckLakes Data Inlining, um Einfügen zu beschleunigen, ohne zu viele kleine Dateien zu schreiben.
  2. Die Delta-Processor-Komponente kann DuckLakes Data Change Feed nutzen, um unnötiges Scannen zu vermeiden.
  3. 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

pattern_2 pattern_2

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

pattern_bonus pattern_bonus

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.