# Apache Iceberg
> Attach an Iceberg REST catalog as a MotherDuck database to read and write Iceberg tables, or scan individual tables by path.
MotherDuck supports the Apache Iceberg format through the DuckDB Iceberg extension.

There are two ways to work with Iceberg in MotherDuck:

- **[Persisted Iceberg catalogs](#persisted-iceberg-catalogs)** — attach an Iceberg REST catalog as a MotherDuck database with `CREATE DATABASE`. The attachment lives in your workspace, so it survives across sessions, and reads and writes run on MotherDuck's compute.
- **[Scanning individual tables](#scanning-individual-iceberg-tables)** — query a single Iceberg table by path with `iceberg_scan`, without attaching a catalog.

## Persisted Iceberg catalogs

Attach an [Iceberg REST catalog](https://duckdb.org/docs/stable/core_extensions/iceberg/iceberg_rest_catalogs) as a MotherDuck database. The database persists in your workspace: you attach it once, it appears alongside your other databases, and you don't re-attach it in each new session. Reads and writes run on MotherDuck's [cloud execution engine](/concepts/architecture-and-capabilities#dual-execution).

MotherDuck works with any Iceberg REST catalog endpoint, but your mileage may vary by provider. Amazon S3 Tables and Apache Polaris are tested and supported.

:::note
Persisted Iceberg catalogs require DuckDB 1.5.2 or later. The MotherDuck extension updates automatically when you restart DuckDB.
:::

:::warning
Iceberg REST catalog reads and writes run on MotherDuck's cloud compute. Attaching an Iceberg REST catalog directly in a local DuckDB session (without MotherDuck) is unreliable and not supported here. Attach the catalog as a MotherDuck database instead.
:::

### Authentication

Store your catalog credentials in a MotherDuck secret. Credentials must live in a secret — the database options accept catalog settings only, not credentials.

```sql
-- OAuth2 client credentials
CREATE SECRET my_iceberg_secret IN MOTHERDUCK (
    TYPE ICEBERG,
    CLIENT_ID 'my_client_id',
    CLIENT_SECRET 'my_client_secret',
    OAUTH2_SERVER_URI 'https://my-catalog.example.com/v1/oauth/tokens'
);

-- Bearer token
CREATE SECRET my_iceberg_secret IN MOTHERDUCK (
    TYPE ICEBERG,
    TOKEN 'my_bearer_token'
);
```

See [`CREATE SECRET`](/sql-reference/motherduck-sql-reference/create-secret#iceberg-secrets) for the full list of Iceberg secret parameters.

### Creating the database

:::note
`CREATE DATABASE ... TYPE ICEBERG` does not create a new Iceberg catalog. It connects to an existing REST catalog and registers it as a MotherDuck database, behaving like an attach. The catalog must already exist at the endpoint you point to.
:::

Create the database with `TYPE ICEBERG`, referencing the secret and the catalog endpoint. A `default_schema` that exists in the catalog is required:

```sql
CREATE DATABASE my_datalake (
    TYPE ICEBERG,
    "secret" my_iceberg_secret,
    endpoint 'https://my-catalog.example.com',
    warehouse 'my_warehouse',
    default_schema 'default'
);
```

Once attached, browse and query the catalog with standard SQL:

```sql
-- List schemas
SHOW SCHEMAS IN my_datalake;

-- Inspect a table's columns (duckdb_columns() does not list them)
DESCRIBE my_datalake.default.my_table;

-- Query a table
SELECT * FROM my_datalake.default.my_table;
```

Set the database as the active catalog to use unqualified names:

```sql
USE my_datalake;
SELECT * FROM my_table;
```

### Database options

Pass these options in the `CREATE DATABASE` options list. Credentials (`CLIENT_ID`, `CLIENT_SECRET`, `OAUTH2_*`, `TOKEN`) belong in the [secret](#authentication), not here.

| Option                   | Description                                                                                                         |
| :----------------------- | :------------------------------------------------------------------------------------------------------------------ |
| `secret`                 | Name of the MotherDuck Iceberg or S3 secret holding catalog credentials. Quote as `"secret"`.                       |
| `endpoint`               | URL of the Iceberg REST catalog. Required unless the endpoint is set in the secret or derived from `endpoint_type`. |
| `warehouse`              | Catalog warehouse identifier. For S3 Tables, this is the bucket ARN.                                                |
| `default_schema`         | Required. Schema used to resolve unqualified table names. Must exist in the catalog.                                |
| `endpoint_type`          | Selects a well-known catalog flavor, for example `'s3_tables'` or `'glue'`.                                         |
| `default_region`         | Per-catalog region override. Defaults to your MotherDuck org region.                                                |
| `read_only`              | Attach the catalog as read-only.                                                                                    |
| `access_delegation_mode` | Controls vended-credential delegation, for example `'none'`.                                                        |

For the full set of catalog options, see the [DuckDB Iceberg REST catalog documentation](https://duckdb.org/docs/stable/core_extensions/iceberg/iceberg_rest_catalogs).

### Amazon S3 Tables

For [Amazon S3 Tables](https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables.html), authenticate with an S3 secret (SigV4) and set `endpoint_type` to `'s3_tables'`. The `warehouse` is the table bucket ARN, and the endpoint is derived from it.

```sql
CREATE SECRET s3_tables_secret IN MOTHERDUCK (
    TYPE S3,
    KEY_ID '<aws_access_key_id>',
    SECRET '<aws_secret_access_key>',
    REGION 'us-east-1'
);

CREATE DATABASE my_s3_tables (
    TYPE ICEBERG,
    endpoint_type 's3_tables',
    warehouse 'arn:aws:s3tables:us-east-1:<account_id>:bucket/<bucket_name>',
    "secret" s3_tables_secret,
    default_schema 'default'
);
```

### Reading and writing

A persisted Iceberg catalog supports standard DDL and DML, executed on MotherDuck's compute: creating schemas and tables, inserting data, partitioned writes, `MERGE INTO`, and `ALTER TABLE`.

```sql
CREATE SCHEMA my_datalake.analytics;

CREATE TABLE my_datalake.analytics.events (
    event_id INTEGER,
    event_type VARCHAR,
    created_at TIMESTAMP
);

INSERT INTO my_datalake.analytics.events
VALUES (1, 'page_view', '2025-01-15 10:30:00');

ALTER TABLE my_datalake.analytics.events
SET PARTITIONED BY (year(created_at));
```

Refer to the [DuckDB Iceberg documentation](https://duckdb.org/docs/stable/core_extensions/iceberg/iceberg_rest_catalogs) for the current support matrix for write operations and time travel.

### Time travel

Query a historical snapshot of a catalog table with the `AT` clause, by snapshot ID or timestamp:

```sql
-- Query a specific snapshot by ID
SELECT * FROM my_datalake.default.my_table
    AT (VERSION => 1234567890);

-- Query as of a timestamp
SELECT * FROM my_datalake.default.my_table
    AT (TIMESTAMP => TIMESTAMP '2025-01-15 10:30:00');
```

### Limitations

- Iceberg catalogs can't be shared. To give another account access to the same catalog, create the same Iceberg database in that account.
- `ALTER DATABASE` is not supported on Iceberg databases. To change catalog options, drop and recreate the database.
- `INSERT` and `UPDATE` are not supported on tables that have a sort order.
- Table columns are not populated in `duckdb_columns()`. Run `DESCRIBE <table>` to see a table's columns.
- Reading from REST catalogs is limited to S3, S3 Tables, and GCS storage backends.
- Converting an Iceberg catalog to DuckLake with `iceberg_to_ducklake` is not supported.

## Scanning individual Iceberg tables

Use `iceberg_scan` to query individual Iceberg tables directly by path, without attaching a catalog:

```sql
SELECT count(*)
FROM iceberg_scan('s3://my-bucket/my-iceberg-table',
    allow_moved_paths = true);
```

:::note

To query data in a secure Amazon S3 bucket, configure your [Amazon S3 credentials](../../cloud-storage/amazon-s3).

:::

### `iceberg_scan` parameters

| Parameter                    | Type        | Default                                    | Description                                                          |
| :--------------------------- | :---------- | :----------------------------------------- | :------------------------------------------------------------------- |
| `allow_moved_paths`          | `BOOLEAN`   | `false`                                    | Allow scanning Iceberg tables that have been moved or relocated      |
| `metadata_compression_codec` | `VARCHAR`   | `''`                                       | Set to `'gzip'` to read gzip-compressed metadata files               |
| `snapshot_from_id`           | `UBIGINT`   | `NULL`                                     | Query a specific snapshot by ID                                      |
| `snapshot_from_timestamp`    | `TIMESTAMP` | `NULL`                                     | Query the latest snapshot as of a given timestamp                    |
| `version`                    | `VARCHAR`   | `'?'`                                      | Explicit version string, hint file path, or `'?'` for auto-detection |
| `version_name_format`        | `VARCHAR`   | `'v%s%s.metadata.json,%s%s.metadata.json'` | Custom metadata filename pattern                                     |

### Time travel with `iceberg_scan`

```sql
-- Query a specific snapshot
SELECT *
FROM iceberg_scan('s3://my-bucket/my-iceberg-table',
    allow_moved_paths = true,
    snapshot_from_id = 1234567890);

-- Query as of a timestamp
SELECT *
FROM iceberg_scan('s3://my-bucket/my-iceberg-table',
    allow_moved_paths = true,
    snapshot_from_timestamp = TIMESTAMP '2025-01-15 10:30:00');
```

### Metadata and snapshot functions

Use `iceberg_metadata` to inspect manifest entries (file paths, formats, record counts):

```sql
SELECT *
FROM iceberg_metadata('s3://my-bucket/my-iceberg-table',
    allow_moved_paths = true);
```

Use `iceberg_snapshots` to list available snapshots:

```sql
SELECT *
FROM iceberg_snapshots('s3://my-bucket/my-iceberg-table');
```

### Example with sample dataset

```sql
SELECT count(*)
FROM iceberg_scan('s3://us-prd-motherduck-open-datasets/iceberg/lineitem_iceberg',
    allow_moved_paths = true);
```


---

## Docs feedback

MotherDuck accepts optional user-submitted feedback about this page at `GET https://motherduck.com/docs/api/feedback/agent`.
For agents and automated tools, feedback submission should be user-confirmed before sending.

URL-encode query parameter values and send a GET request:

```text
GET https://motherduck.com/docs/api/feedback/agent?page_path=%2Fintegrations%2Ffile-formats%2Fapache-iceberg%2F&page_title=Apache%20Iceberg&text=<url-encoded user feedback, max 2000 characters>
```

Optionally append `&source=<url-encoded interface identifier>` such as `claude.ai` or `chatgpt`.

`page_path` and `text` are required; `page_title` and `source` are optional. Responses: `200 {"feedback_id": "<uuid>"}`, `400` for malformed query parameters, and `429` when rate-limited.
