2025-11-28

Writes in DuckDB-Iceberg

Tom Ebergen

In den letzten Monaten hat das DuckLabs-Team intensiv an der DuckDB-Iceberg-Erweiterung gearbeitet, mit voller Leseunterstützung und erster Schreibunterstützung in v1.4.0. Heute freuen wir uns, bekannt zu geben, dass Delete- und Update-Unterstützung für Iceberg-v2-Tabellen in v1.4.2 verfügbar ist!

Das offene Iceberg-Tabellenformat ist in den letzten zwei Jahren extrem populär geworden, und viele Datenbanken haben Unterstützung für das ursprünglich bei Netflix entwickelte offene Tabellenformat angekündigt. Im vergangenen Jahr hat das DuckDB-Team Iceberg-Integration zur Priorität gemacht, und heute freuen wir uns über einen weiteren Schritt in diese Richtung. In diesem Blogbeitrag beschreiben wir den aktuellen Funktionsumfang von DuckDB-Iceberg in DuckDB v1.4.2.

Einstieg

Um die neuen DuckDB-Iceberg-Features auszuprobieren, müssen Sie sich mit Ihrem bevorzugten Iceberg REST Catalog verbinden. Es gibt viele Wege, sich mit einem Iceberg REST Catalog zu verbinden: Schauen Sie sich Connecting to REST Catalogs für Catalogs wie Apache Polaris oder Lakekeeper an und die Seite Connecting to S3 Tables, wenn Sie sich mit Amazon S3 Tables verbinden möchten.

ATTACH '⟨warehouse_name⟩' AS iceberg_catalog (
TYPE iceberg,
⟨other options⟩
);

Inserts, Deletes und Updates

Unterstützung für das Anlegen von Tabellen und das Einfügen in Tabellen gab es bereits in DuckDB v1.4.0: Sie können die Standard-DuckDB-SQL-Syntax nutzen, um Daten in Ihre Iceberg-Tabelle einzufügen.

CREATE TABLE iceberg_catalog.default.simple_table (
col1 INTEGER,
col2 VARCHAR
);
INSERT INTO iceberg_catalog.default.simple_table
VALUES (1, 'hello'), (2, 'world'), (3, 'duckdb is great');

Sie können auch jede DuckDB-Table-Scan-Funktion nutzen, um Daten in eine Iceberg-Tabelle einzufügen:

INSERT INTO iceberg_catalog.default.more_data
SELECT * FROM read_parquet('path/to/parquet');

Ab v1.4.2 funktioniert die Standard-SQL-Syntax auch für Deletes und Updates:

DELETE FROM iceberg_catalog.default.simple_table
WHERE col1 = 2;
UPDATE iceberg_catalog.default.simple_table
SET col1 = col1 + 5
WHERE col1 = 1;
SELECT *
FROM iceberg_catalog.default.simple_table;
┌───────┬─────────────────┐
│ col1 │ col2 │
│ int32 │ varchar │
├───────┼─────────────────┤
│ 3 │ duckdb is great │
│ 6 │ hello │
└───────┴─────────────────┘

Die Iceberg-Schreibunterstützung hat derzeit zwei Einschränkungen:

Die Update-Unterstützung ist auf Tabellen beschränkt, die weder partitioniert noch sortiert sind. Der Versuch, Update-, Insert- oder Delete-Operationen auf partitionierten oder sortierten Tabellen mit DuckDB-Iceberg auszuführen, führt zu einem Fehler.

DuckDB-Iceberg schreibt nur Position Deletes für DELETE- und UPDATE-Anweisungen. Copy-on-Write wird noch nicht unterstützt.

Funktionen für Tabelleneigenschaften

Derzeit unterstützt DuckDB-Iceberg nur Merge-on-Read-Semantik. Innerhalb der Iceberg-Tabellenmetadaten können Tabelleneigenschaften beschreiben, welche Form von Deletes oder Updates erlaubt ist. DuckDB-Iceberg respektiert die Tabelleneigenschaften write.update.mode und write.delete.mode für Updates und Deletes. Wenn eine Tabelle diese Eigenschaften hat und sie nicht merge-on-read sind, wirft DuckDB einen Fehler und das UPDATE oder DELETE wird nicht committed. Version v1.4.2 führt drei neue Funktionen ein, um Tabelleneigenschaften einer Iceberg-Tabelle hinzuzufügen, zu entfernen und anzuzeigen:

Sie können sie so nutzen:

-- to set table properties
CALL set_iceberg_table_properties(iceberg_catalog.default.simple_table, {
'write.update.mode': 'merge-on-read',
'write.file.size': '100000kb'
});
-- to read table properties
SELECT * FROM iceberg_table_properties(iceberg_catalog.default.simple_table);
┌───────────────────┬───────────────┐
│ key │ value │
│ varchar │ varchar │
├───────────────────┼───────────────┤
│ write.update.mode │ merge-on-read │
│ write.file.size │ 100000kb │
└───────────────────┴───────────────┘
-- to remove table properties
CALL remove_iceberg_table_properties(
iceberg_catalog.default.simple_table,
['some.other.property']
);

Iceberg-Tabellenmetadaten

DuckDB-Iceberg erlaubt Ihnen auch, die Metadaten Ihrer Iceberg-Tabellen mit den Funktionen iceberg_metadata() und iceberg_snapshots() anzusehen.

SELECT * FROM iceberg_metadata(iceberg_catalog.default.table_1);
┌──────────────────────┬──────────────────────┬──────────────────┬─────────┬──────────────────┬─────────────────────────────────────────────────────────────┬─────────────┬──────────────┐
│ manifest_path │ manifest_sequence_… │ manifest_content │ status │ content │ file_path │ file_format │ record_count │
│ varchar │ int64 │ varchar │ varchar │ varchar │ varchar │ varchar │ int64 │
├──────────────────────┼──────────────────────┼──────────────────┼─────────┼──────────────────┼─────────────────────────────────────────────────────────────┼─────────────┼──────────────┤
│ s3://warehouse/def… │ 1 │ DATA │ ADDED │ EXISTING │ s3://<storage_location>/simple_table/data/019a6ecc-9e9e-7… │ parquet │ 3 │
│ s3://warehouse/def… │ 2 │ DELETE │ ADDED │ POSITION_DELETES │ s3://<storage_location>/simple_table/data/d65b1db8-9fa8-4… │ parquet │ 1 │
│ s3://warehouse/def… │ 3 │ DELETE │ ADDED │ POSITION_DELETES │ s3://<storage_location>/simple_table/data/8d1b92dc-5f6e-4… │ parquet │ 1 │
│ s3://warehouse/def… │ 3 │ DATA │ ADDED │ EXISTING │ s3://<storage_location>/simple_table/data/019a6ecf-5261-7… │ parquet │ 1 │
└──────────────────────┴──────────────────────┴──────────────────┴─────────┴──────────────────┴─────────────────────────────────────────────────────────────┴─────────────┴──────────────┘
SELECT * FROM iceberg_snapshots(iceberg_catalog.default.simple_table);
┌─────────────────┬─────────────────────┬─────────────────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ sequence_number │ snapshot_id │ timestamp_ms │ manifest_list │
│ uint64 │ uint64 │ timestamp │ varchar │
├─────────────────┼─────────────────────┼─────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 1 │ 1790528822676766947 │ 2025-11-10 17:24:55.075 │ s3://<storage_location>/simple_table/data/snap-1790528822676766947-f09658c4-ca52-4305-943f-6a8073529fef.avro │
│ 2 │ 6333537230056014119 │ 2025-11-10 17:27:35.602 │ s3://<storage_location>/simple_table/data/snap-6333537230056014119-316d09bc-549d-46bc-ae13-a9fab5cbf09b.avro │
│ 3 │ 7452040077415501383 │ 2025-11-10 17:27:52.169 │ s3://<storage_location>/simple_table/data/snap-7452040077415501383-93dee94e-9ec1-45fa-aec2-13ef434e50eb.avro │
└─────────────────┴─────────────────────┴─────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

Time Travel

Time Travel ist ebenfalls über Snapshot-IDs oder Zeitstempel mit der Syntax AT (VERSION => ...) oder AT (TIMESTAMP => ...) möglich.

-- via snapshot id
SELECT *
FROM iceberg_catalog.default.simple_table AT (
VERSION => ⟨snapshot_id⟩
);
┌───────┬─────────────────┐
│ col1 │ col2 │
│ int32 │ varchar │
├───────┼─────────────────┤
│ 1 │ hello │
│ 3 │ duckdb is great │
└───────┴─────────────────┘
-- via timestamp
SELECT *
FROM iceberg_catalog.default.simple_table AT (
TIMESTAMP => '2025-11-10 17:27:45.602'
);
┌───────┬─────────────────┐
│ col1 │ col2 │
│ int32 │ varchar │
├───────┼─────────────────┤
│ 1 │ hello │
│ 3 │ duckdb is great │
└───────┴─────────────────┘

Requests an den Iceberg REST Catalog ansehen

Sie sind vielleicht auch neugierig, welche Requests DuckDB an den Iceberg REST Catalog schickt. Aktivieren Sie dazu HTTP-Logging, führen Sie Ihre Workload aus und selektieren Sie dann aus den HTTP-Logs.

CALL enable_logging('HTTP');
SELECT * FROM iceberg_catalog.default.simple_table;
SELECT request.type, request.url, response.status
FROM duckdb_logs_parsed('HTTP');
┌─────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────┬────────────────────┐
│ type │ url │ status │
│ varchar │ varchar │ varchar │
├─────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────┤
│ GET │ https://<catalog_endpoint>/iceberg/v1/<warehouse>/iceberg-testing/namespaces/default │ NULL │
│ HEAD │ https://<catalog_endpoint>/iceberg/v1/<warehouse>/iceberg-testing/namespaces/default/tables/simple_table │ NULL │
│ GET │ https://<catalog_endpoint>/iceberg/v1/<warehouse>/iceberg-testing/namespaces/default/tables/simple_table │ NULL │
│ GET │ https://<storage_endpoint>/data/snap-5943683398986255948-c2217dde-6036-4e07-88f2-… │ OK_200 │
│ GET │ https://<storage_endpoint>/data/f8c95b93-7b6b-4a24-8557-b98b553723d4-m0.avro │ OK_200 │
│ GET │ https://<storage_endpoint>/data/214a7988-da39-4dac-aa3a-4a73d3ead405-m0.avro │ OK_200 │
│ GET │ https://<storage_endpoint>/data/019a7244-c6e8-7bc9-9dd4-7249fcb04959.parquet │ PartialContent_206 │
│ GET │ https://<storage_endpoint>/data/019a7244-fcb5-7308-96ec-1c9e32509eab.parquet │ PartialContent_206 │
│ GET │ https://<storage_endpoint>/data/7f14bb06-f57a-42b4-ba7f-053a65152759-m0.avro │ OK_200 │
│ GET │ https://<storage_endpoint>/data/71f8b43d-51e7-40e7-be88-e8d869836ecd-deletes.parq… │ PartialContent_206 │
│ GET │ https://<storage_endpoint>/data/64f6c6e2-2f54-470e-b990-b201bc615042-m0.avro │ OK_200 │
│ GET │ https://<storage_endpoint>/data/4e54afed-6dd8-4ba0-88fb-16f972ac1d91-deletes.parq… │ PartialContent_206 │
├─────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────┴────────────────────┤
│ 12 rows 3 columns │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

Hier sehen wir Aufrufe an den Iceberg REST Catalog, gefolgt von Aufrufen an den Storage-Endpunkt. Die ersten drei Aufrufe an den Iceberg REST Catalog prüfen, ob das Schema noch existiert, und holen die neueste metadata.json der DuckDB-Iceberg-Tabelle. Als Nächstes werden Manifest-Liste, Manifest-Dateien und schließlich die Dateien mit Daten und Deletes abgefragt. Die Daten- und Delete-Dateien werden lokal in einem Cache gehalten, um Folgeläufe zu beschleunigen.

Transaktionen

DuckDB ist eine ACID-konforme Datenbank, die Transaktionen unterstützt. Die Arbeit an DuckDB-Iceberg wurde mit dem im Hinterkopf gemacht. Innerhalb einer Transaktion gelten für Iceberg-Tabellen die folgenden Bedingungen.

  1. Beim ersten Lesen einer Tabelle in einer Transaktion wird ihre Snapshot-Information in der Transaktion gespeichert und bleibt innerhalb dieser Transaktion konsistent.
  2. Updates, Inserts und Deletes werden erst beim Commit der Transaktion in eine Iceberg-Tabelle geschrieben (also COMMIT);

Punkt 1 ist wichtig für die Leseperformance. Wenn Sie Analytics auf einer Iceberg-Tabelle machen und nicht jedes Mal die neueste Version der Tabelle brauchen, verhindert das Ausführen Ihrer Analytics in einer Transaktion, dass für jede Abfrage die neueste Version geholt wird.

-- truncate the logs
CALL truncate_duckdb_logs();
CALL enable_logging('HTTP')
BEGIN;
-- first read gets latest snapshot information
SELECT * FROM iceberg_catalog.default.simple_table;
-- subsequent read reads from local cached data
SELECT * FROM iceberg_catalog.default.simple_table;
-- get logs
SELECT request.type, request.url, response.status
FROM duckdb_logs_parsed('HTTP');
┌─────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────┬────────────────────┐
│ type │ url │ status │
│ varchar │ varchar │ varchar │
├─────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────┤
│ GET │ https://<catalog_endpoint>/iceberg/v1/<warehouse>/iceberg-testing/namespaces/default │ NULL │
│ HEAD │ https://<catalog_endpoint>/iceberg/v1/<warehouse>/iceberg-testing/namespaces/default/tables/simple_table │ NULL │
│ GET │ https://<catalog_endpoint>/iceberg/v1/<warehouse>/iceberg-testing/namespaces/default/tables/simple_table │ NULL │
│ GET │ https://<storage_endpoint>/data/snap-5943683398986255948-c2217dde-6036-4e07-88f2-1… │ OK_200 │
│ GET │ https://<storage_endpoint>/data/f8c95b93-7b6b-4a24-8557-b98b553723d4-m0.avro │ OK_200 │
│ GET │ https://<storage_endpoint>/data/214a7988-da39-4dac-aa3a-4a73d3ead405-m0.avro │ OK_200 │
│ GET │ https://<storage_endpoint>/data/019a7244-c6e8-7bc9-9dd4-7249fcb04959.parquet │ PartialContent_206 │
│ GET │ https://<storage_endpoint>/data/019a7244-fcb5-7308-96ec-1c9e32509eab.parquet │ PartialContent_206 │
│ GET │ https://<storage_endpoint>/data/7f14bb06-f57a-42b4-ba7f-053a65152759-m0.avro │ OK_200 │
│ GET │ https://<storage_endpoint>/data/71f8b43d-51e7-40e7-be88-e8d869836ecd-deletes.parquet │ PartialContent_206 │
│ GET │ https://<storage_endpoint>/data/64f6c6e2-2f54-470e-b990-b201bc615042-m0.avro │ OK_200 │
│ GET │ https://<storage_endpoint>/data/4e54afed-6dd8-4ba0-88fb-16f972ac1d91-deletes.parquet │ PartialContent_206 │
├─────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────┴────────────────────┤
│ 12 rows 3 columns │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

Hier sehen wir alle Requests aus dem vorherigen Abschnitt. Jetzt sind wir aber in einer Transaktion, das heißt beim zweiten Lesen von iceberg_catalog.default.simple_table müssen wir den REST Catalog nicht nach Tabellen-Updates fragen. DuckDB-Iceberg macht also beim zweiten Lesen einer Tabelle keine Extra-Requests, was die Performance deutlich verbessert.

Fazit und Ausblick

Mit diesen Features hat DuckDB-Iceberg jetzt eine starke Basisunterstützung für Iceberg-Tabellen, die Nutzerinnen und Nutzern die analytische Kraft von DuckDB auf ihren Iceberg-Tabellen erschließt. Es kommt noch mehr Arbeit, und die Iceberg-Tabellenspezifikation hat viele weitere Features, die das DuckDB-Team in DuckDB-Iceberg unterstützen möchte. Wenn Sie ein Feature für Ihre analytischen Workloads priorisieren, melden Sie sich im DuckDB-Iceberg-GitHub-Repository oder nehmen Sie Kontakt mit unseren Engineers auf.

Unten eine Liste geplanter Verbesserungen für die nahe Zukunft (ohne besondere Reihenfolge):