2026-05-07

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:

-- Schema: (text VARCHAR, code BIGINT)
ATTACH './path/to/my_table' AS my_table (TYPE delta);
INSERT INTO my_table
VALUES ('Question 2', 2), ('The Answer', 42);
-- Bulk insert from a query
INSERT INTO my_table
FROM (SELECT text || ' (copy)', code + 100 FROM 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:

Version Inhalt
0 ('Question 1', 1)
1 + ('Question 2', 2), ('The Answer', 42)
2 + ('Question 1 (copy)', 101), ('Question 2 (copy)', 102), ('The Answer (copy)', 142)

Sie können normal anhängen und beliebige Versionen inline abfragen, wenn nötig. Der flexibelste Ansatz:

ATTACH './path/to/my_table' AS my_table (TYPE delta);
SELECT count() FROM my_table AT (VERSION => 0); -- 1 (Question 1 only)
SELECT count() FROM my_table AT (VERSION => 1); -- 3 (after 1st insert)
SELECT count() 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, VERSION 1);
SELECT count() 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);
SELECT count() 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:

Terminal window
# Create a schema
/home/unitycatalog/bin/uc schema create --catalog unity --name my_schema
# Create the "pets" table
/home/unitycatalog/bin/uc table create \
--full_name unity.my_schema.pets \
--columns "uuid STRING, name STRING, age INT, adopted BOOLEAN" \
--format DELTA \
--storage_location file:///home/unitycatalog/etc/data/external/unity/my_schema/tables/pets

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:

LOAD unity_catalog;
CREATE SECRET (
TYPE unity_catalog,
TOKEN 'demo-ignored-token',
ENDPOINT 'http://unitycatalog:8080'
);
ATTACH 'unity' AS my_catalog
(TYPE unity_catalog, DEFAULT_SCHEMA 'my_schema');
SELECT name, age, adopted FROM my_catalog.pets ORDER BY name;
-- returns a single 'Seed' row

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:

Damit können Sie DuckDB direkt nutzen oder die umfangreiche UC Open API direkt ansprechen.

Als Nächstes schließen wir den Kreis und schreiben Daten in unsere Pets-Tabelle:

INSERT INTO my_catalog.pets
(uuid, name, age, adopted)
SELECT
gen_random_uuid()::VARCHAR,
['Luna', 'Milo', 'Bella', 'Charlie', 'Max', 'Lucy', 'Cooper',
'Daisy', 'Buddy', 'Lily', 'Rocky', 'Molly', 'Bear', 'Lola',
'Duke', 'Sadie', 'Tucker', 'Zoe', 'Oliver', 'Stella'
][1 + (random() * 19)::INT],
(1 + (random() * 14)::INT)::INT,
random() > 0.5
FROM range(10);
SELECT count() FROM my_catalog.pets;

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:

Terminal window
tree data
data
└── external
└── unity
└── my_schema
└── tables
└── pets
├── _delta_log
│   ├── 00000000000000000000.json
│   ├── 00000000000000000001.json
│   └── 00000000000000000002.json
├── duckdb-19cb47ae-9f35-4126-b67d-c94fcade68cc.parquet
└── duckdb-e3bb0336-f16a-4d21-9495-0fbf55c6cba8.parquet
7 directories, 5 files

Catalog Managed Tables

Mit den Grundlagen können wir über Catalog Managed Tables (CMT) sprechen. Das gibt es heute sowohl in OSS als auch in Databricks Unity Catalog.

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:

-- Via Spark
CREATE TABLE my_catalog.my_schema.concurrent_tbl (
uuid STRING NOT NULL,
name STRING NOT NULL,
age INT NOT NULL,
adopted BOOLEAN NOT NULL
)
TBLPROPERTIES ('delta.feature.catalogManaged' = 'supported');

Sobald aktiviert, laufen DuckDB-Writes automatisch über UCs Commit-Staging — die INSERT-Syntax bleibt unverändert:

INSERT INTO my_catalog.my_schema.concurrent_tbl
(uuid, name, age, adopted)
VALUES (gen_random_uuid()::VARCHAR, 'Luna', 3, true);

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:

Terminal window
seq 1 20 | xargs -P 8 -I{} scripts/unity/05-cmc/write-single {}
[worker 6] OK - inserted 5 rows
[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:

SELECT count() AS total_rows FROM my_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, VERSION 16);
SELECT count() 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:

SET enable_logging = true;
SET delta_kernel_logging = true;
CALL enable_logging('DeltaKernel', level = 'trace');
ATTACH './path/to/table' AS t (TYPE delta, VERSION 20);
SELECT count() FROM t; -- → 21
-- Delta kernel logs 'Provisionally selecting ... <version>.json'
-- 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.
SELECT count() FROM duckdb_logs
WHERE type = 'DeltaKernel'
AND message LIKE '%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.