automatic history under the SQL:2011 standard
Anyone who needs to reconstruct the state of a row at an arbitrary point in the past classically builds a custom audit trail table with triggers or application code. The SQL:2011 standard defines a far more robust alternative: system-versioned tables, where the database itself automatically maintains a complete history with valid time periods on every UPDATE and DELETE, with no extra application code required. SQL Server and MariaDB already implement this concept in a production-ready way, with dedicated syntax for time-travel queries. This article shows how the mechanism actually works, how it differs from hand-built audit trails, and where its limits lie.
Table of Contents
- 1. How System-Versioning Automatically Maintains a History Table
- 2. How This Differs From Hand-Built Audit Trail Tables
- 3. System-Versioned Temporal Tables in SQL Server in Detail
- 4. System-Versioned Tables in MariaDB
- 5. Querying Historical Data With FOR SYSTEM_TIME
- 6. Availability Gaps: Which Databases Support System-Versioned Tables
- 7. Storage Growth and Performance Implications of the History Table
- 8. What System-Versioning Deliberately Does Not Cover
- 9. Practical Recommendation: When System-Versioning Pays Off
- 10. Summary
- 11. FAQ
1. How System-Versioning Automatically Maintains a History Table
A system-versioned table gets two additional, database-managed timestamp columns per row: a start and an end point for validity as seen by the system. When an application issues an UPDATE or DELETE, the engine automatically writes the previous version of the row, with its validity period closed off, into a history table within the same transaction, before the base table receives the new version. From the application's point of view, the actual SQL statement itself does not change at all.
The key difference from ordinary timestamp columns such as updated_at lies in completeness: there is no code path through which a change can bypass the history, because the logic sits directly in the database's storage layer. Even a bulk update touching a thousand rows reliably produces a thousand history entries, with no trigger that anyone needs to maintain by hand.
-- SQL Server: creating a system-versioned table
CREATE TABLE dbo.contract (
contract_id INT PRIMARY KEY,
customer_id INT NOT NULL,
status VARCHAR(20) NOT NULL,
valid_from DATETIME2 GENERATED ALWAYS AS ROW START,
valid_to DATETIME2 GENERATED ALWAYS AS ROW END,
PERIOD FOR SYSTEM_TIME (valid_from, valid_to)
)
WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.contract_history));
2. How This Differs From Hand-Built Audit Trail Tables
A classic audit trail typically comes from a trigger that copies the old row into a separate log table on UPDATE and DELETE, or from application code that handles this in the ORM layer. Both approaches work as long as every write path disciplined runs through the same layer. As soon as someone issues a direct SQL statement against the database, runs a migration without ORM hooks, or a trigger gets accidentally disabled, silent gaps appear in the history, often noticed only once the missing data is actually needed.
System-versioning eliminates exactly this class of bug, because history maintenance is not tied to application logic but to the act of writing the row itself. The price is less flexibility: anyone who wants to record extra metadata such as who made a change still has to add their own columns and logic, because plain system-versioning only tracks the time of a change, not its author.
3. System-Versioned Temporal Tables in SQL Server in Detail
SQL Server has supported system-versioned temporal tables since version 2016. The two generated columns for the start and end of validity are declared through PERIOD FOR SYSTEM_TIME, and the SYSTEM_VERSIONING = ON option links the base table to a dedicated history table. That history table can be named explicitly, as in the example above, or generated automatically by SQL Server, complete with a matching clustered columnstore index for efficient compression of large history volumes.
The HISTORY_RETENTION_PERIOD option additionally controls how long history entries are kept before SQL Server automatically removes them through a background job. That matters especially for tables with a high change frequency, since without such a limit the history table grows without bound and can outgrow the actual working table many times over within a few months.
4. System-Versioned Tables in MariaDB
MariaDB has implemented system-versioning since version 10.3 through the WITH SYSTEM VERSIONING clause on the CREATE TABLE statement itself. Unlike SQL Server, MariaDB does not create a separate history table by default; instead it keeps old and current rows in the same table, distinguished by internally managed row_start and row_end system columns. Queries without an explicit time reference automatically only see the currently valid rows, as if the history did not even exist.
For managing storage growth, MariaDB offers system-versioning partitions that move historical rows into separate partitions by age and let them be discarded automatically via PARTITION ... HISTORY. That is functionally equivalent to SQL Server's retention period, just implemented through the established partitioning mechanism instead of a dedicated configuration option.
-- MariaDB: creating a system-versioned table
CREATE TABLE contract (
contract_id INT PRIMARY KEY,
customer_id INT NOT NULL,
status VARCHAR(20) NOT NULL
) WITH SYSTEM VERSIONING;
-- Updating a row, MariaDB writes the old version forward automatically
UPDATE contract SET status = 'terminated' WHERE contract_id = 42;
5. Querying Historical Data With FOR SYSTEM_TIME
Both systems use the same standardized syntax for time-travel queries: FOR SYSTEM_TIME AS OF returns the state at an exact point in time, FOR SYSTEM_TIME BETWEEN and FOR SYSTEM_TIME FROM TO return all versions within an interval with slightly different boundary handling, and FOR SYSTEM_TIME ALL returns the entire history of a row including every intermediate version.
In practice this means: instead of writing a custom reconstruction query with several JOINs against a history table, a single time reference in the FROM clause is enough, and the database automatically picks the correct version, even across table boundaries when joining several system-versioned tables together.
-- Reconstruct the contract state as of 2026-03-01, 12:00
SELECT contract_id, status
FROM contract
FOR SYSTEM_TIME AS OF '2026-03-01 12:00:00'
WHERE contract_id = 42;
-- List every status change of a contract during Q1
SELECT contract_id, status, valid_from, valid_to
FROM contract
FOR SYSTEM_TIME BETWEEN '2026-01-01' AND '2026-03-31'
WHERE contract_id = 42
ORDER BY valid_from;
6. Availability Gaps: Which Databases Support System-Versioned Tables
PostgreSQL still has no native implementation of system-versioning under SQL:2011 as of today. Anyone who needs this functionality there either reaches for the temporal_tables extension, which recreates a similar history table through triggers, or builds the solution by hand using range types and exclusion constraints. Plain MySQL, without the MariaDB fork, also offers no built-in support.
IBM DB2 was historically among the first systems with full SQL:2011 support and covers both system-period and application-period tables. Oracle offers Flashback Data Archive, a functionally similar but syntactically distinct concept that also maintains history automatically, though it is not queried through the standardized FOR SYSTEM_TIME syntax but through its own AS OF clauses.
7. Storage Growth and Performance Implications of the History Table
Every change to a system-versioned row produces an additional write into the history, on top of the actual UPDATE. For tables with a very high change frequency, such as status transitions in queues or frequently updated counters, this doubled write cost can noticeably affect individual transaction latency and should be measured under realistic load before going live.
For read access to the history, a dedicated index on the period columns pays off, since otherwise every time-travel query triggers a full table scan of the history table. Combined with a clear retention strategy, whether through HISTORY_RETENTION_PERIOD in SQL Server or history partitions in MariaDB, the history table stays manageable even with millions of changes per year.
8. What System-Versioning Deliberately Does Not Cover
System-versioning only knows a single time axis: when a row was valid from the database's point of view. When a fact was actually valid from a business perspective is not covered by the mechanism at all. For a retroactively corrected invoice, for example, system-versioning does not distinguish between the moment the correction was made in the system and the moment from which the corrected figure was supposed to apply from a business standpoint. Anyone who needs that distinction requires a bitemporal model on top.
Schema changes on the base table also deserve special attention: a new column in SQL Server generally has to be replicated to the history table as well, and while versioning is active certain DDL operations are restricted or require temporarily disabling SYSTEM_VERSIONING, which breaks the history chain at that point and should be documented accordingly.
9. Practical Recommendation: When System-Versioning Pays Off
System-versioning pays off above all where gapless traceability is legally required, for example for contract, price, or status changes in regulated industries, or where developers regularly need to reconstruct what a particular record looked like before a faulty deployment. In those cases the technique replaces an entire category of self-written, error-prone trigger code with a proven database feature.
It makes less sense for tables with extremely high write load and no real audit requirement, since the extra write cost there is pure overhead, and anywhere true bitemporality is needed instead of plain system history. For those requirements, a deliberately bitemporal model with separate time axes for validity and recording is the more appropriate solution.
| Feature | SQL Server | MariaDB | Manual Audit Trail |
|---|---|---|---|
| History table | Separate, named object | Usually the same table with a visibility window | Has to be modeled by hand |
| Consistency on bulk updates | Guaranteed by the engine | Guaranteed by the engine | Depends on trigger coverage |
| Time-travel query | FOR SYSTEM_TIME |
FOR SYSTEM_TIME |
Requires custom JOIN logic |
| Tracking who made a change | Not included | Not included | Freely modelable |
| Available since | SQL Server 2016 | MariaDB 10.3 | Database independent, any time |
Mironsoft
Database optimization, query tuning, and migrations
SQL queries that keep getting slower as the data grows?
We analyze and optimize SQL databases regardless of the system in use, plan safe migrations and schema changes, and teach teams query optimization hands-on.
Query Optimization
Analyze slow queries and speed them up with purpose using indexes and explain plans.
Migration Planning
Execute schema changes and data migrations safely, without downtime.
Team Training
Anchor SQL fundamentals and performance thinking hands-on in the dev team.
10. Summary
System-Versioned Tables at a Glance
Automation
The database maintains history rows itself, without triggers or custom application code.
SQL Server
PERIOD FOR SYSTEM_TIME plus a separate history table, production ready since version 2016.
MariaDB
WITH SYSTEM VERSIONING since version 10.3, history usually kept in the same table.
Limit
Only one time axis: true bitemporality still requires an additional model.