Point-in-Time (As-Of) Joins Against an SCD2 Dimension
Join a fact to the dimension version that was valid at event time using a half-open effective range, not the is_current row.
History is worthless if you only ever read the current row
L4 built a Type-2 (SCD2) dimension: every time a customer attribute changes, you close the old row by setting its effective_to and open a new one, so the table keeps the full history with effective_from and effective_to ranges and an is_current flag. The point of keeping that history is to answer "what was true then", and that is exactly what most people get wrong. They join on is_current = 1, which staples today's attribute onto every historical fact. An order placed while the customer was in the east region gets mislabeled west because that is where they live now.
The as-of (point-in-time) join
Join the fact to the dimension version whose validity window contains the event date:
JOIN dim_customer d
ON d.customer_id = o.customer_id
AND o.event_date >= d.effective_from
AND o.event_date < d.effective_to
The half-open range (>= on the low end, < on the high end) is deliberate: a change that takes effect on a date belongs to the new version, and no event ever matches two versions. The same predicate handles a late-arriving fact: an order that lands after its dimension already changed still joins to the version that was valid on its event date, not the newest one.
Interview nuance: get the boundary right or you double-count. If both ends were inclusive, an event on a change date would match the old and the new version at once, and the fact would fan out to two rows. Half-open ranges are how point-in-time joins stay one-to-one.
In the warehouse this differs. Snowflake, BigQuery, and DuckDB offer an
ASOF JOINkeyword that finds the most recent dimension row at or before the event, so you do not write the range by hand. SQLite has noASOF JOIN, so you express the same result with the half-open range predicate. The correctness idea, attach the version that was valid then and not the current one, is dialect-independent.
CREATE TABLE dim_customer (customer_id INTEGER, region TEXT, effective_from TEXT, effective_to TEXT, is_current INTEGER);
INSERT INTO dim_customer VALUES
(1, 'east', '2026-01-01', '2026-03-01', 0),
(1, 'west', '2026-03-01', '9999-12-31', 1),
(2, 'north', '2026-01-01', '9999-12-31', 1);
CREATE TABLE fact_orders (order_id INTEGER, customer_id INTEGER, event_date TEXT, amount INTEGER);
INSERT INTO fact_orders VALUES
(100, 1, '2026-02-15', 50), (101, 1, '2026-03-10', 80), (102, 2, '2026-02-20', 30), (103, 1, '2026-03-01', 40);-- region_as_of is what was true then; region_current is is_current. Order 100 shows the gap.
SELECT o.order_id, o.event_date,
asof.region AS region_as_of,
cur.region AS region_current
FROM fact_orders o
JOIN dim_customer asof
ON asof.customer_id = o.customer_id AND o.event_date >= asof.effective_from AND o.event_date < asof.effective_to
JOIN dim_customer cur
ON cur.customer_id = o.customer_id AND cur.is_current = 1
ORDER BY o.order_id;Apply
Your turn
The task this lesson builds to.
Write a query that attaches to each order the customer region that was current on the order's event_date, as (order_id, region), over fact_orders(order_id, customer_id, event_date, amount) and the Type-2 dim_customer(customer_id, region, effective_from, effective_to, is_current).
Join to the dimension version whose validity window contains event_date using a half-open range (>= effective_from AND < effective_to), not the is_current row. Alias the columns exactly as named.
3 hints and 1 automated check are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Write a query that returns total revenue bucketed by the region effective at event time versus by the customer's current region, as (region, revenue_as_was, revenue_current), over the same fact_orders and dim_customer tables.
revenue_as_was sums amount grouped by the as-of region (the half-open range join); revenue_current sums amount grouped by the customer's is_current region. A region with no revenue under one attribution shows 0. Alias the columns exactly as named.
3 hints and 1 automated check are waiting in the workspace.