Search Shortcut cmd + k | ctrl + k

DuckLake and DuckDB databases on PostgreSQL - pgvfs:// stores data files as PostgreSQL rows for many writers and readers; pgdb:// stores a DuckDB database for one writer and many readers

Maintainer(s): adonm

Compatibility Matrix

This table covers all DuckDB releases that have not reached end-of-life, including LTS releases, and shows the 2 most recent patch releases of each. Older patch releases are not listed, but the extension may be available for them as well.

Platform 1.4.4 1.4.5 1.5.5 1.5.6 2.0.0
Linux AMD64
Linux ARM64
macOS AMD64
macOS ARM64
Windows AMD64
Windows ARM64
WebAssembly

Installing and Loading

INSTALL pgvfs FROM community;
LOAD pgvfs;

Example

-- One secret serves both the DuckLake catalog and the pgvfs data files.
CREATE SECRET (TYPE postgres, HOST 'db', DATABASE 'lake', USER 'lake', PASSWORD '...');
ATTACH 'ducklake:postgres:' AS lake (DATA_PATH 'pgvfs://lake/');

-- Once per lake, before the first insert: small row groups and files for fast
-- lookups, zstd to keep PostgreSQL storage small (LZ4 reads faster, stores more).
CALL lake.set_option('parquet_compression', 'zstd');
CALL lake.set_option('parquet_version', 2);
CALL lake.set_option('parquet_row_group_size', 8192);
CALL lake.set_option('target_file_size', '64MB');

CREATE TABLE lake.events (site_id INTEGER, day DATE, value DOUBLE);
ALTER TABLE lake.events SET SORTED BY (site_id, day);
INSERT INTO lake.events
    SELECT range % 1000, DATE '2026-01-01' + (range % 365)::INTEGER, random() FROM range(1000000);

-- Readers, in other processes: SELECT-only roles, read replicas welcome.
-- ATTACH 'ducklake:postgres:' AS lake (READ_ONLY);
SELECT count(*) FROM lake.events WHERE site_id = 42 AND day = DATE '2026-02-12';

-- Counters for one process: opens, reads, bytes, time in pgvfs.
SELECT pgvfs_stats()::JSON;

About pgvfs

pgvfs is a DuckDB filesystem, pgvfs://, that stores DuckLake's Parquet data files as rows in PostgreSQL. With the DuckLake catalog in the same database, one PostgreSQL is the whole lake: one secret, one backup, one set of roles, and no object store or gateway.

  • Many writers and readers. Readers need only SELECT, take no locks and work on streaming read replicas. Any process whose PostgreSQL role can write may write, with no lock between writers; roles decide who writes.
  • Fast lookups. Files are immutable, so DuckDB caches them without revalidating, and reads are primary-key range queries straight into DuckDB's buffers.
  • Plain PostgreSQL 14+, including Amazon Aurora. No PostgreSQL extensions or superuser. The first write installs pgvfs's schema.
  • Native DuckDB databases (experimental). ATTACH 'pgdb://lake/sales.duckdb' stores a whole DuckDB database, with indexes, keys and constraints, in the same PostgreSQL: one writer, any number of readers (standbys too) that follow new commits by themselves.
  • Full-text search. Tantivy indexes built and searched from SQL (tantivy_index, tantivy_search, tantivy_count, tantivy_aggregate, tantivy_merge, and fts-style macros): OpenSearch-style queries, sorting, highlights, exclusion of deleted rows, splits per DuckLake commit, on pgvfs or any DuckDB filesystem.

It suits lakes that fit comfortably in one PostgreSQL, with many concurrent readers doing lookups and dashboard-style queries. For scans over large lakes in object storage, a plain DuckLake on S3 is the better fit.

Layout decides most of the read speed: set the lake options above before the first insert, declare a sort order that matches your filters, and load big tables in key-range batches. Credentials resolve per path: a postgres secret named pgvfs_<volume>, else the one named by SET pgvfs_secret, else $PGVFS_URL, else the default postgres secret. Remote servers need SSLMODE 'require'.

Documentation: install, loading data, databases, full-text search, vector search, how it works and performance.

Added Functions

function_name function_type description comment examples
pgvfs_drop_volume scalar Removes every file in a pgvfs volume, unpublishing it for the reap grace. Returns the number of files. NULL [SELECT pgvfs_drop_volume('scratch')]
pgvfs_stats scalar This process's pgvfs counters as JSON: file opens and open-cache hits, reads, bytes, time spent in pgvfs and range queries sent to PostgreSQL. Cumulative; diff two samples. NULL [pgvfs_stats()::JSON]
tantivy_aggregate scalar Tantivy aggregations (an Elasticsearch-shaped JSON request: terms, histograms, statistics, cardinality) over the documents matching a query, as JSON. NULL [SELECT tantivy_aggregate('docs.tantivy', 'cats', '{"by_tag": {"terms": {"field": "tag"}}}')]
tantivy_count scalar The exact number of documents matching a query in a split or a list of splits. Options (JSON): limit (stop counting after limit + 1), distinct (the number of distinct values of a fast field). NULL [SELECT tantivy_count('docs.tantivy', 'cats')]
tantivy_create_index table_macro Builds a split of the given text columns of a table, keyed by an id column, like DuckDB's fts create_fts_index. Named parameters: stemmer, stopwords, strip_accents, lower. Returns the number of rows indexed. NULL [FROM tantivy_create_index('docs.tantivy', 'docs', 'id', ['body'])]
tantivy_drop scalar Removes the split at index. Returns whether there was one. NULL [SELECT tantivy_drop('docs.tantivy')]
tantivy_index aggregate Builds one immutable tantivy split file at index from the documents of each group (a row, or JSON text), with a tantivy schema as JSON and optional options as JSON. Returns the number of documents. NULL [SELECT tantivy_index('docs.tantivy', schema, t) FROM docs t]
tantivy_match_bm25 macro The BM25 score of the row with this id for a query against a split, or NULL when it does not match, like DuckDB's fts match_bm25. NULL [SELECT id, tantivy_match_bm25('docs.tantivy', id, 'cats') AS score FROM docs]
tantivy_merge scalar Merges a list of splits into one new split at the target, optionally leaving out excluded documents. Returns the number of documents kept. NULL [SELECT tantivy_merge(['a.tantivy', 'b.tantivy'], 'ab.tantivy')]
tantivy_search table Searches a split, or a list of splits in parallel, with tantivy's query syntax or OpenSearch query DSL (JSON). Returns score, doc (JSON), path and highlight per hit, best first. Options (JSON): top_k, offset, sort, collapse, highlight, fast, exclude_field. exclude: a roaring BLOB or BIGINT[] of documents to skip. NULL [SELECT score, doc FROM tantivy_search('docs.tantivy', 'small cats', '{"top_k": 10}')]

Overloaded Functions

This extension does not add any function overloads.

Added Types

This extension does not add any types.

Added Settings

name description input_type scope aliases
pgdb_refresh_interval Seconds between checks for a newer published version of each pgdb:// database attached READ_ONLY by its plain path; a newer one is attached in its place (running transactions keep theirs). 0 turns refreshing off DOUBLE GLOBAL []
pgvfs_secret Name of the postgres secret pgvfs:// connects with, unless a secret named pgvfs_ exists for the volume. Unset: $PGVFS_URL, else the unnamed default postgres secret (the one DuckLake's catalog uses) VARCHAR GLOBAL []