lesson 87 Warehouse & reporting projects · advanced
Protect fact grain when joining versions
Join historical versions without multiplying fact rows.
the idea
Joining a fact table to a type-2 history must match exactly one version per fact row. Use either the current version (is_current = 1) for “as now” reporting or the version valid on the fact date for “as then” reporting. Both preserve the fact grain; joining to all versions does not.
Dataset: Retail
The tables the challenges use, with their first 6 rows. Under each column name: the kind of value it holds, and which table it links to.
customer_history7 columns · 1,685 rows
| history_idnumber · unique id | customer_idnumber · links to customers | segmenttext | countrytext | valid_fromdate | valid_todate | is_currentnumber |
|---|---|---|---|---|---|---|
| 1 | 12347 | occasional | Iceland | 2010-10-31 | 2010-11-01 | 0 |
| 2 | 12347 | regular | Iceland | 2010-11-01 | NULL | 1 |
| 3 | 12353 | occasional | Bahrain | 2010-10-27 | NULL | 1 |
| 4 | 12359 | occasional | Cyprus | 2009-12-05 | 2010-01-01 | 0 |
| 5 | 12359 | regular | Cyprus | 2010-01-01 | 2011-11-01 | 0 |
| 6 | 12359 | key_account | Cyprus | 2011-11-01 | NULL | 1 |
- history_idnumber · unique id
- 1
- customer_idnumber · links to customers
- 12347
- segmenttext
- occasional
- countrytext
- Iceland
- valid_fromdate
- 2010-10-31
- valid_todate
- 2010-11-01
- is_currentnumber
- 0
- history_idnumber · unique id
- 2
- customer_idnumber · links to customers
- 12347
- segmenttext
- regular
- countrytext
- Iceland
- valid_fromdate
- 2010-11-01
- valid_todate
- NULL
- is_currentnumber
- 1
- history_idnumber · unique id
- 3
- customer_idnumber · links to customers
- 12353
- segmenttext
- occasional
- countrytext
- Bahrain
- valid_fromdate
- 2010-10-27
- valid_todate
- NULL
- is_currentnumber
- 1
- history_idnumber · unique id
- 4
- customer_idnumber · links to customers
- 12359
- segmenttext
- occasional
- countrytext
- Cyprus
- valid_fromdate
- 2009-12-05
- valid_todate
- 2010-01-01
- is_currentnumber
- 0
- history_idnumber · unique id
- 5
- customer_idnumber · links to customers
- 12359
- segmenttext
- regular
- countrytext
- Cyprus
- valid_fromdate
- 2010-01-01
- valid_todate
- 2011-11-01
- is_currentnumber
- 0
- history_idnumber · unique id
- 6
- customer_idnumber · links to customers
- 12359
- segmenttext
- key_account
- countrytext
- Cyprus
- valid_fromdate
- 2011-11-01
- valid_todate
- NULL
- is_currentnumber
- 1
first 6 of 1,685 rows · your queries use all of them
dim_customer5 columns · 848 rows
| customer_keynumber · unique id | customer_idtext | contact_nametext | countrytext | regiontext |
|---|---|---|---|---|
| 1 | 12347 | Arthur Turner | Iceland | Europe |
| 2 | 12353 | Harry Turner | Bahrain | Rest of World |
| 3 | 12359 | Theo Turner | Cyprus | Europe |
| 4 | 12373 | Harry Hill | Austria | Europe |
| 5 | 12374 | Emily Hill | Austria | Europe |
| 6 | 12384 | Ava Moore | Switzerland | Europe |
- customer_keynumber · unique id
- 1
- customer_idtext
- 12347
- contact_nametext
- Arthur Turner
- countrytext
- Iceland
- regiontext
- Europe
- customer_keynumber · unique id
- 2
- customer_idtext
- 12353
- contact_nametext
- Harry Turner
- countrytext
- Bahrain
- regiontext
- Rest of World
- customer_keynumber · unique id
- 3
- customer_idtext
- 12359
- contact_nametext
- Theo Turner
- countrytext
- Cyprus
- regiontext
- Europe
- customer_keynumber · unique id
- 4
- customer_idtext
- 12373
- contact_nametext
- Harry Hill
- countrytext
- Austria
- regiontext
- Europe
- customer_keynumber · unique id
- 5
- customer_idtext
- 12374
- contact_nametext
- Emily Hill
- countrytext
- Austria
- regiontext
- Europe
- customer_keynumber · unique id
- 6
- customer_idtext
- 12384
- contact_nametext
- Ava Moore
- countrytext
- Switzerland
- regiontext
- Europe
first 6 of 848 rows · your queries use all of them
fact_sales7 columns · 45,151 rows
| sale_keynumber · unique id | order_idnumber | customer_keynumber · links to dim_customer | product_keynumber · links to dim_product | order_datedate | quantitynumber | salesdecimal |
|---|---|---|---|---|---|---|
| 65625 | 3679 | 556 | 2062 | 2011-01-04 | 8 | 34 |
| 65626 | 3679 | 556 | 881 | 2011-01-04 | 1 | 7.95 |
| 65627 | 3679 | 556 | 887 | 2011-01-04 | 1 | 4.95 |
| 65628 | 3679 | 556 | 401 | 2011-01-04 | 2 | 5.9 |
| 65629 | 3679 | 556 | 4059 | 2011-01-04 | 36 | 30.6 |
| 65630 | 3679 | 556 | 4301 | 2011-01-04 | 10 | 29.5 |
- sale_keynumber · unique id
- 65625
- order_idnumber
- 3679
- customer_keynumber · links to dim_customer
- 556
- product_keynumber · links to dim_product
- 2062
- order_datedate
- 2011-01-04
- quantitynumber
- 8
- salesdecimal
- 34
- sale_keynumber · unique id
- 65626
- order_idnumber
- 3679
- customer_keynumber · links to dim_customer
- 556
- product_keynumber · links to dim_product
- 881
- order_datedate
- 2011-01-04
- quantitynumber
- 1
- salesdecimal
- 7.95
- sale_keynumber · unique id
- 65627
- order_idnumber
- 3679
- customer_keynumber · links to dim_customer
- 556
- product_keynumber · links to dim_product
- 887
- order_datedate
- 2011-01-04
- quantitynumber
- 1
- salesdecimal
- 4.95
- sale_keynumber · unique id
- 65628
- order_idnumber
- 3679
- customer_keynumber · links to dim_customer
- 556
- product_keynumber · links to dim_product
- 401
- order_datedate
- 2011-01-04
- quantitynumber
- 2
- salesdecimal
- 5.9
- sale_keynumber · unique id
- 65629
- order_idnumber
- 3679
- customer_keynumber · links to dim_customer
- 556
- product_keynumber · links to dim_product
- 4059
- order_datedate
- 2011-01-04
- quantitynumber
- 36
- salesdecimal
- 30.6
- sale_keynumber · unique id
- 65630
- order_idnumber
- 3679
- customer_keynumber · links to dim_customer
- 556
- product_keynumber · links to dim_product
- 4301
- order_datedate
- 2011-01-04
- quantitynumber
- 10
- salesdecimal
- 29.5
first 6 of 45,151 rows · your queries use all of them
worked example
Run these steps in order to see the idea before you write your own query. The step marked “the pattern” is what the challenges ask for. Running the example does not count toward progress.
See row inflation without a date filter. Joining every history version to every sale multiplies fact_rows—totals would be wrong if you stopped here.the pattern
example.sqlSELECT COUNT(*) AS fact_rows, ( SELECT COUNT(*) FROM fact_sales s JOIN dim_customer c ON c.customer_key = s.customer_key JOIN customer_history h ON h.customer_id = CAST(c.customer_id AS INTEGER) ) AS joined_to_all_versions FROM fact_sales;
Count rows with current-version join. Filtering to is_current = 1 matches one history row per customer, so the join count stays equal to fact_sales.
step-2.sqlSELECT COUNT(*) AS current_join FROM fact_sales s JOIN dim_customer c ON c.customer_key = s.customer_key JOIN customer_history h ON h.customer_id = CAST(c.customer_id AS INTEGER) AND h.is_current = 1;
Count rows with as-of-date join. Matching valid_from and valid_to to each line’s order_date also preserves one row per sale for historical reporting.
step-3.sqlSELECT COUNT(*) AS as_of_join FROM fact_sales s JOIN dim_customer c ON c.customer_key = s.customer_key JOIN customer_history h ON h.customer_id = CAST(c.customer_id AS INTEGER) AND h.valid_from <= s.order_date AND (h.valid_to IS NULL OR s.order_date < h.valid_to);
common mistake
A join on customer only repeats each sale once per version and inflates totals.