⌘K ctrl+k
Search Shortcut cmd + k | ctrl + k
Writing to Iceberg

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 SCHEMA
  • CREATE/DROP TABLE
  • INSERT INTO
  • UPDATE
  • DELETE
  • MERGE INTO
  • ALTER TABLE
  • SELECT

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-bytes and write.parquet.row-group-size-bytes table 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:

  • VARIANT and TIMESTAMP_NS data 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 GEOGRAPHY type 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.mode or write.delete.mode properties set to something other than merge-on-read, the operation fails.

Unsupported Operations

The following are not yet supported by the DuckDB iceberg extension:

  • The GEOGRAPHY data type
  • Copy-on-write semantics for UPDATE, DELETE, and MERGE INTO
© 2026 DuckDB Foundation, Amsterdam NL
DuckDB Home Code of Conduct Trademark Use Blog