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.
datetime2 columns declared GENERATED ALWAYS AS ROW START and ROW END plus PERIOD FOR SYSTEM_TIME. Values are the UTC begin time of the transaction; open rows end at 9999-12-31. They can be HIDDEN from SELECT *.ValidTo set to the transaction time. A history row is written even if no column value changed.PAGE compressed by default. If you name the history table, give schema and name. Figure 2.1 shows how row versions move to the history table and how AS OF reads them.
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;| 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.
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.
TRUNCATE TABLE isn’t allowed while versioning is on, and the history table can’t be modified directly.INSTEAD OF triggers aren’t allowed on either table; AFTER triggers only on the current table.INSERT and UPDATE can’t reference the period columns.Common trap: Running a nightly
DELETEagainst the history table to enforce retention - direct modification of history is blocked while versioning is on; setHISTORY_RETENTION_PERIOD(and keepTEMPORAL_HISTORY_RETENTIONon) or use partition switching.
Common trap: Using
FOR SYSTEM_TIME FROM ... TOwhen rows that started exactly at the end time must be included -FROM ... TOexcludes both boundaries;BETWEEN ... ANDincludes rows that became active at the upper boundary.
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.
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
How each isolation level trades consistency for concurrency, when to use RCSI or snapshot isolation, and which anomalies each one prevents.
How Always Encrypted keeps keys away from the database engine, deterministic versus randomised encryption, and secure enclaves.
How to build row-level security with an inline predicate function and a security policy, and how filter and block predicates differ.
How the OVER clause partitions, orders and frames rows, and how ROWS and RANGE frames decide which rows each calculation sees.
When exact k-nearest-neighbour search is enough, when an approximate DiskANN vector index pays off, and why its metric must match the query.
How to combine full-text and vector search results with reciprocal rank fusion in T-SQL, and why RRF uses ranks rather than raw scores.
Choosing between change event streaming, change data capture, change tracking, Azure Functions and Logic Apps to react to row changes.