Menu

How it works

Table formats never overwrite data files in place. Each write adds new files and records a new version (Delta) or snapshot (Iceberg) that lists which files are live. Older versions remain valid lists of files, so reading “as of version 5” just means reading the files version 5 pointed to.

  • Delta Lake: SELECT * FROM t VERSION AS OF 5 or TIMESTAMP AS OF '2026-10-20', RESTORE TABLE t TO VERSION AS OF 5. History lasts until VACUUM deletes unreferenced files (7 days by default) and log retention (30 days by default) expires.
  • Iceberg: FOR VERSION AS OF <snapshot_id> or FOR TIMESTAMP AS OF, until snapshots are expired.
  • Snowflake: AT (OFFSET => -3600) or BEFORE (STATEMENT => '<query id>'), UNDROP TABLE, and zero-copy clones of a past state, for 1 day by default and up to 90 days on Enterprise edition. Fail-safe adds 7 more days that only Snowflake support can recover.

Typical uses are undoing a bad load or delete, comparing today’s table with yesterday’s to debug a metric, and reproducing the exact data an ML model or report used. Time travel is not a backup: retention is short, and dropping or vacuuming removes it.

Example

An accidental delete removes two rows. The old version is still readable, and restoreToVersion brings it back as a new commit (PySpark 4.2.0 with delta-spark 4.4.0).

import os, tempfile
from pyspark.sql import SparkSession
from delta import configure_spark_with_delta_pip
from delta.tables import DeltaTable

builder = (SparkSession.builder.master("local[2]").appName("glossary-time-travel")
           .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension")
           .config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog")
           .config("spark.sql.shuffle.partitions", "2"))
spark = configure_spark_with_delta_pip(builder).getOrCreate()
spark.sparkContext.setLogLevel("ERROR")
path = os.path.join(tempfile.mkdtemp(), "orders")

spark.createDataFrame([(1, 40.0), (2, 25.0), (3, 10.0)], "id INT, amount DOUBLE").write.format("delta").save(path)
table = DeltaTable.forPath(spark, path)
table.delete("amount < 30")                                    # version 1: an accidental delete

print("now:", spark.read.format("delta").load(path).count(), "rows")
print("version 0:", spark.read.format("delta").option("versionAsOf", 0).load(path).count(), "rows")

table.restoreToVersion(0)                                      # version 2: undo it
print("after restore:", spark.read.format("delta").load(path).count(), "rows")
table.history().select("version", "operation").orderBy("version").show()
spark.stop()
now: 1 rows
version 0: 3 rows
after restore: 3 rows
+-------+---------+
|version|operation|
+-------+---------+
|      0|    WRITE|
|      1|   DELETE|
|      2|  RESTORE|
+-------+---------+

Common mistakes

  • Running VACUUM with zero retention to save storage, which removes the versions you might need to restore.
  • Treating time travel as a backup strategy when retention is days, not months.
  • Raising Snowflake retention on large, frequently changing tables without counting the storage cost.
  • Forgetting that a restore is a new version: downstream incremental jobs see it as a change.
Search
Filter by type