An order arrived at 09:15. The price changed at 09:10 and again at 09:25. An equality join finds no price at 09:15. A plain “earlier than” join finds too many. We want the most recent price already in effect.
That is an as-of match: pair each event with the applicable earlier state of the same entity.
Explore the idea
Move an order along the price history
Each WREN price applies from its timestamp until the next update.
NULL
order 101 has no applicable price
Before the first known price
The 09:00 price is in the future. An ASOF LEFT JOIN preserves the order without inventing a price.
10.00
price for order 102
An exact-time update counts
Our >= condition includes a price published at exactly the order’s timestamp.
12.00
price for order 103
Use the latest price already in effect
The 09:10 update is the nearest earlier one. The next update at 09:25 must not leak into this order.
11.50
price for order 104
Move forward when a new price takes effect
All three prices are earlier, but only the most recent one is applicable.
Two timelines that do not line up
A price applies from its effective timestamp, including that instant, until the next price for the same SKU. The fixture uses local, timezone-free timestamps under one shared clock. A production system must normalize its time semantics before comparing events.
prices4 rows
The price history
Prices apply from their effective timestamp, including that instant, until the next price for the same SKU.
| id | sku | effective_at | price |
|---|---|---|---|
| 1 | WREN | 2026-01-08 09:00:00 | 10 |
| 2 | WREN | 2026-01-08 09:10:00 | 12 |
| 3 | WREN | 2026-01-08 09:25:00 | 11.5 |
| 4 | FIR | 2026-01-08 09:05:00 | 20 |
orders5 rows
Orders placed along that timeline
Order 101 predates every WREN price. FIR has its own independent price history.
| id | sku | ordered_at | qty |
|---|---|---|---|
| 101 | WREN | 2026-01-08 08:55:00 | 2 |
| 102 | WREN | 2026-01-08 09:00:00 | 1 |
| 103 | WREN | 2026-01-08 09:15:00 | 3 |
| 104 | WREN | 2026-01-08 09:30:00 | 2 |
| 105 | FIR | 2026-01-08 09:10:00 | 1 |
WREN has three price updates. FIR has a separate history. Order 101 arrives before any WREN price, so we cannot legitimately assign it one from this dataset.
Why an ordinary inequality join returns too much
Look at order 104, placed at 09:30. A regular join on an earlier-or-equal price time keeps all three WREN prices:
A regular inequality join keeps every earlier price
SELECT o.id, p.effective_at::VARCHAR AS price_time, p.price
FROM orders o LEFT JOIN prices p
ON o.sku = p.sku AND o.ordered_at >= p.effective_at
WHERE o.id = 104 ORDER BY p.effective_at;
| id | price_time | price |
|---|---|---|
| 104 | 2026-01-08 09:00:00 | 10 |
| 104 | 2026-01-08 09:10:00 | 12 |
| 104 | 2026-01-08 09:25:00 | 11.5 |
Every row satisfies the condition. Only the last is the applicable price. If you multiplied the quantity by every matching price and summed, you would invent extra charges.
A regular equality join has the opposite problem: it only matches an order placed exactly when a price was published. An unchanged price remains applicable between updates.
Ask for the closest earlier match
ASOF JOIN combines the equality key with the time inequality. The left side contains events; the right side contains the state changes we want to look backward through.
Match the closest earlier price for each order
SELECT o.id, o.sku, p.effective_at::VARCHAR AS price_time, p.price
FROM orders o
ASOF LEFT JOIN prices p
ON o.sku = p.sku AND o.ordered_at >= p.effective_at
ORDER BY o.id;
| id | sku | price_time | price |
|---|---|---|---|
| 101 | WREN | NULL | NULL |
| 102 | WREN | 2026-01-08 09:00:00 | 10 |
| 103 | WREN | 2026-01-08 09:10:00 | 12 |
| 104 | WREN | 2026-01-08 09:25:00 | 11.5 |
| 105 | FIR | 2026-01-08 09:05:00 | 20 |
Scroll for more columns →
The SKU condition keeps the two product histories separate. ordered_at >= effective_at chooses the latest right-hand timestamp that is not after the order. The equality part of that inequality makes the 09:00 price valid for the 09:00 order.
LEFT preserves order 101 with NULL price fields. An inner as-of join would remove an order with no match, which is inconvenient if the task is to audit every order.
The result has at most one matched right-hand row per left-hand row. Do not rely on an arbitrary winner if your price history contains duplicate (sku, effective_at) keys. Resolve those duplicates under a documented rule before joining; the latest-record lesson demonstrates an explicit tie-break.
Calculate with the matched state
Once the applicable price is attached, the amount is quantity times price:
Calculate amounts while preserving the unpriced order
SELECT o.id, o.qty, p.price, o.qty * p.price AS amount
FROM orders o
ASOF LEFT JOIN prices p
ON o.sku = p.sku AND o.ordered_at >= p.effective_at
ORDER BY o.id;
| id | qty | price | amount |
|---|---|---|---|
| 101 | 2 | NULL | NULL |
| 102 | 1 | 10 | 10 |
| 103 | 3 | 12 | 36 |
| 104 | 2 | 11.5 | 23 |
| 105 | 1 | 20 | 20 |
Scroll for more columns →
Order 103 uses 12.00, producing 36.00. Order 104 uses 11.50, producing 23.00. Order 101 stays unpriced. Replacing that missing price with zero would convert “we cannot price it” into “it was free,” which is a different business statement.
Your turn · check your answer
Find orders with no earlier price
Return id for orders with no applicable price at or before their timestamp. Keep the SKU equality condition.
See the expected result
| id |
|---|
| 101 |
Preparing the editor…
Your result
Need a hint?
An equality join mistakes an unchanged price for a missing one. Use ASOF LEFT JOIN and then test the matched price ID.
Show a solution
SELECT o.id
FROM orders o
ASOF LEFT JOIN prices p
ON o.sku = p.sku AND o.ordered_at >= p.effective_at
WHERE p.id IS NULL ORDER BY o.id;Put boundaries around “applicable”
This example assumes a price stays valid until the next update. If prices expire after a fixed interval, add a policy that checks the age of the matched state. An earlier match can be technically correct yet too stale for the business question.
If timestamps come from different time zones, decide whether they represent local wall-clock values or actual instants. Normalize them consistently; changing a display format is not a time-zone conversion.
The same pattern works for exchange rates, sensor readings, and account status at the time of an event. Each needs the same decisions: the equality key, the direction of time, whether an exact-time match counts, what happens before the first state, and how stale a match may be.