Using SQL Server Temporal Tables to Track Row History Without Custom Code
Learn how SQL Server temporal tables automatically keep a full history of row changes, letting you query past states with simple FOR SYSTEM_TIME clauses—no triggers required.
12 Sept 2026, 10:20 UTC

Problem: Needing a reliable audit trail without writing triggers
Many applications require a full history of data changes for compliance, debugging, or analytics. Building that history with triggers or application‑level code adds complexity, introduces bugs, and makes schema evolution harder.
Thesis: SQL Server system‑versioned temporal tables give you automatic, query‑friendly history with minimal schema changes
By enabling SYSTEM_VERSIONING = ON on a table that has a primary key, SQL Server silently creates a history table and two hidden datetime columns that mark the period each row was valid. You can then read current data or any point‑in‑time state using the FOR SYSTEM_TIME clause.
How to set up a temporal table
- Permissions: You need
CREATE TABLEandALTERrights on the target schema, plusREFERENCESon the primary key column if it references another table. - Definition: Create the table with the required hidden columns and a period.
-- Run in the database where you want the table
CREATE TABLE dbo.Employee (
EmployeeId INT PRIMARY KEY,
FirstName NVARCHAR(50) NOT NULL,
LastName NVARCHAR(50) NOT NULL,
Department NVARCHAR(100),
-- Hidden system columns
SysStartTime DATETIME2 GENERATED ALWAYS AS ROW START HIDDEN,
SysEndTime DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN,
PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime)
)
WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.EmployeeHistory));
SQL Server creates dbo.EmployeeHistory with the same schema plus the two hidden columns. No triggers are needed.
Worked example: inserting and updating rows
- Insert a row (run as a user with
INSERTpermission):INSERT INTO dbo.Employee (EmployeeId, FirstName, LastName, Department) VALUES (1, 'Ada', 'Lovelace', 'Research'); - Update the row twice (each update generates a new history row):
UPDATE dbo.Employee SET Department = 'Development' WHERE EmployeeId = 1; UPDATE dbo.Employee SET LastName = 'King' WHERE EmployeeId = 1; - Query the full history (any user with
SELECTon the table can run this):SELECT EmployeeId, FirstName, LastName, Department, SysStartTime, SysEndTime FROM dbo.Employee FOR SYSTEM_TIME ALL ORDER BY SysStartTime;You should see three rows: the original insert, the first update, and the second update, each with a non‑overlapping
SysStartTime–SysEndTimeinterval that shows when that version was valid.
Reading a specific point in time
To see what the employee record looked like after the first update but before the second, use AS OF:
SELECT *
FROM dbo.Employee
FOR SYSTEM_TIME AS OF '2026-10-18 10:15:00';
Replace the timestamp with a value that falls between the two update times.
Trade‑offs and limitations
- Storage overhead: Every change creates a new row in the history table. Monitor the size of
dbo.EmployeeHistoryand consider partitioning it bySysStartTimeif retention grows large. - Primary key constraints: The primary key cannot be nullable and should not consist of columns that change frequently. If the key changes, SQL Server must delete the old history row and insert a new one, which can cause blocking.
- Edition support: Temporal tables are available in Enterprise, Standard, Developer, and Azure SQL Database, but not in SQL Server Express.
Practical verification steps
- Confirm the hidden columns exist:
SELECT name, is_hidden FROM sys.columns WHERE object_id = OBJECT_ID('dbo.Employee');You should seeSysStartTimeandSysEndTimemarked as hidden. - Check that system versioning is on:
SELECT is_system_versioned FROM sys.tables WHERE name = 'Employee';returns 1. - After performing inserts/updates, run the
FOR SYSTEM_TIME ALLquery and verify the number of rows matches the expected count (insert + number of updates).
Actionable closing
If you need an audit trail and you are on a supported edition of SQL Server, enable system versioning on your core tables. Start with a low‑traffic table, verify the history rows appear as expected, and then extend the pattern to other tables. Keep an eye on history table growth and set up a retention policy (e.g., periodic cleanup or partitioning) to manage storage costs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.