Time Travel Queries Tutorial
Time Travel Queries Tutorial
Available since: v3.0.0
Build: default — no feature flag required
Module: heliosdb_lite::sql::extensions::time_travel, heliosdb_lite::storage::time_travel
SQL: SELECT … AS OF TIMESTAMP, AS OF TRANSACTION, AS OF SCN, VERSIONS BETWEEN
UVP
Auditors ask “what did this row say last Tuesday?” — and most databases need a sidecar audit table to answer. Lite stores MVCC versions natively and exposes them through three SQL clauses: AS OF TIMESTAMP, AS OF TRANSACTION, and AS OF SCN. Snapshot retention is configurable; queries against historical state run at less than 2× the overhead of a current-state read. Pair with CREATE BRANCH … AS OF to fork the past, or with materialized views for moving baselines. Audit, debug, recover, and compare — all without leaving SQL.
Prerequisites
- HeliosDB Lite v3.0+
- A REPL or
psqlclient - 10 minutes
1. The Three AS OF Variants
-- 1. Wall-clock time (timestamp literal).SELECT * FROM orders AS OF TIMESTAMP '2025-01-15 10:30:00'WHERE customer_id = 42;
-- 2. Transaction id (monotonic; visible via system views).SELECT * FROM inventory AS OF TRANSACTION 987654WHERE product_id = 'SKU-99';
-- 3. System change number (LSN-style global counter).SELECT * FROM audit_log AS OF SCN 123456789;Use TIMESTAMP for human-driven audits (“the state on closing day”), TRANSACTION when you have an explicit txn id (e.g. captured in an outbox), and SCN for cross-table consistency at a single global commit point.
2. Comparing Past and Present
The most common use is a self-join between current and historical state:
SELECT cur.id, cur.price AS current_price, old.price AS price_2025_01_01, cur.price - old.price AS deltaFROM products curJOIN products AS OF TIMESTAMP '2025-01-01 00:00:00' old ON cur.id = old.idWHERE cur.price <> old.priceORDER BY delta DESCLIMIT 20;Every existing optimisation — indexes, materialized views, RLS — applies to historical scans. The only difference is the snapshot the planner reads from.
3. The Rust API
Two convenience methods on EmbeddedDatabase:
use heliosdb_lite::EmbeddedDatabase;
let db = EmbeddedDatabase::new("./mydb.helio")?;
let historical = db.query_as_of_timestamp( "SELECT * FROM products WHERE category = $1", &["Electronics"], "2025-01-01 12:00:00",)?;
let at_txn = db.query_as_of_transaction( "SELECT count(*) FROM orders", &[], 42,)?;For raw control, just feed the AS OF clause through the standard query API — the parser preprocesses the syntax (sql::parser::Parser::preprocess_time_travel_sql) and the executor’s scan node honours it.
4. Use Cases
Audit query — “Who changed this row, and when?”
WITH revisions AS ( SELECT account_id, balance, '2025-01-01' AS snap FROM accounts AS OF TIMESTAMP '2025-01-01' UNION ALL SELECT account_id, balance, '2025-02-01' AS snap FROM accounts AS OF TIMESTAMP '2025-02-01' UNION ALL SELECT account_id, balance, 'now' AS snap FROM accounts)SELECT * FROM revisions WHERE account_id = 'ACC-123' ORDER BY snap;Debugging — “Reproduce the report from yesterday”
SELECT region, sum(total)FROM orders AS OF TIMESTAMP '2026-04-25 23:59:59'GROUP BY region;You don’t need to roll back the database; the live state continues serving traffic.
Soft-recovery without a restore
If a runaway UPDATE zeroed a price column at 09:42:
-- Inspect.SELECT id, price FROM products AS OF TIMESTAMP '2026-04-26 09:41:00'WHERE price = 0;
-- Repair from the historical snapshot.UPDATE products curSET price = old.priceFROM products AS OF TIMESTAMP '2026-04-26 09:41:00' AS oldWHERE cur.id = old.id AND cur.price = 0;Moving baselines for analytics
CREATE MATERIALIZED VIEW yoy_revenue ASSELECT date_trunc('month', cur.order_date) AS month, sum(cur.amount) AS this_year, sum(prev.amount) AS last_yearFROM orders curLEFT JOIN orders AS OF TIMESTAMP (CURRENT_TIMESTAMP - INTERVAL '1 year') prev ON cur.id = prev.idGROUP BY 1;Combined with the auto-refresh scheduler, the comparison stays current without manual recomputation.
5. Branch + Time Travel
CREATE BRANCH … AS OF forks at a historical instant. Once on the branch you can mutate to your heart’s content without disturbing live traffic:
CREATE BRANCH year_end FROM main AS OF TIMESTAMP '2025-12-31 23:59:59';USE BRANCH year_end;
-- Free to write here; main is unaffected.INSERT INTO trial_balance SELECT * FROM accounts;Drop the branch when you’re done. See DATABASE_BRANCHING_TUTORIAL for the full branching story.
6. Retention and Garbage Collection
Historical versions don’t live forever. The retention horizon is configurable:
[phase3]time_travel_retention_days = 30Or per-tenant via the multi-tenant quotas (time_travel_days field on TenantQuotas). Once a version falls outside the window it becomes a candidate for vacuum.
The system view pg_database_branches() reports per-branch sizing; pair with the storage vacuum_backup module if you need finer-grained reclamation control.
7. Performance Notes
The MVCC engine keeps versioned tuples inline with current data; an AS OF read is a regular scan with a different visibility filter. The README quotes <100ms for indexed historical lookups against typical workloads, and end-to-end overhead under 2× compared to a current-state query.
For analytical workloads that hammer historical state, consider creating a materialized view on the AS OF query — the auto-refresh scheduler handles maintenance.
Where Next
- DATABASE_BRANCHING_TUTORIAL — fork from history, write freely, merge back.
- MATERIALIZED_VIEWS_TUTORIAL — pin slow
AS OFjoins to incremental views. - AUDIT_LOGGING_TUTORIAL — combine MVCC history with structured audit events.
- AS_OF_QUICK_REFERENCE — the syntax cheat sheet.
- TIME_TRAVEL_OPTIMIZATION_QUICKSTART — tuning historical-scan throughput.