FREE STUDY NOTES · DP-800

System-versioned temporal tables in SQL Server and Azure SQL

How temporal tables keep row history automatically, and how FOR SYSTEM_TIME AS OF, BETWEEN and ALL query it.

From Ultra Transcenders DP-800 by Tony Rough (coming December 2026)

A system-versioned temporal table keeps the current rows in the table and every previous version in a linked history table, with the period of validity recorded by the engine. It answers “what did this row look like at time T” without application code.

How it works

An UPDATE or DELETE on the current table copies the old row version into the history table, with ValidTo set to the transaction time, and open rows in the current table end at 9999-12-31. A plain SELECT reads only the current table, while FOR SYSTEM_TIME AS OF t reads both and returns the version where ValidFrom <= t AND ValidTo > t.
Figure 2.1: A system-versioned temporal table and its history table
CREATE TABLE dbo.Employee
(
    EmployeeId   int NOT NULL PRIMARY KEY CLUSTERED,
    Department   varchar(100) NOT NULL,
    AnnualSalary decimal(10,2) NOT NULL,
    ValidFrom    datetime2 GENERATED ALWAYS AS ROW START HIDDEN,
    ValidTo      datetime2 GENERATED ALWAYS AS ROW END HIDDEN,
    PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
WITH (SYSTEM_VERSIONING = ON (
        HISTORY_TABLE = dbo.EmployeeHistory,
        HISTORY_RETENTION_PERIOD = 6 MONTHS));

SELECT EmployeeId, AnnualSalary
FROM dbo.Employee FOR SYSTEM_TIME AS OF '2026-01-01T00:00:00'
WHERE EmployeeId = 1000;

Querying with FOR SYSTEM_TIME

Subclause Rows returned
AS OF t ValidFrom <= t AND ValidTo > t (the version current at t)
FROM s TO e ValidFrom < e AND ValidTo > s (excludes both boundaries)
BETWEEN s AND e ValidFrom <= e AND ValidTo > s (includes rows that became active at e)
CONTAINED IN (s, e) ValidFrom >= s AND ValidTo <= e (opened and closed inside the range)
ALL Union of current and history rows

Rows with zero-length validity (several updates to one key in one transaction) are filtered out of temporal queries; query the history table directly to see them. A plain SELECT without FOR SYSTEM_TIME reads only current rows.

Retention

HISTORY_RETENTION_PERIOD accepts DAYS, WEEKS, MONTHS or YEARS and defaults to INFINITE. A background task deletes aged history rows (in chunks of up to 10,000 for a rowstore history table, or whole rowgroups for a clustered columnstore history table). It needs the database flag TEMPORAL_HISTORY_RETENTION (on by default, but turned off automatically after a point-in-time restore) and, on the history table, either a clustered columnstore index or a rowstore clustered index that starts with the period end column. Setting SYSTEM_VERSIONING = OFF discards the retention setting, and turning it back on without HISTORY_RETENTION_PERIOD means infinite retention. The alternatives are a partition sliding window on the history table (switch out with SYSTEM_VERSIONING still on) or a custom cleanup that sets versioning off, deletes and sets it on again.

Limitations

Common trap: Running a nightly DELETE against the history table to enforce retention - direct modification of history is blocked while versioning is on; set HISTORY_RETENTION_PERIOD (and keep TEMPORAL_HISTORY_RETENTION on) or use partition switching.

Common trap: Using FOR SYSTEM_TIME FROM ... TO when rows that started exactly at the end time must be included - FROM ... TO excludes both boundaries; BETWEEN ... AND includes rows that became active at the upper boundary.

Get the whole book

This note is one section of Ultra Transcenders DP-800: Developing AI-Enabled Database Solutions, an independent study guide that explains every topic the exam covers by technology, with comparison tables, diagrams and the common traps, plus a glossary linked to Microsoft Learn.

Amazon.co.ukKindle: coming soonPaperback: coming soon
Amazon.comKindle: coming soonPaperback: coming soon

Due on Amazon in December 2026, in Kindle and paperback editions.

About the book · DP-800 terms in the glossary · All DP-800 study notes

More DP-800 study notes