Temporal SQL lets a Db2 for z/OS application ask not only “what is true now?” but also “what did the database contain then?” and “when is this fact valid for the business?” Those questions use two different clocks. SYSTEM_TIME tracks database versions, while BUSINESS_TIME tracks application-defined validity. This tutorial builds both models, queries points and ranges, changes temporal data, and explains the integrity and performance decisions that make the results dependable.
You should be comfortable with CREATE TABLE, ALTER TABLE, SELECT, INSERT, UPDATE, DELETE, constraints, and indexes. You also need authority to create or alter the sample objects in a development subsystem. Temporal DDL details and available enhancements depend on Db2 function level and application compatibility, so verify the IBM documentation for the release where the SQL will run.
A system-period temporal table answers what Db2 recorded at an earlier system time. It contains generated row-begin, row-end, and transaction-start-ID columns. The SYSTEM_TIME period is formed from the row-begin and row-end columns. When system-period data versioning is enabled, an associated history table receives previous versions created by updates and deletes.
An application-period temporal table answers when a fact is valid in the domain. For example, a rate might be entered today but valid from next January through March. Its BUSINESS_TIME begin and end values are supplied by the application. Db2 gives those columns formal period semantics, but it cannot decide the correct business dates for you.
A table can define both periods and become bitemporal. Then it can answer a precise question such as, “According to what the database knew on March 10, which premium was considered valid on July 1?” Do not substitute one clock for the other: a load timestamp is not automatically an effective date, and an effective date does not prove when Db2 learned the fact.
12345678910111213141516171819CREATE TABLE HR.EMPLOYEE_TEMPORAL ( EMP_ID INTEGER NOT NULL PRIMARY KEY, DEPT_CODE CHAR(3) NOT NULL, JOB_TITLE VARCHAR(60) NOT NULL, SYS_START TIMESTAMP(12) NOT NULL GENERATED ALWAYS AS ROW BEGIN, SYS_END TIMESTAMP(12) NOT NULL GENERATED ALWAYS AS ROW END, TRANS_START TIMESTAMP(12) GENERATED ALWAYS AS TRANSACTION START ID, PERIOD SYSTEM_TIME (SYS_START, SYS_END) ); CREATE TABLE HR.EMPLOYEE_TEMPORAL_HISTORY LIKE HR.EMPLOYEE_TEMPORAL; ALTER TABLE HR.EMPLOYEE_TEMPORAL ADD VERSIONING USE HISTORY TABLE HR.EMPLOYEE_TEMPORAL_HISTORY;
The application normally omits SYS_START, SYS_END, and TRANS_START from INSERT and UPDATE statements because Db2 generates them. The base and history tables must satisfy IBM's compatibility requirements before versioning can be enabled. A production design also needs table spaces, indexes, privileges, utility plans, and retention rules; the compact DDL here focuses on temporal behavior.
12345678910INSERT INTO HR.EMPLOYEE_TEMPORAL (EMP_ID, DEPT_CODE, JOB_TITLE) VALUES (42, 'A00', 'ANALYST'); UPDATE HR.EMPLOYEE_TEMPORAL SET JOB_TITLE = 'SENIOR ANALYST' WHERE EMP_ID = 42; DELETE FROM HR.EMPLOYEE_TEMPORAL WHERE EMP_ID = 42;
INSERT creates the current version in the base table. UPDATE closes the old version, preserves it in the history table, and leaves the new current version in the base table. DELETE removes the current base row while preserving its final version in history. These effects participate in the same unit of work as the original change; a rollback does not leave a committed temporal history of a change that never committed.
1234SELECT EMP_ID, DEPT_CODE, JOB_TITLE FROM HR.EMPLOYEE_TEMPORAL FOR SYSTEM_TIME AS OF :AS_OF_TS WHERE EMP_ID = :EMP_ID;
AS OF asks for versions whose system period contains one timestamp. Under the usual inclusive-start, exclusive-end interpretation, a version qualifies when SYS_START is less than or equal to the supplied value and SYS_END is greater than it. Query the temporal base-table name rather than manually unioning the base and history tables; Db2 then applies the temporal rules consistently.
1234567SELECT EMP_ID, JOB_TITLE, SYS_START, SYS_END FROM HR.EMPLOYEE_TEMPORAL FOR SYSTEM_TIME FROM TIMESTAMP('2026-01-01-00.00.00.000000000000') TO TIMESTAMP('2026-04-01-00.00.00.000000000000') WHERE EMP_ID = 42 ORDER BY SYS_START;
FROM/TO returns versions that overlap the requested interval. The first boundary is included and the requested upper boundary is excluded. A version can begin before January and still qualify if it remained current during part of the requested window. This is an overlap test, not a requirement that the complete version fit inside it.
12345SELECT EMP_ID, JOB_TITLE, SYS_START, SYS_END FROM HR.EMPLOYEE_TEMPORAL FOR SYSTEM_TIME BETWEEN :START_TS AND :END_TS WHERE EMP_ID = 42 ORDER BY SYS_START;
Temporal BETWEEN also finds overlapping versions, but the upper requested endpoint participates in the overlap test. It is therefore not interchangeable punctuation for FROM/TO. With equal values, BETWEEN behaves like a point-in-time request, while FROM/TO with equal bounds describes an empty interval. Test rows ending or beginning exactly on each boundary.
Product prices are business-valid facts. DATE is suitable when the business rule changes at day boundaries. The following key uses BUSINESS_TIME WITHOUT OVERLAPS so one product cannot have conflicting price periods. Adjacent periods are allowed.
12345678910111213141516CREATE TABLE SALES.PRODUCT_PRICE ( PRODUCT_ID INTEGER NOT NULL, PRICE DECIMAL(11,2) NOT NULL, VALID_FROM DATE NOT NULL, VALID_TO DATE NOT NULL, PERIOD BUSINESS_TIME (VALID_FROM, VALID_TO), PRIMARY KEY (PRODUCT_ID, BUSINESS_TIME WITHOUT OVERLAPS) ); INSERT INTO SALES.PRODUCT_PRICE (PRODUCT_ID, PRICE, VALID_FROM, VALID_TO) VALUES (100, 19.95, DATE('2026-01-01'), DATE('2026-04-01')); INSERT INTO SALES.PRODUCT_PRICE (PRODUCT_ID, PRICE, VALID_FROM, VALID_TO) VALUES (100, 21.50, DATE('2026-04-01'), DATE('2026-07-01'));
The periods meet at April 1 without overlapping. A third row for product 100 covering March 15 through May 1 would conflict with the temporal key. Database enforcement is safer than a “check, then insert” query because two concurrent units of work could both pass an application-only overlap check.
1234567891011121314-- Price valid on one business date SELECT PRODUCT_ID, PRICE FROM SALES.PRODUCT_PRICE FOR BUSINESS_TIME AS OF DATE('2026-05-15') WHERE PRODUCT_ID = 100; -- Price periods overlapping the first half of 2026 SELECT PRODUCT_ID, PRICE, VALID_FROM, VALID_TO FROM SALES.PRODUCT_PRICE FOR BUSINESS_TIME FROM DATE('2026-01-01') TO DATE('2026-07-01') WHERE PRODUCT_ID = 100 ORDER BY VALID_FROM;
FOR BUSINESS_TIME uses the same AS OF, FROM/TO, and BETWEEN shapes as SYSTEM_TIME, but applies them to application-maintained business dates or timestamps. Keep parameter types compatible with the period columns. A character string that merely looks like a date adds conversion risk and can make access-path reasoning harder.
An ordinary UPDATE changes every qualifying row as a whole. An eligible FOR PORTION OF BUSINESS_TIME UPDATE changes only the overlapping business-time portion. Db2 can split one source period into before, changed, and after rows so that values outside the requested portion remain intact.
123456UPDATE SALES.PRODUCT_PRICE FOR PORTION OF BUSINESS_TIME FROM DATE('2026-05-01') TO DATE('2026-06-01') SET PRICE = 22.25 WHERE PRODUCT_ID = 100;
If the existing row covers April 1 through July 1, the logical result can be three adjacent periods: the old price for April, the new price for May, and the old price from June through June 30. The exact generated changes must still satisfy uniqueness, constraints, triggers, and authorization. FOR PORTION OF BUSINESS_TIME can also apply to supported DELETE forms, removing only the selected business-valid portion rather than necessarily deleting the complete source period.
12345DELETE FROM SALES.PRODUCT_PRICE FOR PORTION OF BUSINESS_TIME FROM DATE('2026-05-10') TO DATE('2026-05-20') WHERE PRODUCT_ID = 100;
12345SELECT POLICY_ID, PREMIUM FROM INSURANCE.POLICY_RATE FOR SYSTEM_TIME AS OF :WHAT_DB2_KNEW_AT FOR BUSINESS_TIME AS OF :BUSINESS_VALID_DATE WHERE POLICY_ID = :POLICY_ID;
This query requires a table that defines both periods. The result must satisfy both specifications. Each period name can appear only once on the same table reference. Db2 also has CURRENT TEMPORAL SYSTEM_TIME and CURRENT TEMPORAL BUSINESS_TIME special registers that can apply an implicit AS OF specification when the relevant package bind option is sensitive. They are useful for consistent reporting, but pooled connections must reset them deliberately so one request does not affect the next.
Ordinary referential integrity asks whether a matching parent key exists. Temporal referential integrity adds a coverage question: is the child's BUSINESS_TIME period covered by matching parent rows? Coverage can come from one parent row or from the union of contiguous matching parent periods. This lets a child contract remain valid across adjacent versions of its parent.
12345-- Conceptual shape; verify release-specific DDL requirements CONSTRAINT FK_CONTRACT_CUSTOMER FOREIGN KEY (CUSTOMER_ID, PERIOD BUSINESS_TIME) REFERENCES SALES.CUSTOMER_STATUS (CUSTOMER_ID, PERIOD BUSINESS_TIME)
IBM documents supporting index requirements, including a parent unique index with BUSINESS_TIME WITHOUT OVERLAPS and an appropriate child index that supports period coverage checks. Parent UPDATE and DELETE support also depends on the active function level in Db2 12 environments. Treat temporal RI as a schema feature to design and test, not as a phrase to add after ordinary foreign-key DDL.
System-period history is valuable audit evidence because it can show old values and the system periods during which they were current. It can help answer who or what changed a row when combined with trusted actor, transaction, or audit columns. However, generated period columns alone do not identify the human reason for a change. If that matters, capture approved user identity, source transaction, and reason codes through a governed design.
Temporal history is also not a substitute for recovery. An AS OF query can help locate an earlier row value, but recovering from media failure, object damage, accidental DDL, or broad corruption still requires Db2 logs, image copies, backups, catalog consistency, and tested RECOVER procedures. History retention and recovery retention serve different purposes and should have separate policies.
Imagine a school keeps two calendars. The first is a photo album. Every time a name card changes, the school saves the old photo and writes down when the new photo replaced it. That is SYSTEM_TIME. You can ask, “What did the card look like last Tuesday?”
The second calendar says when a rule is supposed to apply. A lunch price entered today might be marked “good from September until December.” That is BUSINESS_TIME. One calendar remembers when the school knew something; the other says when that thing is meant to be true.
Temporal SQL uses SYSTEM_TIME or BUSINESS_TIME periods to query or change data according to time. SYSTEM_TIME describes when Db2 stored a version, while BUSINESS_TIME describes when the fact is valid in the business domain.
No. Applications normally reference the system-period temporal base table and add FOR SYSTEM_TIME. Db2 evaluates current and associated history data as required. Direct history-table access bypasses that unified temporal interpretation.
AS OF selects versions valid at one instant. FROM value1 TO value2 selects versions that overlap a range whose requested upper endpoint is excluded. BETWEEN value1 AND value2 also selects overlapping versions but includes the requested upper endpoint in the temporal overlap test.
Yes. A bitemporal table has both periods. A query can apply one SYSTEM_TIME specification and one BUSINESS_TIME specification to the same table reference so that a row must satisfy both dimensions.
Authorized direct changes might be possible in specific configurations, but treating history as ordinary application data undermines its audit meaning. Corrections should follow a governed design that preserves who changed what and when, and operational changes should be verified against the deployed Db2 release and controls.
Start from real predicates. A common query that specifies a business key and time range can benefit from an index beginning with that key and then period columns. The base and history tables are separate physical objects, so collect statistics and evaluate access paths for both with EXPLAIN.
1. Which temporal dimension records when a row version was current in Db2?
2. What does FOR SYSTEM_TIME AS OF :TS return?
3. How does FROM :START TO :END differ from BETWEEN :START AND :END?
4. Who normally supplies the begin and end values for BUSINESS_TIME?
5. What is the purpose of FOR PORTION OF BUSINESS_TIME in an UPDATE?
6. Why is a system-period history table not a complete recovery strategy?
Understand generated system periods, versioning, and associated history tables
Model application-defined validity and prevent overlapping business periods
Combine database history with real-world business validity in one design
Enforce parent coverage for child BUSINESS_TIME periods