> Modeling when facts are true (valid-time), when they were recorded (transaction-time), or both (bitemporal), enabling time-travel queries and regulatory audit.
Scanned 9/11/2026
Install to Claude Code
npx -y skills add Intense-Visions/harness-engineering --skill db-temporal-data --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Db Temporal Data?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/intense-visions-db-temporal-data-231eb8d7)More formats (shields.io, HTML) on the badges page.
# Temporal Data
> Modeling when facts are true (valid-time), when they were recorded (transaction-time), or both (bitemporal), enabling time-travel queries and regulatory audit.
## When to Use
- Insurance policies with effective dates that change over time
- Price histories where "what was the price on date X?" is a query requirement
- Employee role and salary changes tracked for HR reporting
- Regulatory requirements to reconstruct past states (financial compliance, audit)
- Any domain where historical accuracy matters and overwrites destroy information
## Instructions
### Key Concepts
Three temporal dimensions:
**1. Valid-time (application time):** When the fact is true in the real world. An employee salary might be effective 2024-01-01 to 2024-12-31. The database might record this fact on 2023-12-15 -- the valid time and the recording time differ.
**2. Transaction-time (system time):** When the row was stored in the database. Columns like `recorded_at` and `superseded_at` track the database's knowledge. Transaction-time columns are never manually edited -- the system manages them.
**3. Bitemporal:** Both dimensions combined. Answers questions like "what did the system believe was true about time T, as of database time S?" Essential for late-arriving corrections in financial systems.
**Slowly Changing Dimensions (SCD) types:**
| Type | Strategy | History? |
| ------ | ------------------------ | -------- |
| Type 1 | Overwrite the row | No |
| Type 2 | New row with date range | Yes |
| Type 3 | Previous/current columns | Partial |
| Type 6 | Hybrid (Type 1 + 2 + 3) | Yes |
SCD Type 2 is the most common for temporal data. In PostgreSQL, use range types:
```sql
CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE employee_salaries (
id serial PRIMARY KEY,
employee_id int NOT NULL,
salary numeric(10,2) NOT NULL,
valid_range tstzrange NOT NULL,
EXCLUDE USING gist (
employee_id WITH =,
valid_range WITH &&
)
);
```
The **exclusion constraint** prevents overlapping valid periods for the same employee -- the database enforces temporal integrity, not the application.
### Worked Example
Insurance policy pricing with valid-time tracking:
```sql
-- Initial price: $500/month effective Jan 1 to Dec 31
INSERT INTO policy_prices (policy_id, monthly_rate, valid_range)
VALUES (101, 500.00, '[2024-01-01, 2025-01-01)');
-- Price change mid-year: close the old range, insert new
UPDATE policy_prices
SET valid_range = '[2024-01-01, 2024-07-01)'
WHERE policy_id = 101 AND valid_range @> '2024-06-15'::timestamptz;
INSERT INTO policy_prices (policy_id, monthly_rate, valid_range)
VALUES (101, 550.00, '[2024-07-01, 2025-01-01)');
```
**Point-in-time query -- "what was the price on June 15?":**
```sql
SELECT monthly_rate FROM policy_prices
WHERE policy_id = 101
AND valid_range @> '2024-06-15'::timestamptz;
-- Returns: 500.00
```
**Bitemporal query -- "what did we think the price was on June 15, before the July correction?":**
```sql
SELECT monthly_rate FROM policy_prices
WHERE policy_id = 101
AND valid_range @> '2024-06-15'::timestamptz
AND recorded_at < '2024-07-01'::timestamptz;
```
This is essential when auditors need to reconstruct what the system reported at a specific point in database history.
### Anti-Patterns
1. **Using `updated_at` as a substitute for temporal modeling.** `updated_at` only records the last change -- all history is lost. It cannot answer "what was true at time T?"
2. **Overlapping valid periods without exclusion constraints.** Two rows claiming the same employee had different salaries on the same date is corrupt data. Use exclusion constraints.
3. **Managing two timestamp columns manually instead of range types.** Separate `valid_from` and `valid_to` columns require application-level overlap checks. Range types with exclusion constraints push this to the database.
4. **Using SCD Type 1 (overwrite) when audit trail is required.** Overwriting destroys evidence. Use Type 2 or a separate audit table.
5. **NULL for "current" end date.** Use range type upper-bound infinity instead: `'[2024-07-01,)'` -- the open upper bound means "still current" and works correctly with range operators.
### PostgreSQL Specifics
**Range types** (`tstzrange`, `daterange`) provide built-in temporal operators:
- `@>` -- contains a timestamp: `valid_range @> '2024-06-15'::timestamptz`
- `&&` -- overlaps another range: `range1 && range2`
- `-|-` -- adjacent ranges: `range1 -|- range2`
**GiST index** for range columns:
```sql
CREATE INDEX idx_prices_valid ON policy_prices USING gist (valid_range);
```
**Exclusion constraints** require the `btree_gist` extension:
```sql
CREATE EXTENSION btree_gist;
-- Prevents overlapping valid periods for the same policy
EXCLUDE USING gist (policy_id WITH =, valid_range WITH &&)
```
**SQL:2011 temporal support** is partially implemented in PostgreSQL. System-versioned tables are not yet native -- use triggers or the `temporal_tables` extension to approximate transaction-time tracking.
## Details
### Advanced Topics
**SQL:2011 standard temporal syntax** (`FOR SYSTEM_TIME AS OF`) is supported in MariaDB and SQL Server but not yet in PostgreSQL. PostgreSQL approximates this with view-based patterns or the `temporal_tables` extension.
**Gap detection** in temporal data -- finding periods with no valid record:
```sql
SELECT upper(a.valid_range) AS gap_start, lower(b.valid_range) AS gap_end
FROM policy_prices a
JOIN policy_prices b ON a.policy_id = b.policy_id
AND upper(a.valid_range) < lower(b.valid_range)
WHERE NOT EXISTS (
SELECT 1 FROM policy_prices c
WHERE c.policy_id = a.policy_id
AND c.valid_range && tstzrange(upper(a.valid_range), lower(b.valid_range))
);
```
**Temporal joins** for combining two temporal tables:
```sql
SELECT e.name, s.salary, d.department_name
FROM employees e
JOIN employee_salaries s ON e.id = s.employee_id
JOIN employee_departments d ON e.id = d.employee_id
AND s.valid_range && d.valid_range;
```
**Partitioning temporal tables** by `valid_from` enables archival of old time periods and efficient pruning of historical queries.
### Engine Differences
MySQL lacks range types and exclusion constraints entirely. Temporal data in MySQL requires:
- Separate `valid_from DATETIME` and `valid_to DATETIME` columns
- Application-level overlap prevention (no database-enforced exclusion)
- Manual NULL handling for "current" records (no range infinity)
MySQL 8.0 does not support SQL:2011 temporal tables.
**MariaDB 10.3+** is the notable exception -- it supports system-versioned tables (`WITH SYSTEM VERSIONING`), making it the only major open-source engine with native transaction-time temporal support. MariaDB's temporal tables automatically track row history without triggers.
### Real-World Case Studies
**Financial compliance system tracking 10M account balances.** Regulators require reconstructing the account balance at any historical date. SCD Type 2 implementation using `tstzrange` with GiST index on `(account_id, valid_range)`. Point-in-time queries ("what was balance on date X?") complete in 3ms. After an audit discovered late-arriving corrections (a deposit backdated to last month), the team added bitemporal columns to track both valid-time and transaction-time. This enabled the query "what did we report the balance was on March 15, before the April 2 correction?" -- critical for regulatory reconciliation.
## Source
- [PostgreSQL Range Types](https://www.postgresql.org/docs/current/rangetypes.html)
- [Snodgrass -- Developing Time-Oriented Database Applications](https://www2.cs.arizona.edu/~rts/tdbbook.pdf)
## Process
1. Read the key concepts to understand the three temporal dimensions and SCD types.
2. Apply the correct temporal strategy based on whether you need valid-time, transaction-time, or bitemporal tracking.
3. Verify that range types with exclusion constraints prevent overlapping periods and that point-in-time queries return correct results.
## Harness Integration
- **Type:** knowledge -- this skill is a reference document, not a procedural workflow.
- **No tools or state** -- consumed as context by other skills and agents.
- **related_skills:** db-audit-trail, db-time-series, db-denormalization, microservices-event-sourcing
## Success Criteria
- Temporal tables use range types with exclusion constraints (not manual timestamp pairs).
- SCD type is chosen based on business requirements (Type 2 for full history, Type 1 only when history is not needed).
- Point-in-time queries are verified to return correct results.
- GiST indexes are present on range columns.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!