skip to content

The SQL:2011 standard added system-versioned tables and application-time periods. What do those features give you compared with a hand-rolled history table, and where do they fall short?

level: middleimportance: nice to knowfreq 26%

answer

  1. PERIOD FOR SYSTEM_TIME + WITH SYSTEM VERSIONING
  2. FOR SYSTEM_TIME AS OF / BETWEEN / ALL
  3. application time: PRIMARY KEY … WITHOUT OVERLAPS
  4. UPDATE … FOR PORTION OF splits rows
  5. no changed_by; support is uneven

basics

~20 s

System-versioned tables make the engine timestamp every row version and keep old versions automatically, queryable with FOR SYSTEM_TIME AS OF. Application-time periods let you declare a business validity period the engine can keep non-overlapping and split on update. Support is uneven, and neither records who made the change.

solid answer

~60 s

SQL:2011 defines two independent period concepts. **System-versioned tables** (transaction time): you declare `PERIOD FOR SYSTEM_TIME (row_start, row_end)` and mark the table `WITH SYSTEM VERSIONING`. The engine stamps every row version, moves superseded versions to a history table automatically, and lets you query the past with `FOR SYSTEM_TIME AS OF <ts>` / `BETWEEN` / `FROM…TO` / `ALL`. You get complete, non-bypassable history with no triggers to write. **Application-time periods** (valid time): you declare `PERIOD FOR business_time (valid_from, valid_to)`, can define a primary key `WITHOUT OVERLAPS`, and use `UPDATE/DELETE … FOR PORTION OF business_time FROM … TO …`, which automatically splits the affected rows. That is the effective-dating pattern, enforced by the engine. **Limits.** Support is uneven — SQL Server, MariaDB, DB2 and Oracle offer versions of it; PostgreSQL and MySQL have no system versioning. System time records when the database was *told*, not when the fact was true. There is no `changed_by`, so if you need identity you still add it yourself. And history growth, retention and schema evolution remain your problem.

code

sql · 11 lines
sql
CREATE TABLE employee (
  id        INT PRIMARY KEY,
  salary    NUMERIC(12,2),
  row_start TIMESTAMP(6) GENERATED ALWAYS AS ROW START,
  row_end   TIMESTAMP(6) GENERATED ALWAYS AS ROW END,
  PERIOD FOR SYSTEM_TIME (row_start, row_end)
) WITH SYSTEM VERSIONING;

SELECT id, salary
  FROM employee
  FOR SYSTEM_TIME AS OF TIMESTAMP '2024-03-01 00:00:00';

go deeper

for a junior

Know that the standard lets the database keep old row versions itself and query them with FOR SYSTEM_TIME AS OF, instead of you writing triggers.

for a middle

Distinguish system time from application time, mention WITHOUT OVERLAPS and FOR PORTION OF, and be honest that engine support varies.

for a senior

Compare it with trigger-based history on completeness and cost, and name the operational gaps: actor identity, retention, schema evolution, and erasure of protected history rows.

for a principal

Decide whether engine-native versioning is worth the portability constraint, how it interacts with replication, retention and privacy obligations, and where a bitemporal model is actually required.

## Two periods, two meanings SQL:2011 formalised what temporal-database research had long distinguished: - **System time (transaction time)** — the interval during which the database *held* this row version. Set by the engine, never by the application, and by definition never in the future. - **Application time (valid time)** — the interval during which the fact is *true in the business*. Set by the application; may be in the past (a backdated correction) or the future (a scheduled price change). The standard supports both, independently. A table with only system time gives you an automatic audit of database state; a table with only application time gives you effective dating; a table with both is *bitemporal*. ## System-versioned tables You declare two columns generated by the engine and mark the table versioned. In standard terms: ``` CREATE TABLE employee ( id INT PRIMARY KEY, salary NUMERIC(12,2), row_start TIMESTAMP(6) GENERATED ALWAYS AS ROW START, row_end TIMESTAMP(6) GENERATED ALWAYS AS ROW END, PERIOD FOR SYSTEM_TIME (row_start, row_end) ) WITH SYSTEM VERSIONING; ``` From then on, `UPDATE` and `DELETE` do not destroy anything: the engine closes the current version's `row_end` and retains it (in a separate history table in SQL Server and DB2, in the same table in MariaDB), inserting the new version with a fresh `row_start`. Ordinary queries see only current rows, so existing application SQL is unaffected. Time travel uses period predicates: ``` SELECT * FROM employee FOR SYSTEM_TIME AS OF TIMESTAMP '2024-03-01 00:00:00'; SELECT * FROM employee FOR SYSTEM_TIME BETWEEN :a AND :b; SELECT * FROM employee FOR SYSTEM_TIME ALL; ``` What this buys over a hand-rolled trigger-based history table: - **Completeness.** No writer can bypass it — not a migration script, not a DBA fixing data by hand, not a bulk load. Triggers can be disabled and application code can be bypassed; system versioning is part of the table. - **Correctness.** Versions are stamped with the transaction's timestamp and are transactionally consistent by construction, with no race between the change and the record of it. - **Ergonomics.** `AS OF` is dramatically simpler than the equivalent hand-written correlated query, and the optimizer understands it. - **Less code.** No triggers, no history DDL to maintain by hand, no test surface for "did we remember to audit this column". ## Application-time periods The second half of the standard applies to business validity: ``` CREATE TABLE product_price ( product_id INT, price NUMERIC(12,2), valid_from DATE, valid_to DATE, PERIOD FOR business_time (valid_from, valid_to), PRIMARY KEY (product_id, business_time WITHOUT OVERLAPS) ); UPDATE product_price FOR PORTION OF business_time FROM DATE '2024-03-01' TO DATE '2024-04-01' SET price = 21.00 WHERE product_id = 42; ``` Two things are notable. `WITHOUT OVERLAPS` makes the engine reject overlapping periods for the same entity — the constraint that hand-rolled effective dating most often lacks. And `FOR PORTION OF` performs the row-splitting that application code normally has to implement: if the existing period is wider than the portion being changed, the engine automatically closes and re-opens the surrounding fragments. Periods here are half-open, matching the convention you would choose anyway. ## Where it falls short **Availability.** This is the big one. SQL Server (2016+, "temporal tables"), MariaDB (10.3+), DB2 and Oracle (Flashback Data Archive plus valid-time support) implement meaningful subsets. PostgreSQL has no system versioning — the usual answer there is an extension or a trigger-based history table, with application-time period features arriving only gradually in recent releases. MySQL has neither. So the honest interview answer names the standard, then names what your engine actually does. **No actor.** System versioning records *when*, never *who*. Compliance auditing almost always needs the application user, so you end up adding a `changed_by` column that the application must populate — which re-introduces the very bypass problem system versioning solved, since a direct SQL update will leave it stale or null. **System time is not truth time.** A salary entered on the 10th, effective the 1st, appears in system time as a change on the 10th. Answering "what was the salary on the 1st" correctly needs application time. Conversely, application time alone cannot tell you what a report run last month would have said. That is the argument for bitemporal design. **Operational weight.** History still grows without bound and still needs retention, partitioning and archival — most implementations give you tools for this, but it is not automatic. Schema changes to a versioned table need care, since the history must accommodate old and new shapes. Some implementations restrict DDL, `TRUNCATE`, partitioning or replication interactions on versioned tables. And history rows are usually protected from modification, which is a feature for auditors and an obstacle when you must genuinely erase personal data. **Granularity.** Versioning is per row, not per column, so a change to one column rewrites the whole row into history — the same storage profile as a snapshot-style history table. ## How to use this in an answer Say that the standard separates system time from application time, that system versioning gives complete engine-enforced history that no writer can bypass, that application-time periods give non-overlapping effective dating plus automatic row splitting, and then be specific about your engine's support and about the two gaps that always remain: identity of the actor, and history retention.

  • What does a system-versioned table still not tell you that a hand-rolled audit table usually does?
    Who made the change, and why. System versioning stamps time only, because the engine knows the session but not your application's user. Teams add a changed_by column the application must set, which reopens the bypass problem — a direct SQL update stamps the version correctly but leaves the actor column wrong. Request ids, reasons and approval references have the same issue.
  • If a salary change is entered on 10 March but takes effect from 1 March, what does FOR SYSTEM_TIME AS OF '2024-03-05' return?
    It returns the row as the database held it on 5 March, which is the old salary — system time reflects when the database was told, not when the fact became true. To answer "what was the salary on 5 March, as best we now know", you need an application-time (valid-time) period. Needing both questions answered is exactly the case for a bitemporal design.

saying these in an interview costs you the question

  • Assuming every mainstream engine supports system-versioned tables, notably claiming PostgreSQL or MySQL do
  • Treating system time as the effective date of the business fact
  • Expecting the feature to record the application user
  • Believing history is maintenance-free — retention, growth and schema evolution still apply
  • Thinking versioning is per column rather than per row

context