The delta extension adds support for the Delta Lake open-source storage format. It is built using the Delta Kernel. The extension reads Delta tables, local and remote, and writes them: it can create tables and append to them.
For implementation details, see the “Native Delta Lake Support in DuckDB” blog post.
To connect to Unity Catalog, DuckDB has the
unity_catalogextension.
Installing and Loading
The delta extension will be transparently autoloaded on first use from the official extension repository.
If you would like to install and load it manually, run:
INSTALL AND LOAD delta;
Usage
To scan a local Delta table, run:
SELECT *
FROM delta_scan('file:///path');
Attaching Tables
A Delta table can also be attached as a database. The database holds exactly one table, with the same name as the database, so my_table and my_table.my_table refer to the same table:
ATTACH 'file:///path' AS my_table (TYPE delta);
SELECT * FROM my_table;
Reading from an S3 Bucket
To scan a Delta table in an S3 bucket, run:
SELECT *
FROM delta_scan('s3://my_bucket/path');
To authenticate, create a secret:
CREATE SECRET (
TYPE s3,
PROVIDER credential_chain
);
SELECT *
FROM delta_scan('s3://my_bucket/path');
For a public bucket, you may need to set its region in a secret:
CREATE SECRET (
TYPE s3,
REGION 'my_region'
);
SELECT *
FROM delta_scan('s3://my_bucket/path');
Reading from Azure Blob Storage
To scan a Delta table in an Azure Blob Storage container, run:
SELECT *
FROM delta_scan('az://my_container/path');
To authenticate, create a secret:
CREATE SECRET (
TYPE azure,
PROVIDER credential_chain,
ACCOUNT_NAME 'storage_account_name'
);
SELECT *
FROM delta_scan('az://my_container/path');
Reading from Google Cloud Storage
To scan a Delta table in a GCS bucket through its S3 API compatibility, use HMAC keys and create a secret:
CREATE SECRET (
TYPE gcs,
KEY_ID 'hmac_key_id',
SECRET 'hmac_secret'
);
SELECT *
FROM delta_scan('gs://my_bucket/path');
Creating Tables
New This feature was introduced in DuckDB 2.0.
To create a Delta table, attach the location for it and create a table named after the attached database:
ATTACH 's3://my_bucket/path' AS events (TYPE delta);
CREATE TABLE events.events (id BIGINT, region VARCHAR)
PARTITIONED BY (region);
INSERT INTO events VALUES (1, 'eu');
Only CREATE TABLE needs the full events.events name: without the database name, it creates the table in the default database.
The table can be read and written without attaching it again.
Warning
ATTACHdoes not check that the path holds a Delta table. A mistyped path attaches as an empty database and only fails on the first read.
Table Properties
Options in a WITH clause are stored as Delta table properties, except location, which must match the attached path:
ATTACH 's3://my_bucket/other_path' AS audit (TYPE delta);
CREATE TABLE audit.audit (id BIGINT, name VARCHAR)
WITH ('delta.enableDeletionVectors' = 'true', 'delta.appendOnly' = 'true');
Properties that enable a Delta feature, such as deletion vectors, also upgrade the table's protocol as needed. Values can be any constant expression except NULL. Some properties cannot be set at creation, such as 'delta.enableVariantShredding'.
To create a table with column mapping, set 'delta.columnMapping.mode' to 'name' or 'id'.
Appending Data
To append rows to a Delta table, attach it and use INSERT INTO:
ATTACH 's3://my_bucket/path' AS my_table (TYPE delta);
INSERT INTO my_table SELECT * FROM other_table;
Since v1.5.6, if the table schema declares CHAR(n) or VARCHAR(n) (Spark writes these), a write with a value longer than n characters fails as a whole.
Time Travel
New Time travel by timestamp was introduced in DuckDB 2.0.
To read a table as it was at an earlier version or point in time, use the AT clause, an ATTACH option or a delta_scan parameter:
-- Query an attached table
SELECT * FROM my_table AT (VERSION => 5);
SELECT * FROM my_table AT (TIMESTAMP => TIMESTAMPTZ '2026-09-01 12:00:00+00');
-- Attach the table at a version or point in time
ATTACH 's3://my_bucket/path' AS my_table_v5 (TYPE delta, VERSION 5);
ATTACH 's3://my_bucket/path' AS my_table_then (TYPE delta, TIMESTAMP TIMESTAMPTZ '2026-09-01 12:00:00+00');
-- Scan without attaching
SELECT * FROM delta_scan('s3://my_bucket/path', version => 5); -- since v1.5.4
SELECT * FROM delta_scan('s3://my_bucket/path', timestamp => TIMESTAMPTZ '2026-09-01 12:00:00+00');
- A table attached at a version or timestamp is read-only.
- A version or timestamp given to
ATTACHis the default for queries on that table; anATclause overrides it. - A version and a timestamp cannot be given together.
- A timestamp reads the latest version committed at or before it. One later than
now()or earlier than the table's first commit fails. - A timestamp without a time zone uses the session's
TimeZone. - Within a transaction, a timestamp always reads the same version.
Checkpointing
Since v1.5.1, CHECKPOINT compacts the Delta log of an attached table into a checkpoint file, which speeds up reads of tables with long logs:
ATTACH 'path' AS my_table (TYPE delta);
CHECKPOINT my_table;
Idempotent Appends
To retry a batch without writing it twice, tag the append with an application ID and a version. DuckDB commits the append only if the table's last recorded version for that ID is the one you expect. This uses Delta's per-application transaction versions.
Within a transaction, tag the append with delta_set_transaction_version(table, app_id, new_version, expected_previous_version):
ATTACH 'path' AS my_table (TYPE delta);
BEGIN TRANSACTION;
CALL delta_set_transaction_version('my_table', 'my_app_id', 1::UBIGINT, NULL::UBIGINT);
INSERT INTO my_table VALUES (1);
COMMIT;
If another process committed a different version for my_app_id first, COMMIT fails. A rolled-back transaction leaves the version unchanged. Read the current version with delta_get_transaction_version(table, app_id), which returns NULL if no version has been recorded yet.
Attach Options
When attaching a Delta table you can pass the following options to ATTACH:
| Option | Type | Default | Description |
|---|---|---|---|
VERSION |
UBIGINT |
latest | Attach the table at a specific table version. |
TIMESTAMP |
TIMESTAMP WITH TIME ZONE |
latest | Attach the table at the version current at a point in time. |
PIN_SNAPSHOT |
BOOLEAN |
false |
Resolve the latest table snapshot once, on first use, and reuse it for every later query. Without it, each transaction reads the latest version; since v1.5.3, it loads only the log entries committed since the previous read. |
PUSHDOWN_PARTITION_INFO |
BOOLEAN |
true |
Push down partition information so that whole files can be skipped based on partition values. |
PUSHDOWN_FILTERS |
VARCHAR |
all |
Filter pushdown mode for file skipping. One of none, all, constant_only, dynamic_only. |
ATTACH 's3://my_bucket/path' AS my_table (
TYPE delta,
PIN_SNAPSHOT true,
PUSHDOWN_FILTERS 'constant_only'
);
Inspecting Scanned Files
delta_list_files returns the data files a scan would read for a table, together with their cardinality, partition values, and whether they carry deletion vectors. This is useful for understanding the effect of data skipping:
SELECT * FROM delta_list_files('file:///path');
| Column | Type | Description |
|---|---|---|
data_file |
VARCHAR |
Path to the Parquet data file. |
cardinality |
UBIGINT |
Number of rows in the file. |
partitions |
MAP(VARCHAR, VARCHAR) |
Partition column values for the file. |
have_deletes |
BOOLEAN |
Whether the file has an associated deletion vector. |
To list the files of an earlier table version, pass version => version. To add a delete_count column with the number of rows each file's deletion vector removes, pass delete_count => true:
SELECT data_file, cardinality, delete_count
FROM delta_list_files('file:///path', version => 3, delete_count => true);
Reading Domain Metadata
Since v1.5.1, delta_domain_metadata returns one row per domain of a table, with the VARCHAR columns domain and configuration. Pass version => version for an earlier version:
SELECT domain, configuration
FROM delta_domain_metadata('file:///path');
For an attached table, duckdb_tables() shows the same pairs in its tags column.
Column Mapping
Delta tables with column mapping enabled are read as the Delta protocol specifies for each mode: in id mode, columns are matched by Parquet field ID; in name mode, by physical column name.
The protocol requires every data file of an id-mode table to carry Parquet field IDs, but some writers omit them, including DuckDB 1.5.5 and earlier. Since v1.5.6, the delta_column_mapping_policy setting decides how such files are read:
strict: fail with an error that names the file.lenient: match the file's columns by physical name, or failing that by logical name, and log a warning under thedelta.ColumnMappinglog type. The names must cover every column in the file, otherwise the read fails.
Warning The default changed from
lenientin DuckDB 1.5 tostrictin v2.0. To read such files as v1.5 did, runSET delta_column_mapping_policy = 'lenient';.
VARIANT Columns
Delta variant columns are read as DuckDB VARIANT, whether their files are shredded or not, and top-level VARIANT columns can be appended to.
New The following were introduced in DuckDB 2.0.
- A query that reads one field of a
VARIANTorSTRUCTcolumn, such asSELECT v.a, reads only that field from the Parquet files, so on a shredded table it skips the columns of the other fields. This does not apply to a column with aTIMESTAMP WITH TIME ZONEfield at any depth: DuckDB reads all of it. CREATE TABLEacceptsVARIANTcolumns.- When writing, DuckDB shreds
VARIANTvalues only if the table sets'delta.enableVariantShredding'to'true', as the Delta protocol requires; otherwise it writes them unshredded. BecauseCREATE TABLEcannot set this property, a table created by DuckDB holds unshredded files.
Catalog-Managed Tables
A Delta table with the catalogManaged feature, such as a Unity Catalog managed table, has commits that only its catalog tracks. Reading or writing such a table straight from storage, with delta_scan or ATTACH ... (TYPE delta), fails with an error. Attach its catalog instead, for example with the unity_catalog extension, and query the table through it.
Credential Chains in Delta
For some network operations, the delta extension uses delta-kernel-rs and object_store, which try credential sources in a different order than DuckDB and include different ones by default. If more than one source is available, e.g., a service principal in the environment and the Azure CLI, credential loading may be inconsistent. Configure exactly one credential type in the chain of your secret.
Since v1.5.6, the delta extension supports Azure workload identity (for example on AKS): name workload_identity in the CHAIN of an Azure credential_chain secret, and set the AZURE_FEDERATED_TOKEN_FILE, AZURE_CLIENT_ID and AZURE_TENANT_ID environment variables.
Settings
The delta extension adds the following settings:
| Setting | Type | Default | Description |
|---|---|---|---|
delta_column_mapping_policy |
VARCHAR |
strict |
How to read an id-mode data file without Parquet field IDs: strict raises an error, lenient matches its columns by name. See Column Mapping. |
delta_kernel_logging |
BOOLEAN |
false |
Forward the internal logging of the Delta Kernel to the DuckDB logger. May impact performance even when DuckDB logging is disabled. |
delta_scan_explain_files_filtered |
BOOLEAN |
true |
Add the filtered files to the EXPLAIN output. May impact the performance of delta_scan during EXPLAIN ANALYZE queries. |
Features
The delta extension supports:
- multithreaded scans and Parquet metadata reading
- data skipping/filter pushdown
- skipping row groups in file (based on Parquet metadata)
- skipping complete files (based on Delta partition information and file statistics)
- projection pushdown, down to single fields of struct and
VARIANTcolumns - scanning tables with deletion vectors
- column mapping in
nameandidmode - time travel by version or timestamp
- all primitive types except intervals
- nested types (structs, lists and maps)
- creating tables (
CREATE TABLE) - blind appends (
INSERT INTO), with file statistics for data skipping - idempotent appends
- checkpoints (
CHECKPOINT) - cloud storage (AWS S3, Azure, GCS) with secrets
Limitations
CREATE OR REPLACE TABLEandCREATE TABLE ... AS SELECTare not supported. Create the table, thenINSERTinto it.UPDATE,DELETE,MERGE INTO,ALTER TABLEandDROP TABLEare not supported, nor areRETURNINGandON CONFLICTclauses onINSERT.CREATE TABLEdoes not support:- constraints, including
NOT NULL DEFAULTvalues and generated columnsSORTED BY- column types without a Delta equivalent, such as
TIMEor unsigned integers - a table whose columns are all partition columns
- constraints, including
- A table created with column mapping cannot have nested or partition columns, because DuckDB cannot write to such tables yet.
- Tables with an interval (
interval year to monthorinterval day to second),geometryorgeographycolumn cannot be read. - Writing to a table with the
allowColumnDefaultswriter feature fails, even if no column has a default yet: DuckDB cannot fill in Delta defaults and does not writeNULLinstead. Reading works. - Writing to a table that declares a
CHAR(n)orVARCHAR(n)width on a nested field, such asARRAY<VARCHAR(5)>, is not supported. - Writing to a table with a
VARIANTinside a struct, list or map is not supported.
Supported Platforms
The delta extension currently only supports the following platforms:
- Linux (x86_64 and ARM64):
linux_amd64andlinux_arm64 - macOS Intel and Apple Silicon:
osx_amd64andosx_arm64 - Windows AMD64:
windows_amd64
Support for the other DuckDB platforms is work-in-progress.
Using delta-rs with DuckDB
This example writes a Delta table with the delta-rs Python package and reads it with DuckDB. It then reads the table's change data feed through the Arrow zero-copy integration. For larger data, use Arrow Datasets to read it lazily.
The example needs deltalake, the Python package of delta-rs, with its pyarrow extra:
pip install duckdb "deltalake[pyarrow]"
Full example
import deltalake as dl
import duckdb
import pyarrow as pa
# Create a delta table and read it with DuckDB Delta extension
dl.write_deltalake(
"tmp/some_table",
pa.table({
"id": [1, 2, 3],
"value": ["a", "b", "c"]
})
)
with duckdb.connect() as conn:
conn.sql("""
SELECT * FROM delta_scan('tmp/some_table')
""").show()
# Append some data and read the change data feed, an Arrow record batch reader
dl.write_deltalake(
"tmp/some_table",
pa.table({
"id": [4, 5],
"value": ["d", "e"]
}),
mode="append"
)
table = dl.DeltaTable("tmp/some_table").load_cdf(starting_version=1, ending_version=2)
with duckdb.connect() as conn:
conn.register("t", table)
conn.sql("SELECT * FROM t").show()