Feature Request: pin a table to the catalog, and push filters down to inlined data #1401
tanejagagan
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Tested with: DuckDB v1.5.5,
ducklakeextensiond8a1881e, catalogs on both alocal DuckDB file and PostgreSQL 16.
Data inlining makes small writes land in the catalog database instead of creating tiny
Parquet files. For a workload of many small, frequent writes this is exactly the right
behaviour, and we would like to rely on it for a table that is intended to stay
catalog-resident: a per-producer high-water-mark table used for ingestion de-duplication.
It is small (one row per producer), read on every batch, and updated constantly.
Two things prevent us from using it that way.
1. There is no supported way to keep a table inlined
Today inlined data stays inlined indefinitely, but only because nothing currently flushes it
— and that is a bug, not a contract. The documentation states that
CHECKPOINTshould flushinlined data, and #1368 tracks the fact that it does not. When #1368 is fixed, any design
that relies on data staying inlined will silently start losing that property.
That is exactly why an explicit opt-out is needed: the behaviour we would like to depend on
is currently indistinguishable from a defect that is scheduled to be repaired.
We confirmed that none of the maintenance routines move inlined data at present:
ducklake_merge_adjacent_filesducklake_expire_snapshotsducklake_cleanup_old_filesducklake_rewrite_data_filesCHECKPOINTOnly
ducklake_flush_inlined_datamoves it.Nor is there an option to express the intent.
data_inlining_row_limitis documented as"Maximum amount of rows to inline in a single insert" — it governs the write path and says
nothing about residency. Every name we probed for a residency option is rejected with
Unsupported option:(For contrast,
data_inlining_row_limit,parquet_compression,parquet_row_group_size,target_file_sizeandparquet_versionare all accepted, so the probe distinguishes realoption names from invented ones.)
So a design that depends on rows staying in the catalog is depending on a defect. Once
CHECKPOINTflushes as documented, that design breaks with no warning and no way to opt out.Request
A persisted option, e.g.
data_inlining_pinned, settable atGLOBALandTABLEscopethrough the existing mechanism:
Semantics: data for a pinned table is never flushed to Parquet implicitly. An explicit
ducklake_flush_inlined_datacould either refuse or require a force flag.Option storage already supports what is needed —
ducklake_optionspersists values withGLOBAL/TABLEscope and they survive re-ATTACH— so this is mechanically small. Thesubstantive part is that it becomes a documented guarantee other DuckLake implementations
must honour, since a writer unaware of the flag would otherwise flush the table.
2. Filters are not pushed down to inlined data
Projection pushdown already works — this is only about filters. Capturing the SQL
DuckLake issues against a PostgreSQL catalog (
log_statement=all), forSELECT last_batch_id FROM dl.wide WHERE producer_id = 'prod-42'on a table with columnsproducer_id, last_batch_id, c1, c2, c3, c4:c1throughc4are correctly omitted, and the projection tracks the query — a secondquery,
SELECT c3 FROM dl.wide WHERE c4 = 84, sends"c4", "c3"and drops the rest. Thatpart behaves well.
The filter is the gap.
producer_id = 'prod-42'never reaches PostgreSQL; the onlypredicate pushed is the snapshot visibility bound
begin_snapshot <= '2'. Every row of theinlined table is streamed to DuckDB and filtered there.
The consequence at scale: a point lookup reads the whole inlined table.
Setup: 100,000 rows inlined in a PostgreSQL catalog, with an index created on the inlined
table:
50 point lookups issued through DuckLake (
SELECT last_batch_id FROM dl.wm WHERE producer_id = 'prod-…'), measured withpg_stat_user_tables:5,000,000 = 50 x 100,000. The entire table is read for every lookup and filtered in
DuckDB; the index is never consulted.
The same 50 lookups issued directly to PostgreSQL, as a control:
So the index is perfectly usable — the predicate simply never reaches PostgreSQL. This is
not a query-formulation problem: expressing the lookup through a DuckDB variable
(
SET VARIABLE pid = 'prod-42'thengetvariable('pid')) produces the identical fullscan,
seq_tup_read=100000, idx_scan=0.Request
Push equality/range predicates on inlined data down to the catalog database when the
catalog is a real DBMS. Failing that, some documented way to make a point lookup on an
inlined table better than O(table).
#859 addressed a structurally similar problem — a full table scan of
ducklake_file_column_statson every query against a PostgreSQL metadata backend — so thereis precedent for pushing work into the catalog rather than pulling rows out of it.
Why this belongs with (1)
Pinning without pushdown gives a guarantee that cannot be used: the table stays in the
catalog forever and every lookup remains a full scan. Pushdown without pinning gives fast
lookups that may evaporate the moment a flush policy is introduced. Together they make an
inlined table usable as what it physically already is — a table in a database.
Workaround we are using, and its cost
Partitioning the table by
hash(key) % Nwith inlining disabled does prune on read:This works and is supported, but it forces us out of inlining entirely for exactly the
table whose access pattern inlining suits best — many tiny writes — and it puts the bucket
computation in the application.
Minor observations from the same investigation
ducklake_inlined_data_<table_id>_<schema_version>, registeredin
ducklake_inlined_data_tables. Schema evolution creates an additional inlined tableper schema version, so one logical table accumulates several. Any index created by hand
on one of them is not carried to the next version.
VARCHARcolumns are stored asbytea(visible in theIndex Condabove). Worth documenting for anyone reading catalog tables directly.All reactions