Validity windows pattern
Learn how to match each fact row to the dimension row whose validity window contains it, such as price bands and SCD Type 2 history
What is a validity window?
A validity window is a dimension row that is only true for a period of time. Each row stores both ends of that period, a valid_from and a valid_to.
For example, finance keeps a price_bands table. Each band has a rate per currency that applies from one date until the next band starts. Every order should pick up the one band it falls in.
The same shape appears in many places:
- Price lists, tax rates and FX rates with an effective date range
- Slowly changing dimensions (SCD Type 2), where each version of a customer row has
valid_fromandvalid_to - Contract terms, territory assignments and plan tiers
Sample data:
price_bands
| id | currency | valid_from | valid_to | rate |
|---|---|---|---|---|
| usd-1 | USD | 2024-01-01 | 2024-03-01 | 1.00 |
| usd-2 | USD | 2024-03-01 | 2024-06-01 | 1.10 |
| usd-3 | USD | 2024-06-01 | NULL | 1.20 |
| eur-1 | EUR | 2024-02-01 | NULL | 0.90 |
orders
| id | currency | ordered_at |
|---|---|---|
| o1 | USD | 2024-02-15 |
| o2 | USD | 2024-03-01 |
| o3 | USD | 2023-12-31 |
| o4 | EUR | 2024-05-01 |
| o5 | USD | 2024-07-01 |
o2 falls exactly on the 1 March boundary. o3 is earlier than every USD band.
The question: how many orders were placed at each rate?
Solution: a range relationship
Declare a range relationship. from and to name a unique column on each side. match holds the whole join condition: the currency equality and the window test.
Step 1: Define models
Model orders {
type: 'table'
table_name: 'ecommerce.orders'
data_source_name: 'your_datasource_name'
dimension id {
type: 'text'
primary_key: true
}
dimension currency { type: 'text' }
dimension ordered_at { type: 'date' }
measure order_count {
label: 'Order Count'
type: 'number'
definition: @aql count(orders.id);;
}
}Model price_bands {
type: 'table'
table_name: 'finance.price_bands'
data_source_name: 'your_datasource_name'
dimension id {
type: 'text'
primary_key: true
}
dimension currency { type: 'text' }
dimension valid_from { type: 'date' }
dimension valid_to { type: 'date' }
dimension rate { type: 'number' }
}Step 2: Define the relationship
Relationship order_price_band {
type: 'range'
from: r(orders.id)
to: r(price_bands.id)
match: @aql
// The order and the band must use the same currency
orders.currency == price_bands.currency
// The order date must fall within the validity window
and orders.ordered_at >= price_bands.valid_from
// The end of the window is exclusive (half-open interval)
and orders.ordered_at < coalesce(price_bands.valid_to, @9999-12-31)
;;
}- The window is half-open:
>=at the start,<at the end. Two adjacent bands can share a boundary date without overlapping, and an order on that date goes to the later band. coalesceturns an open-ended band (valid_toisNULL) into one that runs forever. Without it, the comparison againstNULLmatches nothing.rangeclaims each order matches at most one band, the same asmany_to_one. That claim holds because bands within one currency do not overlap.
Step 3: Add it to a dataset
Dataset finance {
data_source_name: 'your_datasource_name'
models: [orders, price_bands]
relationships: [
relationship(order_price_band, true)
]
}Step 4: Query
explore {
dimensions { rate: price_bands.rate }
measures { orders: count(orders.id) }
}AMQL puts the equality and the window test in the ON clause:
LEFT JOIN price_bands
ON orders.currency = price_bands.currency
AND orders.ordered_at >= price_bands.valid_from
AND orders.ordered_at < coalesce(price_bands.valid_to, DATE '9999-12-31')Result:
| rate | orders | which orders |
|---|---|---|
| 0.90 | 1 | o4 |
| 1.00 | 1 | o1 |
| 1.10 | 1 | o2, on the boundary, goes to the later band |
| 1.20 | 1 | o5 |
| NULL | 1 | o3, earlier than every USD band |
Five orders, five rows. Each order lands in exactly one band, and the total matches the orders table.
SCD Type 2: current version only
An SCD Type 2 dimension keeps every version of a row. Use the range relationship above when facts need the attributes as of their own date.
When a report only needs the current version, filter the to side instead. This is an ordinary many_to_one with a where {} block:
Relationship order_customer_current {
type: 'many_to_one'
from: r(orders.customer_id)
to: r(dim_customers.customer_id)
where { to: @aql dim_customers.is_current == true ;; }
}The filter sits on the relationship, not the model. A second, point-in-time relationship into dim_customers can still see every version.
Validity windows or interval overlap?
Both patterns use a date inequality. The difference is how many dimension rows one fact row can match.
| Each fact row matches... | Example | Pattern |
|---|---|---|
| at most one dimension row | an order and its price band | Validity windows (range) |
| any number of dimension rows | an order and every day it is open | Interval overlap (many_to_many) |
Key points
- Non-overlap is your claim. AMQL does not check that windows within a partition do not overlap. Overlapping windows fan out and return wrong numbers, the same as a false
many_to_one. - Handle open ends explicitly. Wrap a nullable end in
coalesce, or test it withis null. A bare comparison againstNULLdrops the row. - Unmatched rows are kept. A fact row that falls in no window reports under
NULLinstead of disappearing. - Works on every database. The emitted SQL is a plain range join.