Skip to content

Commit d199035

Browse files
authored
feat(week-10): add dbt SQL practice exercises for Week 10 (#35)
1 parent bcbaf77 commit d199035

17 files changed

Lines changed: 589 additions & 0 deletions

File tree

data-track/week-10/README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# HYF Data Track — Week 10 Practice Exercises
2+
3+
Four exercises that consolidate Week 10 (dbt Core: SQL transformations as a tested, documented, version-controlled project). Each exercise targets one skill from the content chapters. Work through them in order: Exercise 1 is a prerequisite for the `fct_trips` mart, and later exercises assume the project state from Exercises 1–3.
4+
5+
These exercises require your Week 10 dbt project (set up in Chapter 2) running against the shared Azure PostgreSQL instance. They are not standalone: you copy the provided SQL files into your project and run dbt commands.
6+
7+
## Layout
8+
9+
| Folder | Skill | dbt command to verify |
10+
|--------|-------|----------------------|
11+
| `exercise_1` | Macros and computed columns for `stg_trips` | `dbt run --select stg_trips` |
12+
| `exercise_2` | Write and run a singular test | `dbt test --select test_type:singular` |
13+
| `exercise_3` | Debug a broken `ref()` | `dbt compile --select fct_trips` |
14+
| `exercise_4` | Propagate a column change from staging to mart | `dbt run --select +fct_trips` |
15+
16+
## Folder structure
17+
18+
```text
19+
week-10/
20+
exercise_1/
21+
safe_divide.sql -- copy to macros/safe_divide.sql
22+
stg_trips.sql -- copy to models/staging/stg_trips.sql
23+
README.md
24+
solutions/
25+
safe_divide.sql
26+
stg_trips.sql
27+
exercise_2/
28+
assert_fare_amount_non_negative.sql -- copy to tests/
29+
README.md
30+
solutions/
31+
assert_fare_amount_non_negative.sql
32+
exercise_3/
33+
fct_trips_broken.sql -- copy to models/marts/fct_trips.sql (overwrites)
34+
README.md
35+
solutions/
36+
fct_trips.sql
37+
exercise_4/
38+
stg_trips.sql -- copy to models/staging/stg_trips.sql
39+
fct_trips.sql -- copy to models/marts/fct_trips.sql
40+
README.md
41+
solutions/
42+
stg_trips.sql
43+
fct_trips.sql
44+
```
45+
46+
## How to run
47+
48+
These are dbt model, macro, and test files. Copy each file to the path shown in the exercise README, then run the dbt command shown in the table above. You need your Week 10 dbt project connected to the shared Azure PostgreSQL and `PG_PASSWORD` set in your environment.
49+
50+
No `requirements.txt` is needed here: your dbt project already has `dbt-core` and `dbt-postgres` installed from Chapter 2.
51+
52+
## Reference solutions and spoiler discipline
53+
54+
Each `exercise_N/solutions/` folder holds the answer with `-- WHY` comments explaining the non-obvious choices. Time-box yourself to 15–20 minutes on each exercise before peeking. When you do open a solution, read the WHY notes: the reasoning is the teaching content.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Exercise 1: Macros and computed columns for `stg_trips`
2+
3+
## What you do
4+
5+
The [Materializations chapter](https://github.com/HackYourFuture/datatrack) builds `fct_trips` assuming `stg_trips` already exposes `tip_pct`, `fare_per_mile`, and `payment_type_label`. This exercise adds those three columns so `fct_trips` can use them.
6+
7+
There are two files:
8+
9+
| File | Copy to your project at |
10+
|---|---|
11+
| `safe_divide.sql` | `macros/safe_divide.sql` |
12+
| `stg_trips.sql` | `models/staging/stg_trips.sql` |
13+
14+
## How to run
15+
16+
Copy both files into your Week 10 dbt project, complete the TODOs, then:
17+
18+
```bash
19+
dbt compile --select stg_trips
20+
```
21+
22+
Open `target/compiled/<project>/models/staging/stg_trips.sql` and confirm every `{{ ... }}` block has been replaced with plain SQL.
23+
24+
```bash
25+
dbt run --select stg_trips
26+
```
27+
28+
Spot-check the result against your schema:
29+
30+
```sql
31+
SELECT payment_type, payment_type_label, tip_pct, fare_per_mile
32+
FROM dev_<your_name>.stg_trips
33+
LIMIT 10;
34+
```
35+
36+
## Success criteria
37+
38+
- Every `payment_type` code (1–6) resolves to a readable label.
39+
- `tip_pct` and `fare_per_mile` are non-null for rows with non-zero denominators.
40+
- `dbt compile` exits cleanly with no Jinja errors.
41+
42+
Stuck? The reference implementation is in `solutions/`. Try for 20 minutes first.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-- Exercise 1a: safe_divide macro
2+
--
3+
-- Create this file at macros/safe_divide.sql in your Week 10 dbt project.
4+
--
5+
-- A macro is a reusable Jinja function. This one wraps a division so it returns
6+
-- NULL instead of a database error when the denominator is 0 or NULL.
7+
--
8+
-- Signature: safe_divide(numerator, denominator) -> SQL expression
9+
-- Returns: numerator / denominator, or NULL when denominator is 0 or NULL.
10+
--
11+
-- Used by stg_trips.sql for tip_pct and fare_per_mile.
12+
13+
{% macro safe_divide(numerator, denominator) %}
14+
-- TODO: write the macro body. It should:
15+
-- 1. Wrap denominator in NULLIF(denominator, 0) so a zero denominator becomes NULL.
16+
-- 2. Divide numerator by that result. PostgreSQL propagates NULL automatically.
17+
-- 3. Return only the SQL expression — no semicolon, no SELECT.
18+
--
19+
-- Expected compiled output for safe_divide('tip_amount', 'fare_amount'):
20+
-- tip_amount / NULLIF(fare_amount, 0)
21+
{% endmacro %}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-- Exercise 1a solution: safe_divide macro
2+
3+
{% macro safe_divide(numerator, denominator) %}
4+
{{ numerator }} / NULLIF({{ denominator }}, 0)
5+
-- WHY NULLIF: NULLIF(x, 0) returns NULL when x is 0, and x otherwise.
6+
-- PostgreSQL propagates NULL through division automatically, so the whole
7+
-- expression becomes NULL instead of raising a division-by-zero error.
8+
-- This is the standard dbt pattern for any ratio column that could have a
9+
-- zero denominator (tip_pct when fare_amount=0, fare_per_mile when trip_distance=0).
10+
{% endmacro %}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
-- Exercise 1b solution: stg_trips with computed columns
2+
3+
SELECT
4+
-- identifiers
5+
vendorid,
6+
lpep_pickup_datetime AS pickup_datetime,
7+
lpep_dropoff_datetime AS dropoff_datetime,
8+
9+
-- location keys
10+
pulocationid AS pickup_location_id,
11+
dolocationid AS dropoff_location_id,
12+
13+
-- trip facts
14+
trip_distance,
15+
fare_amount,
16+
tip_amount,
17+
payment_type,
18+
19+
-- computed: tip as a fraction of fare
20+
{{ safe_divide('tip_amount', 'fare_amount') }} AS tip_pct,
21+
-- WHY safe_divide: fare_amount can be 0 (voided or no-charge trips in the TLC data).
22+
-- Without NULLIF the database raises a division-by-zero error on those rows.
23+
24+
-- computed: fare per mile of travel
25+
{{ safe_divide('fare_amount', 'trip_distance') }} AS fare_per_mile,
26+
-- WHY safe_divide: trip_distance is 0 on some short trips recorded as
27+
-- metered-time-only (airport flat-rate, etc.). Same NULLIF guard needed.
28+
29+
-- computed: human-readable payment label from TLC integer code
30+
CASE payment_type
31+
{% for code, label in [
32+
(1, 'Credit card'),
33+
(2, 'Cash'),
34+
(3, 'No charge'),
35+
(4, 'Dispute'),
36+
(5, 'Unknown'),
37+
(6, 'Voided trip')
38+
] %}
39+
WHEN {{ code }} THEN '{{ label }}'
40+
{% endfor %}
41+
ELSE 'Other'
42+
END AS payment_type_label
43+
-- WHY Jinja loop: the six codes and labels live in one place in the template;
44+
-- adding or removing a code is a one-line change rather than editing six WHEN branches.
45+
46+
FROM {{ source('nyc_taxi', 'raw_trips') }}
47+
WHERE pickup_location_id IS NOT NULL
48+
AND fare_amount >= 0
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
-- Exercise 1b: Add computed columns to stg_trips
2+
--
3+
-- Replace (or update) models/staging/stg_trips.sql in your Week 10 dbt project.
4+
--
5+
-- The Materializations chapter builds fct_trips assuming stg_trips already
6+
-- exposes tip_pct, fare_per_mile, and payment_type_label. This exercise adds
7+
-- those three derived columns so fct_trips can use them.
8+
--
9+
-- Start from this template — it has the source reference and basic column list
10+
-- from Chapter 2. Your task is to add the three derived columns.
11+
12+
SELECT
13+
-- identifiers
14+
vendorid,
15+
lpep_pickup_datetime AS pickup_datetime,
16+
lpep_dropoff_datetime AS dropoff_datetime,
17+
18+
-- location keys (used as join keys in fct_trips — keep as integers)
19+
pulocationid AS pickup_location_id,
20+
dolocationid AS dropoff_location_id,
21+
22+
-- trip facts
23+
trip_distance,
24+
fare_amount,
25+
tip_amount,
26+
payment_type,
27+
28+
-- TODO 1: add tip_pct using {{ safe_divide('tip_amount', 'fare_amount') }}
29+
-- alias it as tip_pct
30+
31+
-- TODO 2: add fare_per_mile using {{ safe_divide('fare_amount', 'trip_distance') }}
32+
-- alias it as fare_per_mile
33+
34+
-- TODO 3: add payment_type_label using a {% for %} loop over the TLC code mapping:
35+
-- 1 -> 'Credit card'
36+
-- 2 -> 'Cash'
37+
-- 3 -> 'No charge'
38+
-- 4 -> 'Dispute'
39+
-- 5 -> 'Unknown'
40+
-- 6 -> 'Voided trip'
41+
-- Use a CASE WHEN block generated by the loop. See the Jinja chapter for
42+
-- the pattern. The final alias should be payment_type_label.
43+
44+
FROM {{ source('nyc_taxi', 'raw_trips') }}
45+
WHERE pickup_location_id IS NOT NULL
46+
AND fare_amount >= 0
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Exercise 2: Write a singular test
2+
3+
## What you do
4+
5+
Singular tests are plain `.sql` files that return the bad rows. dbt passes the test when the query returns zero rows. You write one yourself to prove you understand when to use a singular test over a generic YAML test, and experience the `FAIL 182 → PASS` cycle that confirms the test actually catches real data issues.
6+
7+
| File | Copy to your project at |
8+
|---|---|
9+
| `assert_fare_amount_non_negative.sql` | `tests/assert_fare_amount_non_negative.sql` |
10+
11+
## How to run
12+
13+
1. Copy the file to `tests/` and complete the TODO.
14+
2. Temporarily remove the `AND fare_amount >= 0` filter from `stg_trips.sql` and rebuild:
15+
16+
```bash
17+
dbt run --select stg_trips
18+
dbt test --select test_type:singular
19+
```
20+
21+
The test must report `FAIL 182`. This step proves the test actually catches the problem.
22+
23+
3. Restore `AND fare_amount >= 0` to `stg_trips.sql`, rebuild, and re-run the test. It must now report `PASS`.
24+
25+
## Success criteria
26+
27+
- Test reports `FAIL 182` against unfiltered `stg_trips`, and `PASS` after the filter is restored.
28+
- You can state in one sentence why a singular test is the right tool here rather than a generic `not_null` YAML test.
29+
30+
Stuck? The reference implementation is in `solutions/`. Try for 15 minutes first.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-- Exercise 2: Write a singular test
2+
--
3+
-- Create this file at tests/assert_fare_amount_non_negative.sql in your dbt project.
4+
--
5+
-- A singular test is a plain SELECT that returns the *bad* rows.
6+
-- dbt runs this query and reports FAIL if any rows are returned, PASS if zero rows come back.
7+
--
8+
-- Your task: write a SELECT against {{ ref('stg_trips') }} that returns every row
9+
-- where fare_amount is negative.
10+
--
11+
-- Expected result when you run `dbt test --select test_type:singular`:
12+
-- PASS (because stg_trips already filters fare_amount >= 0)
13+
--
14+
-- Before completing the TODO, first temporarily comment out the WHERE fare_amount >= 0
15+
-- filter in stg_trips.sql and re-run dbt run --select stg_trips + dbt test --select test_type:singular
16+
-- to confirm the test correctly reports FAIL 182 against the unfiltered data.
17+
-- Then restore the filter and confirm the test goes back to PASS.
18+
19+
-- TODO: write the singular test SELECT here.
20+
-- Replace this comment with: SELECT * FROM {{ ref('stg_trips') }} WHERE ...
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-- Exercise 2 solution: singular test for negative fare amounts
2+
3+
SELECT *
4+
FROM {{ ref('stg_trips') }}
5+
WHERE fare_amount < 0
6+
-- WHY singular test (not a generic YAML test): the check involves the column's actual
7+
-- value, not just presence or allowed set. A generic `not_null` only checks for NULL,
8+
-- and `accepted_values` requires listing every valid value. A singular test can express
9+
-- any SQL predicate, including numeric ranges or cross-column rules.
10+
--
11+
-- WHY this test is expected to PASS on the filtered stg_trips:
12+
-- stg_trips already applies WHERE fare_amount >= 0, so no negative rows survive into
13+
-- the model. The test passes because the staging filter removed the bad data upstream.
14+
-- If the filter were ever accidentally removed, this test would catch the regression.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Exercise 3: Debug a broken `ref()`
2+
3+
## What you do
4+
5+
When dbt cannot resolve a `{{ ref() }}`, it fails at compile time, not at run time. That is different from a plain SQL `CREATE VIEW`: Postgres would not catch a bad table name until someone queries the view. This exercise drills the compile-to-find-the-typo workflow.
6+
7+
| File | Copy to your project at |
8+
|---|---|
9+
| `fct_trips_broken.sql` | `models/marts/fct_trips.sql` (overwrites your working file) |
10+
11+
## How to run
12+
13+
1. Copy the file and run:
14+
15+
```bash
16+
dbt compile --select fct_trips
17+
```
18+
19+
2. Read the error message. Write down: which node did dbt name, and which file did it point at?
20+
21+
3. Find the typo and fix it directly in `models/marts/fct_trips.sql`.
22+
23+
4. Re-run `dbt compile --select fct_trips` to confirm it compiles cleanly.
24+
25+
## Success criteria
26+
27+
- You spotted the typo from the error message without reading the file top-to-bottom.
28+
- You can state in one sentence why dbt reports the error at compile time rather than at run time.
29+
30+
Stuck? The corrected file is in `solutions/`. Try for 10 minutes first.

0 commit comments

Comments
 (0)