The DuckDB iceberg extension supports writing to Iceberg tables when an Iceberg catalog is attached.
Writing to a table read directly from storage with iceberg_scan is not supported: the catalog is required to commit the new table metadata.
Supported Operations
The iceberg extension supports the following operations when used with a REST Catalog attached:
CREATE/DROP SCHEMACREATE/DROP TABLEINSERT INTOUPDATEDELETEMERGE INTOALTER TABLESELECT
Since these operations are supported, the following will also work:
COPY FROM DATABASE duckdb_db TO iceberg_datalake;
-- Or
COPY FROM DATABASE iceberg_datalake TO duckdb_db;
This functionality enables deep copies between Iceberg and DuckDB storage.
Creating Schemas and Tables
Iceberg namespaces are exposed as schemas, which can be created and dropped with CREATE SCHEMA and DROP SCHEMA:
CREATE SCHEMA iceberg_catalog.sales;
USE iceberg_catalog.sales;
Tables are created with the standard CREATE TABLE syntax. Iceberg table properties can be supplied with the WITH clause:
CREATE TABLE test_create_table (a INTEGER)
WITH (
'format-version' = '2', -- format version will be elevated to format-version when creating a table
'location' = 's3://path/to/data', -- location will be elevated to location when creating a table
'property1' = 'value1',
'property2' = 'value2'
);
Tables can also be created from the result of a query:
CREATE TABLE iceberg_catalog.sales.events_copy AS
FROM iceberg_catalog.sales.events;
Table properties can also be inspected and modified after creation, see Table Properties Functions.
UPDATE and DELETE
The UPDATE and DELETE statements are supported on both partitioned and unpartitioned tables:
UPDATE iceberg_catalog.sales.events
SET event_name = 'view'
WHERE id = 1;
DELETE FROM iceberg_catalog.sales.events
WHERE event_time < TIMESTAMP '2026-01-01';
They use merge-on-read semantics and write positional deletes, see Limitations for UPDATE and DELETE.
MERGE INTO
The MERGE INTO statement is the recommended way to express upserts against Iceberg tables, which do not have a primary key. You can apply a change set in a single statement, deciding per row whether to insert, update, or delete:
MERGE INTO iceberg_catalog.default.people AS target
USING (
FROM (VALUES
(1, 'John', 105_000.0),
(3, 'Sarah', 95_000.0)
) t(id, name, salary)
) AS upserts
ON (upserts.id = target.id)
WHEN MATCHED THEN UPDATE
WHEN NOT MATCHED THEN INSERT;
You can also use WHEN MATCHED THEN DELETE to express a delete set in the same statement. As with UPDATE and DELETE, MERGE INTO uses merge-on-read semantics and writes positional deletes to the Iceberg table.
ALTER TABLE and Schema Evolution
The ALTER TABLE statement is supported against Iceberg tables, covering the most common schema-evolution operations:
-- Rename the table
ALTER TABLE iceberg_catalog.default.simple_table
RENAME TO renamed_table;
-- Add a column
ALTER TABLE iceberg_catalog.default.renamed_table
ADD COLUMN col3 DOUBLE;
-- Rename a column
ALTER TABLE iceberg_catalog.default.renamed_table
RENAME COLUMN col2 TO name;
-- Drop a column
ALTER TABLE iceberg_catalog.default.renamed_table
DROP COLUMN col3;
-- Change the type of a column
ALTER TABLE iceberg_catalog.default.renamed_table
ALTER COLUMN col1 TYPE BIGINT;
-- Set and drop the default value of a column
ALTER TABLE iceberg_catalog.default.renamed_table
ALTER COLUMN col1 SET DEFAULT 0;
ALTER TABLE iceberg_catalog.default.renamed_table
ALTER COLUMN col1 DROP DEFAULT;
-- Set the format-version
ALTER TABLE iceberg_catalog.default.renamed_table
SET ('format-version' = 3);
Each ALTER TABLE statement updates the current-schema-id of the Iceberg table, and the changes become visible to other Iceberg-aware engines the next time they query the LoadTableInformation endpoint. Iceberg schema evolution is metadata-only, so no data files are rewritten.
Partitioning
Tables can be partitioned with the PARTITIONED BY clause using the Iceberg partition transforms:
| Transform | Description |
|---|---|
column |
Identity: partition by the column value directly. |
year(column), month(column), day(column), hour(column) |
Partition by a date or timestamp component. |
bucket(n, column) |
Hash the column's value into n buckets. This is useful for stable partitioning on a high-cardinality column. |
truncate(w, column) |
Group rows by the first w characters (or by the column's value rounded down to a multiple of w for numeric columns). This is useful for prefix-based partitioning. |
CREATE TABLE iceberg_catalog.default.events (
event_id BIGINT,
user_id BIGINT,
country VARCHAR,
event_time TIMESTAMP
)
PARTITIONED BY (day(event_time), bucket(16, user_id), truncate(2, country));
The partition spec of an existing table can be changed with ALTER TABLE ... SET PARTITIONED BY:
ALTER TABLE iceberg_catalog.default.events
SET PARTITIONED BY (month(event_time));
The bucket and truncate transforms are also available as the iceberg_bucket and iceberg_truncate functions.
The
write.target-file-size-bytesandwrite.parquet.row-group-size-bytestable properties are honored for both partitioned and unpartitioned tables. Files are rotated at row-group boundaries, so if the row-group size exceeds the target file size, DuckDB caps the row-group size to the target file size.
Iceberg V3 Support
DuckDB-Iceberg supports the following Iceberg v3 specification features for both reads and writes:
VARIANTandTIMESTAMP_NSdata types- Schema-level default values for columns
- Binary deletion vectors
- Row lineage tracking
The biggest change in practice is binary deletion vectors. In v2 tables, DuckDB-Iceberg writes positional deletes as Parquet files; in v3 tables, the same information is encoded as a much more compact binary deletion vector (Puffin file). DuckDB picks the right format automatically based on the table's format-version. You can create a v3 table by setting the format-version table property at creation time:
CREATE TABLE iceberg_catalog.default.v3_table
WITH ('format-version' = 3) AS
FROM (VALUES
(1, {'kind': 'click', 'x': 10}::VARIANT, TIMESTAMP_NS '2026-05-20 12:00:00.123456789'),
(2, {'kind': 'view'}::VARIANT, TIMESTAMP_NS '2026-05-20 12:00:00.987654321')
) t(id, payload, event_time);
Warning The
GEOGRAPHYtype is not yet supported in DuckDB-Iceberg.
Limitations for UPDATE and DELETE
The UPDATE and DELETE operations have the following limitations:
- They only work on tables that are not sorted. Attempting these operations on sorted tables results in an error.
- DuckDB-Iceberg only writes positional deletes (encoded as binary deletion vectors for v3 tables). Copy-on-write functionality is not yet supported.
- DuckDB-Iceberg only supports merge-on-read semantics. If a table has
write.update.modeorwrite.delete.modeproperties set to something other thanmerge-on-read, the operation fails.
Unsupported Operations
The following are not yet supported by the DuckDB iceberg extension:
- The
GEOGRAPHYdata type - Copy-on-write semantics for
UPDATE,DELETE, andMERGE INTO