Effective dating, bitemporal models, snapshots, event sourcing, and SQL:2011 temporal tables — ask what was true, what we believed, and query the past natively.
⏱ 17 min readTopics chapter readerLevel · Dimensional & Warehouse
01 · Overview
Start here
⏱ 5 min · Topic 1 of 16
In Slowly Changing Dimensions (SCD), Asha moved from IN to DE on 2026-01-08. Type 2 kept both versions, each with valid_from and valid_to.
This chapter asks a harder question. What if the warehouse learns about the move four days late?
Then one pair of dates is not enough. You need a second pair. Senior interviewers ask about exactly this.
What you will learn
Valid time and recorded time, the two kinds of time.
Effective dating, and how an as-of join finds the right row.
Bitemporal rows, and how to ask “as known” and “as true”.
Changes against corrections, and how to restate a report.
Snapshots, event tables, and rows that arrive late.
Time travel in engines, and how to choose a design.
Three common mistakes.
02 · The idea
Two kinds of time
⏱ 6 min · Topic 2 of 16
Valid time is when a fact is true in the real world. Recorded time is when the warehouse learns the fact.
Take Asha’s move. She moved on 2026-01-08. The support team told the warehouse on 2026-01-12.
Asha’s move, with two dates
Kind of time
Date
What it means
Valid time
2026-01-08
She lives in DE from this day.
Recorded time
2026-01-12
The warehouse learns about it on this day.
For four days, the warehouse says IN while Asha lives in DE. Order 102 is dated 2026-01-10, inside those four days.
Two different questions need two different dates:
What was true on 2026-01-10? This uses valid time.
What did we believe on 2026-01-10? This uses recorded time.
Other books call recorded time “transaction time” or “system time”. Valid time is also called application time or business time. The ideas are the same.
Watch out
A column called updated_at shows recorded time only. It says nothing about when the change was true.
03 · Valid time
Effective dating and as-of joins
⏱ 8 min · Topic 3 of 16
Effective dating stores valid time on each row. You know it from Type 2. Here are Asha’s two rows again:
dim_customer, Asha’s rows (2 rows)
customer_key
customer_id
country
valid_from
valid_to
is_current
1001
1
IN
2026-01-01
2026-01-08
false
1006
1
DE
2026-01-08
9999-12-31
true
Each row is true from valid_from up to, but not including, valid_to. We write this as [valid_from, valid_to).
The square bracket includes the start day. The round bracket leaves out the end day.
Why leave out the end day? Test the day of the move, 2026-01-08. First use BETWEEN, which includes both ends:
Row 1001 was closed on 2026-01-12, so it fails the recorded test. Row 1006 passes both tests.
Both answers are correct. They answer different questions.
The first is the old report. The second is the truth we found later.
06 · Change or correction
Changes and corrections
⏱ 5 min · Topic 6 of 16
A change means the world changed. A correction means our data was wrong or late. They need different edits.
Change or correction?
What happened
Kind
What you store
Asha moves on 2026-01-08, and we hear the same day.
Change
In Type 2, close the IN row at 2026-01-08 and add a DE row. In a bitemporal table, do the three steps from Bitemporal rows, with recorded date 2026-01-08.
On 2026-01-12 we learn the move happened on 2026-01-08.
Correction
In Type 2, you make the same edit as for a change. In a bitemporal table, keep the old row’s country and valid dates and set its recorded_to = 2026-01-12. Then add new rows with the right valid dates.
A pending order becomes paid, and we hear the same day.
Change
Add a new event or version. Do not overwrite the pending row.
Someone typed a wrong amount, and we find it later.
Correction
In a bitemporal table, close the wrong row with recorded_to and add the right row. In an event table, add a new event that fixes it.
A change starts on the day you hear about it. A correction fixes the past: its valid date is earlier than its recorded date.
In a bitemporal table, both set recorded_to on the old row. Only a correction changes what we believed about days before the recorded date.
The late move is a correction. Its valid date is 2026-01-08, and its recorded date is 2026-01-12.
07 · Reports
Restate a report
⏱ 6 min · Topic 7 of 16
To restate a report means to publish new numbers for a past period. The 2026-01-10 report counted Asha’s paid orders under IN. We now know order 102 belongs to DE.
The query below runs the as-of join twice. WITH makes a small table, report_dates, with two “known on” dates. We assume both orders were loaded by 2026-01-10.
Paid revenue for Asha, as known on two datesworked example
On 2026-01-10 both orders use row 1001, so IN has 40.00 + 25.00 = 65.00. On 2026-01-31, order 101 uses row 1008 and order 102 uses row 1006.
You can still rebuild the old report. It is the first row.
The new report moves 25.00 from IN to DE. The total is 65.00 both times.
Publish the new numbers with their known date, and keep the old report. A reader can then see why the number moved.
Remember
Type 1 and Type 2 both lose the 2026-01-10 report. After the fix, Type 2 says order 102 is DE. Only recorded dates can rebuild IN 65.00.
08 · Snapshots
Snapshots
⏱ 6 min · Topic 8 of 16
A snapshot is a full copy of a table, taken on a schedule. A nightly job copies the current customer rows into a table with a snapshot_date column. It stores no ranges.
Here are Asha’s rows for five nights. The 2026-01-12 copy is taken after the late news arrives:
Event 2 was recorded on 2026-01-27, so it is not known yet on 2026-01-26.
LIMIT 1 keeps the first row after sorting. It works in PostgreSQL, MySQL and Snowflake. Other engines use TOP 1 or FETCH FIRST 1 ROW ONLY.
Remember
Event 2 arrived late, so it is a correction. You still only add it, with both dates. Fix a wrong event with a new event too, and never edit the old one.
10 · Late rows
Rows that arrive late
⏱ 8 min · Topic 10 of 16
A late fact is loaded after its day, like order 106 in Fact Table Design. You look up the dimension by the order date, not the load date.
A late dimension version is harder. Old fact rows may already point at the wrong version.
On 2026-01-10, order 102 was loaded. Asha had one row then, key 1001. So the fact stored key 1001.
On 2026-01-12, the move arrives. In the Type 2 table, you close row 1001 at 2026-01-08 and add row 1006. Now order 102 points at the wrong row:
fact_orders, as loaded (6 rows)
order_id
customer_key
order_date
status
amount
101
1001
2026-01-03
paid
40.00
102
1001
2026-01-10
paid
25.00
103
1002
2026-01-11
refunded
60.00
104
1003
2026-01-15
paid
15.50
105
1003
2026-01-20
pending
NULL
106
1004
2026-01-21
paid
80.00
Find the wrong keys with a check query. It joins each fact to its stored row, then to the row that is valid on the order date:
Find facts with the wrong keyworked example
SQL
Input data
dim_customer (Asha’s rows)2 rows
customer_key
customer_id
country
valid_from
valid_to
is_current
1001
1
IN
2026-01-01
2026-01-08
false
1006
1
DE
2026-01-08
9999-12-31
true
The other four customers have one row each, valid from 2026-01-01.
fact_orders2 rows
order_id
customer_key
order_date
101
1001
2026-01-03
102
1001
2026-01-10
Orders 103 to 106 are in the table above. Each one already points at its right row.
The IN row covers 2026-01-01 up to 2026-01-12. This is “as known”.
The engine does not know the move was on 2026-01-08. So it cannot answer “as true on 2026-01-10”.
SQL:2011 also has application-time periods for valid time. Db2 and MariaDB support them; SQL Server does not. In most warehouses, you model valid time yourself.
Watch out
Time travel does not replace your own date columns. Many engines delete old versions after a set number of days. Then an audit (an official check of past numbers) for last year fails.
12 · Decide
Choose a temporal design
⏱ 5 min · Topic 12 of 16
Start from the question people will ask. Each question points at one design:
Question, dates needed, design
Question
Dates needed
Design
What is Asha’s country now?
None
Type 1: overwrite
Which country was the customer in at order time?
Valid time
Type 2: valid_from and valid_to
What did the 2026-01-10 report say, and why did it change?
Valid and recorded time
Bitemporal rows
Can we rebuild the table as it was at month end?
Recorded time
Snapshots
What happened to order 105, step by step?
Event time
An event table
Bitemporal rows cost the most. They have four dates, and every query tests two ranges. Use them for the few columns that audits or restated reports need.
Do not use them for every column. Most columns need only Type 1 or Type 2.
Interview question
Interviewers ask what you knew on date X, and what was true then. Answer with four dates, and edit only recorded_to on an old row. Show a query that tests both ranges.
13 · Mistakes
Three common mistakes
⏱ 6 min · Topic 13 of 16
Three mistakes break most temporal tables:
Overlapping ranges. Two rows are true on the same day, so a join returns two rows and counts twice.
Inclusive end dates. A BETWEEN test matches the old and the new row on the day of the move.
Current values for old facts. The join uses is_current, so old orders show today’s value.
You saw mistake 2 in Effective dating. Here is a check for mistake 1, then an example of mistake 3.
You can find overlaps with a self-join. A self-join joins a table to itself. Here, someone ended Asha’s IN row on 2026-01-09 by mistake:
Find overlapping rangesworked example
SQL
Input data
dim_customer (Asha’s rows, with the mistake)2 rows