Collect min/max rowid stats per data file to enable pruning on the rowid meta column #1389
Replies: 2 comments
|
Follow-up: the same gap applies to the Tested on DuckDB 1.5.5 (ducklake Before compaction a After compaction even that goes away. Because the column is now present in the file, the constant-filter path no longer applies and the predicate is evaluated per row group instead. Same table, same query:
So after compaction both meta columns are in the same position: correct results, every file opened.
For Repro (DuckDB 1.5.5): ATTACH 'ducklake:meta.ducklake' AS lake (DATA_PATH 'data/', DATA_INLINING_ROW_LIMIT 0);
CREATE TABLE lake.main.t (customer_id INT, v INT);
ALTER TABLE lake.main.t SET PARTITIONED BY (customer_id);
INSERT INTO lake.main.t SELECT (i % 4)+1, i FROM range(400) r(i); -- run 3 times
EXPLAIN ANALYZE SELECT count(*) FROM lake.main.t WHERE snapshot_id = 99; -- 0 files read, 12 skipped
CALL ducklake_merge_adjacent_files('lake');
EXPLAIN ANALYZE SELECT count(*) FROM lake.main.t WHERE snapshot_id = 99; -- 4 files read, 0 skipped
SELECT name FROM parquet_schema('data/main/t/customer_id=1/*.parquet'); -- shows _ducklake_internal_snapshot_id in the merged file |
|
Third meta column with the same gap: Same setup as above (DuckDB 1.5.5, ducklake
A Same on the change feed: The mechanism is the one already described for Unlike Why this matters in practice: a consumer that already knows which files it wants — from |
Uh oh!
There was an error while loading. Please reload this page.
Problem
A predicate on the
rowidmeta column prunes nothing — every query with arowidfilter lists andopens every data file in the table.
rowidis registered as a virtual column(
DuckLakeTableEntry::GetVirtualColumns()), andDuckLakeMultiFileList::AddFilterToPushdownInfo()returns early for virtual columns, so a
rowidfilter never reaches the metadata-level pruning path.The filter is still applied correctly, it just skips nothing.
Nothing in the metadata catalog records which row ids a data file contains, so there is nothing to
prune against.
ducklake_data_filehasrow_id_startbut no end, andducklake_file_column_statsholds rows only for user columns — I confirmed there is no row for the internal row id column.
Why this matters
rowidis the only stable physical row identifier DuckLake offers, so it is the natural key for afamily of access patterns that are all full scans today: point and range lookups from an application
holding materialized
rowidpointers (a secondary or inverted index, an embedding store, aresumable-processing bookmark), re-reading a row set after a
SELECT rowid, ...pass(
WHERE rowid IN (...)), chunked processing viaWHERE rowid >= $lo AND rowid < $hi, and semi-joinsagainst a delta table keyed by
rowid. Row ids are broadly assigned in append order, so they areexceptionally well clustered per file — close to a best case for zone-map pruning.
The part that makes this non-trivial
The obvious derivation —
[row_id_start, row_id_start + record_count - 1]— is exact for ordinaryfiles, but wrong for files carrying an embedded
_ducklake_internal_row_idcolumn, which the readerprefers when present (flushed inlined data,
MERGE/UPDATErewrites, compaction of non-adjacentfiles).
MERGE ... UPDATEpreserves row ids, so the rewritten file's ids are unrelated to itsassigned
row_id_start. On ducklake v1.5.2:So a pruning predicate built on
row_id_start + record_countwould prune the only live file forWHERE rowid = 0and return zero rows. And no existing metadata column distinguishes the two kindsof file, so the derivation cannot even be applied selectively — whether a file has embedded row ids
is only discoverable by opening its Parquet footer, which is exactly the cost catalog-level pruning
exists to avoid. The range therefore needs to be captured at write time (where the Parquet writer
already computes it for embedded files, and where it is trivially derivable for the rest) and stored
in the metadata catalog.
Possible directions
Two approaches that both keep the pruning decision inside the catalog query: store the row id min/max
as a row in
ducklake_file_column_statsunder a reservedcolumn_id, which needs no DDL ormigration and reuses the existing stats-pruning code path as-is; or add explicit
row_id_min/row_id_maxcolumns toducklake_data_filealongside therow_id_startthey generalize. Happy toleave the choice and the design to the maintainers — I have a more detailed analysis of the write
paths involved if it's useful.
All reactions