Delta wird erwachsen: Writes, Unity Catalog und Time Travel
Ben Fleis
Willkommen zurück! Wir bei DuckLabs sind zwar eher vom Quaken,
waren aber fleißig wie Biber und haben unser Delta hergerichtet für
das, was als Nächstes kommt… Unity Catalog! Schauen wir uns an, wie
DuckDBs
Delta- und
Unity-Catalog-Erweiterungen
erwachsen genug geworden sind, um das Experimental-Tag abzulegen, und was
sich seit unserem letzten
Update
geändert hat.
Zeit, das Delta zu öffnen
Bevor wir einsteigen, ein kurzer Rückblick. Delta ist ein grundlegendes
offenes Tabellenformat und Toolset zum Aufbau und
zur Verwaltung von Data Lakes, verwandt mit Iceberg und anderen
Lakehouse-Formaten. DuckDB unterstützt Delta-Tabellen über seine
Delta-Erweiterung.
Im letzten Update haben wir Performance-Gewinne hervorgehoben, vor allem File
Skipping über Filter-Pushdowns und Metadata-Caching mit Snapshot-Pinning. Darauf
bauen wir jetzt auf und ergänzen Writes, Time Travel und Unity-Catalog-Unterstützung,
plus weitere Performance-Gewinne!
Das Delta (Lake) aufbauen: Writes
Was nützen Reads ohne Writes? Die große Ergänzung seit dem letzten Gespräch
ist INSERT-Unterstützung! Sie funktioniert so einfach, wie man es erwartet.
Angenommen, Sie haben eine Delta-Tabelle bereit. Einfach INSERTen:
FROM (SELECTtext||' (copy)', code +100FROM my_table);
Ebenfalls erwähnenswert: Mehrere INSERTs in einem BEGIN / COMMIT-Block
werden als eine einzige Delta-Version gespeichert: ein atomarer Commit, ein
neuer Log-Eintrag. Und wie Sie später sehen, funktioniert das auch mit
Katalogen! UPDATE, MERGE und DELETE sind noch nicht unterstützt, stehen
aber auf unserer künftigen Arbeitsliste.
Time Travel
DuckDBs Delta-Erweiterung unterstützt jetzt
Time Travel.
Jede Delta-Tabelle lässt sich zu einer bestimmten Version abfragen. DuckDB
unterstützt das Binden an eine konkrete VERSION entweder beim ATTACH
oder als Teil einer einzelnen Abfrage.
Angenommen, wir haben die obige my_table schrittweise aufgebaut, mit den
Versionen 0, 1 und 2, die enthalten:
SELECTcount() FROM my_table AT (VERSION=>0); -- 1 (Question 1 only)
SELECTcount() FROM my_table AT (VERSION=>1); -- 3 (after 1st insert)
SELECTcount() FROM my_table; -- 6 (latest)
Oder Sie hängen fest an eine bestimmte Version, nützlich, wenn Sie eine
stabile Referenz wollen, die sich unabhängig von späteren Writes nie ändert:
-- Always v1, no matter what gets written later
ATTACH'./path/to/my_table'AS my_table_v1
(TYPE delta, VERSION1);
SELECTcount() FROM my_table_v1; -- → 3
-- Locked to whatever was latest at attach time
ATTACH'./path/to/my_table'AS my_table_pinned
(TYPE delta, PIN_SNAPSHOT);
SELECTcount() FROM my_table_pinned; -- → 6
Erwachsen geworden: kein Kit mehr 🦫
Die DuckDB-Delta-Erweiterung ist kein
Kit mehr und ist
seit einem Jahr deutlich gewachsen.
Wie Sie gerade gesehen haben, haben wir Writes und Time Travel ergänzt. Diese
Funktionen öffnen die Tür zu etwas Größerem: Unity-Catalog-Koordination.
Unity-Catalog-Unterstützung auf dem Delta
Data-Lake-Systeme glänzen bei Skala. Wenn Ihre Datenbestände wachsen,
brauchen Sie einen Weg, zu entdecken, was existiert, zu steuern, wer
zugreifen kann, zu prüfen, wie es genutzt wird, und Writes über mehrere
Engines zu koordinieren. Datenkataloge haben sich genau dafür entwickelt:
Sie sitzen über der Storage-Schicht und verwalten Metadaten, Governance und
die transaktionale Buchführung, die große Data Lakes wirksam machen. Das
OSS-Unity-Catalog-Team hat einen guten
Überblick,
wenn Sie tiefer einsteigen möchten; die Konzepte gelten unabhängig vom
verwendeten Katalog.
Was ist Unity Catalog?
Unity Catalog (kurz UC) ist ein offener Standard zur Governance von Daten-
und KI-Assets, einschließlich Tabellen, Volumes, Modellen und Funktionen,
über Engines und Clouds hinweg. Er macht aus Ihrem Data Lake ein Lakehouse
und gibt Ihnen einen Ort, um Daten zu entdecken, zu prüfen und den Zugriff
zu steuern, unabhängig davon, was liest oder schreibt. DuckDBs
Unity-Catalog-Erweiterung baut auf der
Unity Catalog Open API auf. Es gibt
zwei wesentliche Implementierungen: OSS Unity Catalog,
das Sie selbst hosten können (und in Minuten Docker-fähig machen), und
Databricks Unity
Catalog,
die verwaltete Variante. Wie Delta hat die DuckDB-Unity-Catalog-Erweiterung
ihr Experimental-Tag abgelegt. Legen wir beide an.
Einstieg: OSS Unity Catalog
Wir haben ein Docker-Image-Playground vorbereitet, das OSS Unity Catalog und
DuckDB
bündelt,
damit Sie mit einem einfachen Docker-Build-and-Run mitmachen können. Holen
Sie es sich, wenn Sie die Beispiele durchgehen oder selbst experimentieren
möchten. (Wenn Sie OSS UC lieber direkt laufen lassen, ist das offizielle
Image der Upstream unseres Playgrounds.)
Starten wir mit Docker. Angenommen, das Image läuft, hat es in der
Build-Phase bereits (ungefähr) die folgenden Schritte ausgeführt, um unseren
Playground vorzubereiten:
Danach können wir es von DuckDB aus testen. Um selbst nachzusehen, gibt
docker exec -it duckdb-playground duckdb Ihnen eine DuckDB-Shell im
Container.
Bevor etwas Sinnvolles passiert, brauchen wir ein DuckDB-Secret. In diesem
Beispiel ignoriert der lokale OSS-UC-Server den TOKEN-Wert, das Feld ist
aber Pflicht. Legen Sie das Secret an, dann können Sie sofort anhängen und
lesen:
Das war’s! Sie haben gerade Unity-Catalog-verwaltete, Delta-gespeicherte
Pets-Daten abgefragt.
Tipp Möchten Sie das auf Databricks Unity Catalog ausprobieren? Das
Aufsetzen eines Databricks Unity Catalogs sprengt den Rahmen dieses
Beitrags, aber wenn Sie einen bereit haben, brauchen Sie zum Bootstrappen
mit DuckDB Folgendes:
setzen Sie ENDPOINT auf Ihre Workspace-
URL
(typisch: https://{instance}.cloud.databricks.com/)
Sie können die erzeugten Dateien auch leicht finden; schauen Sie ins lokale
Verzeichnis data (im Docker ebenfalls bind-gemountet), dort sollten Sie
sowohl vorbestehende Dateien als auch eine neue Parquet-Datei mit den
eingefügten Zeilen finden. Bei mir sieht das so aus:
Das große Feature in CMT sind Catalog Commits, die koordinierte parallele
Writes ermöglichen. Ohne Catalog Commits gehen DuckDB-Writes direkt ins
Delta-Log. Moderne Storage-Backends verhindern zwar verlorene Writes, UC
bleibt aber komplett außen vor. Seine Metadaten, der Audit-Trail und die
Statistiken geraten außer Sync mit dem tatsächlichen Tabellenstand, und
andere Engines, die über UC abfragen, können eine veraltete Sicht sehen.
Catalog Commits (CC) beheben das: Jeder Write wird über UC gestaged und
registriert, bevor er sichtbar wird. UC agiert als Commit-Schiedsrichter,
bewahrt First-Writer-Commits und schickt späteren Schreibern einen
Konfliktfehler. Das zählt überall dort, wo mehrere Schreiber gleichzeitig
anhängen, z. B. parallele ETL-Pipelines, partitionierte Bulk Loads und
gleichzeitige analytische Inserts. Jeder Schreiber arbeitet unabhängig; UC
stellt sicher, dass genau ein Commit pro Version landet, und hält den eigenen
Katalog mit jedem davon in Sync.
Konsistente Reads und Audit-Historie liegen bereits bei Delta bzw. UC.
CC fügt keine Funktion hinzu, es stellt nur sicher, dass UC mit jedem Commit
in Sync bleibt. Und Catalog Commits koordinieren pro Tabelle; es gibt keine
tabellenübergreifende Atomarität. Schreiben Sie in zwei Tabellen im selben
BEGIN / COMMIT-Block, committet jede Tabelle unabhängig.
Um eine Tabelle in CMT (und damit CC) zu holen, setzen Sie die Table Property
delta.feature.catalogManaged beim Anlegen. Das geschieht über Spark oder
die UC-CLI, weil DuckDBs Unity-Catalog-Erweiterung CREATE TABLE-DDL noch
nicht unterstützt:
Jeder DuckDB-Schreiber staget seinen Commit jetzt in ein Verzeichnis
_staged_commits/ und registriert ihn bei UC, bevor die Daten sichtbar
werden. UC schlichtet: Genau ein Schreiber gewinnt jede Version in einem
Rennen, die anderen bekommen einen Konfliktfehler und können retryen. Als
Nächstes schauen wir, wie UC das Rennen behandelt.
Tieferer Einstieg
Racing Commits
Um zu sehen, wie Catalog Commits schlichtet, haben wir 20 parallele
DuckDB-Schreiber gestartet, 8 auf einmal, alle inserten in dieselbe
Managed Table:
[worker 5] CONFLICT - another writer won this version, retry needed
[worker 2] CONFLICT - another writer won this version, retry needed
[worker 8] CONFLICT - another writer won this version, retry needed
[worker 7] CONFLICT - another writer won this version, retry needed
[worker 3] CONFLICT - another writer won this version, retry needed
[worker 1] OK - inserted 5 rows
[worker 4] CONFLICT - another writer won this version, retry needed
[worker 16] OK - inserted 5 rows
[worker 13] CONFLICT - another writer won this version, retry needed
[worker 15] CONFLICT - another writer won this version, retry needed
[worker 11] CONFLICT - another writer won this version, retry needed
[worker 14] CONFLICT - another writer won this version, retry needed
[worker 12] OK - inserted 5 rows
[worker 9] CONFLICT - another writer won this version, retry needed
[worker 10] CONFLICT - another writer won this version, retry needed
[worker 17] CONFLICT - another writer won this version, retry needed
[worker 20] CONFLICT - another writer won this version, retry needed
[worker 18] OK - inserted 5 rows
[worker 19] CONFLICT - another writer won this version, retry needed
Hier sehen wir 5 erfolgreiche Writes und 15 gemeldete Konflikte. Prüfen wir
das in den Daten:
SELECTcount() AS total_rows FROMmy_catalog.my_schema.concurrent_tbl;
┌────────────┐
│ total_rows │
│ int64 │
├────────────┤
│ 35 │
└────────────┘
10 geseedete Zeilen + (5 Writes × 5 Zeilen) = 35 Zeilen insgesamt. (In einem
echten Workload würden Sie die konfliktbehafteten Writes retryen und alle
20 Inserts landen.) Catalog-Managed-Table-Commits haben uns bei stark
parallelen Writes klare Signale und Semantik gegeben, wie versprochen.
Schneller durch die Zeit reisen
Das Laden von DuckDB-Delta-Snapshots wird schneller: Snapshots werden
wo möglich inkrementell geladen, Time Travel über nahe Versionen wird
deutlich schneller. Betrachten Sie eine Tabelle, bei der erste Abfragen
gegen Version 16 laufen:
ATTACH'./path/to/table'AS t (TYPE delta, VERSION16);
SELECTcount() FROM t; -- → 17
Und jetzt braucht es Arbeit gegen Version 20. Schauen wir unter die Haube
(Achtung: sneaky Code folgt), sehen wir, dass keine der zuvor geladenen
Delta-Log-Metadaten-Dateien erneut geladen wurde:
-- whenever it reads a log file from scratch. We search for any such
-- message referencing a zero-padded log filename; zero matches
-- means the cached v16 snapshot was reused rather than rebuilt.
SELECTcount() FROM duckdb_logs
WHEREtype='DeltaKernel'
ANDmessageLIKE'%00000000000000000%.json%';
-- → 0
In Delta Lakes mit Tausenden oder Millionen Snapshots ist inkrementelles
Laden ein großer Gewinn, wenn man über mehrere Versionen arbeitet.
Zum Zeitpunkt der Veröffentlichung ist inkrementelles Snapshot-Loading in
Nightly Builds unterstützt.
Installation:
FORCE INSTALL delta FROM core_nightly;
Nightly Builds sind nicht für den Produktionseinsatz gedacht.
Die Implementierung kommt im nächsten stabilen Release,
v1.5.3.
Fazit
Vor einem Jahr konnte DuckDB Delta-Tabellen lesen. Heute kann es Daten
hinein inserten, durch ihre Historie reisen und über einen governed Katalog
abfragen und schreiben – ohne Experimental-Vorbehalt. Die Kombination aus
Delta für offenen Storage, Unity Catalog für Governance und Koordination und
DuckDB für schnelle analytische Abfragen ist ein Stack, auf dem man bauen
kann.
Es kommt noch mehr: DDL-Unterstützung zum direkten Anlegen und Verwalten von
Tabellen, Delete/Update/Merge und Multi-Table-Atomarität für Writes über mehr
als eine Tabelle. Bis dahin hat das oben verlinkte Playground-Image alles,
was Sie zum Ausprobieren brauchen. Feedback und Bugreports sind wie immer
willkommen auf GitHub.