Querying
The Timelake reader is Apache DataFusion with a Timelake table provider. You write standard SQL (DataFusion’s dialect); tables are addressed as namespace.table and always read at their latest committed version.
Time buckets
time_bucket(interval, ts [, origin]) works as in TimescaleDB. It is DataFusion’s date_bin under another name: buckets are aligned to the Unix epoch unless you pass an origin.
SELECT device_id, time_bucket(INTERVAL '5 minutes', time) AS bucket, avg(temp), max(temp) FROM iot.readings WHERE time >= now() - INTERVAL '1 day' GROUP BY 1, 2
What the reader accelerates
Time and series filters
Filters are evaluated against each file’s Iceberg column bounds before any data is read, so a time range or a series key value skips whole files. Inside the sorted Parquet files, row-group statistics and page indexes skip the rest.
Latest row per series
This query shape is rewritten to read the last-value index instead of scanning the table:
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY device_id ORDER BY time DESC) AS rn FROM iot.readings ) WHERE rn = 1
The PARTITION BY columns must be exactly the series key and the ordering must be the time column descending. Other predicates combined with rn = 1 are applied to the latest rows, as the SQL means.
Latest row matching a series filter
SELECT * FROM iot.readings WHERE device_id = 'pump-7' ORDER BY time DESC LIMIT 1
Rewritten to the index when the filters reference only series key columns and the limit is 1. A filter on any other column (for example temp > 80) keeps the normal scan, because the latest matching row may not be the latest row.
Both rewrites need a last-value index, which every append maintains. Check with EXPLAIN: a rewritten query reads an in-memory source instead of Parquet files.
Object storage
Before planning, the reader loads every table a query references concurrently (version hint, metadata, manifests, last-value index), and it reads each Parquet footer once. On remote stores it uses more scan partitions than cores (4× cores, at least 32) because the work is latency-bound. Table versions loaded for planning are reused for one second within a process; the process’s own commits are always visible immediately.
Joins with plain tables
SELECT d.site, avg(r.temp) FROM iot.readings r JOIN iot.devices d USING (device_id) WHERE r.time >= TIMESTAMP '2026-01-03 00:00:00' GROUP BY 1
Reading with DuckDB
Timelake tables follow Iceberg’s Hadoop-catalog layout, so DuckDB’s iceberg extension reads a table directory directly. It sees committed data and none of the reader’s rewrites.
INSTALL iceberg; LOAD iceberg;
SELECT * FROM iceberg_scan('s3://my-bucket/lake/iot/readings');
-- the last-value index is plain Parquet; timelake duckdb <table> prints its current location
SELECT * FROM read_parquet('s3://my-bucket/lake/iot/readings/metadata/lvi-….parquet');Duplicates written since the last compaction are visible to DuckDB until compaction removes them; the Timelake reader sees the same rows.