Temporal Tables

By the end of this lesson you'll be able to make the database keep a full, automatic history of every row — and then query "what did this look like last Tuesday?" with a single line. Temporal tables turn an ordinary table into a time machine you can rewind to any instant.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free SQL course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

An ordinary table only remembers the present . When you UPDATE a row, the old value is overwritten and gone forever. A system-versioned temporal table fixes that: the database keeps every previous version of every row automatically, in a paired history table , so you can look up any past state on demand.

A normal table is a whiteboard — erase and rewrite, and the old text is lost. A temporal table is a notebook : every edit starts a fresh line, and the previous lines stay on the page. You can always flip back to see what was written on any date.

"System-versioning" means the system (the DB engine), not you, manages the versioning. You keep writing normal INSERT , UPDATE , and DELETE statements; the engine quietly files away each superseded row for you.

"Temporal" is an umbrella term. There are two distinct clocks, and mixing them up is the most common beginner mistake:

When you switch on SYSTEM_VERSIONING , the engine creates (or attaches) a second table — here employees_history — with the exact same columns as employees . The two stay in lockstep: add a column to the main table and the history table gains it too.

The split is simple: employees holds only the current version of each row; employees_history holds every superseded version. Each row carries a valid_from and valid_to stamp marking the window during which it was the live row.

Let's follow employee 7, Alice. You write three ordinary statements — one insert and two updates — and the engine records each version behind the scenes. You never write to the history table directly.

After those three statements, the full timeline of Alice's row looks like this. Each version's valid_from / valid_to shows exactly when it was the live row, and the newest one runs to the engine's "max date":

The top two rows live in employees_history ; the bottom row (ending 9999-12-31) is the current row in employees . Notice how each valid_to matches the next row's valid_from — there are no gaps.

You don't have to query the history table by name. Add FOR SYSTEM_TIME ALL to a query on the main table and the engine transparently returns the current row plus every historical version, all with the same columns.

Fill in the blanks to see Alice's row as it stood on 1 February 2024. The expected result is in the comments so you can check yourself.

This is the headline feature. FOR SYSTEM_TIME AS OF '<timestamp>' rewinds the entire table to a single instant and shows the version of each row that was live then. It's how you answer "what did this row look like last Tuesday?"

The query reads from employees as if it were a normal SELECT , but the engine silently swaps in the right historical row. Your application code barely changes — you just bolt FOR SYSTEM_TIME AS OF onto the table.

At exactly 09:00 on 1 April the raise to 60,000 had just taken effect, but the move to Marketing hadn't — so you get the middle version, not today's 65,000.

Beyond a single instant, you can grab every version that was active across a window of time:

Two blanks this time — return the complete change history of employee 7, oldest version first.

Sometimes you care about real-world validity, not storage time — insurance policies, hotel bookings, subscription plans. Here you own the dates, and you query them with a plain WHERE — no FOR SYSTEM_TIME , because the database isn't managing this clock.

Use 9999-12-31 (or 'infinity' on engines that support it) as the valid_to of the current, open-ended period. Then "what applies on date X?" is just valid_from <= X AND valid_to > X .

No — that's the whole point of system -versioning. You keep writing normal INSERT / UPDATE / DELETE , and the engine files away each old version automatically. Manual triggers are only needed on engines without built-in support (MySQL, PostgreSQL).

Q: What's the difference between system-time and valid-time?

System-time (transaction-time) is when the database stored the row and is auto-managed. Valid-time (application-time) is when a fact is true in the real world and you set it yourself. A table tracking both is called bitemporal .

Q: Does keeping history slow my normal queries down?

Plain SELECT s read only the small current table, so they're unaffected. History lives in a separate table that's touched only when you add FOR SYSTEM_TIME . It does use more storage, so index and occasionally archive the history table on busy systems.

Not with this exact syntax — neither has built-in temporal tables yet. On those you simulate it: add valid_from / valid_to columns and a trigger that copies the old row into a history table on every update. SQL Server 2016+, MariaDB 10.3+, Db2, and Oracle support it natively.

Put it all together — a brief, a blank canvas, and the expected results in the comments. Write the two queries, then run them on a SQL Server or MariaDB instance to confirm.

Practice quiz

What does a system-versioned temporal table do automatically?

  • Speeds up all queries
  • Encrypts the data
  • Keeps every previous version of every row in a paired history table
  • Deletes old rows nightly

Answer: Keeps every previous version of every row in a paired history table. The engine keeps each superseded row in a history table, so you can look up any past state on demand.

What does 'system-versioning' mean in this context?

  • The DB engine, not you, manages the versioning
  • You manually write each old version
  • Versions are numbered like migrations
  • Only schema changes are tracked

Answer: The DB engine, not you, manages the versioning. You keep writing normal INSERT/UPDATE/DELETE; the system (engine) files away each superseded row automatically.

What is the difference between transaction-time and valid-time?

  • They are the same
  • Valid-time is auto-managed; transaction-time is manual
  • Both are set by the user
  • Transaction-time is when the DB stored the row (auto); valid-time is when a fact is true in reality (you set it)

Answer: Transaction-time is when the DB stored the row (auto); valid-time is when a fact is true in reality (you set it). Transaction-time (system) is DB-managed storage time; valid-time (application) is when a fact holds in the real world and you set it.

What does FOR SYSTEM_TIME AS OF '<timestamp>' do?

  • Deletes rows before that time
  • Rewinds the table to show each row's version that was live at that instant
  • Creates a backup
  • Shows only future rows

Answer: Rewinds the table to show each row's version that was live at that instant. AS OF rewinds the whole table to a single instant, returning the version of each row that was live then.

What does FOR SYSTEM_TIME ALL return?

  • The current row plus every historical version
  • Only the current row
  • Only history rows
  • An error

Answer: The current row plus every historical version. ALL returns the current row and every historical version, all with the same columns.

How does the engine flag the current (still-active) row in its valid_to column?

  • With NULL
  • With the creation date
  • With the maximum date it supports (e.g. 9999-12-31)
  • With 0

Answer: With the maximum date it supports (e.g. 9999-12-31). The current row's valid_to is set to the engine's max date, so there's no NULL to special-case.

Can you write directly to the valid_from / valid_to columns?

  • Yes, you must set them on every insert
  • No, they are GENERATED ALWAYS and the engine owns them
  • Only on UPDATE
  • Only the primary can

Answer: No, they are GENERATED ALWAYS and the engine owns them. Those columns are GENERATED ALWAYS; trying to INSERT/UPDATE them yourself causes an error.

What is the boundary difference between BETWEEN 'a' AND 'b' and FROM 'a' TO 'b'?

  • No difference
  • FROM..TO is inclusive; BETWEEN is exclusive
  • Both exclude the start
  • BETWEEN is inclusive of both ends; FROM..TO is exclusive of the end instant

Answer: BETWEEN is inclusive of both ends; FROM..TO is exclusive of the end instant. BETWEEN includes both ends; FROM..TO is the same idea but exclusive of the end instant b.

Which engines have built-in system-versioned temporal tables?

  • MySQL and PostgreSQL
  • SQL Server 2016+, MariaDB 10.3+, Db2, Oracle (Flashback)
  • Only SQLite
  • All databases

Answer: SQL Server 2016+, MariaDB 10.3+, Db2, Oracle (Flashback). SQL Server 2016+, MariaDB 10.3+, Db2, and Oracle support it natively; MySQL and PostgreSQL need trigger-based history.

How do you query valid-time (application-time) data?

  • With FOR SYSTEM_TIME AS OF
  • It can't be queried
  • With a plain WHERE on your own date columns (valid_from <= X AND valid_to > X)
  • With ALL

Answer: With a plain WHERE on your own date columns (valid_from <= X AND valid_to > X). Valid-time uses date columns you own, queried with a normal WHERE; the database isn't managing that clock.