Skip to content

Commit bcbaf77

Browse files
lassebenniLasse Benningaclaude
authored
Polish Week 9 practice exercises: reorder, add fan-out, and fix SQL numbering (#34)
* fix(data-track/week-9): clarify validation expectations, deterministic sample, assignment cast note Polish from a Week 9 exercise review (all solutions live-verified against the shared Azure Postgres: Manhattan top, 0 NULL pickups, 189 duplicate groups, 0 orphans, fact view 56,369 = 56,551 - 182 negatives). - Ex1: add ORDER BY pickup_datetime before LIMIT 5 so the sample is stable between runs (a bare LIMIT returns an arbitrary 5 rows). - Ex5: tell students 5a returns 0 and 5c returns empty BY DESIGN (pickup IDs are complete; orphans do not exist on this load). A 0 is the check passing, not a mistake. Prevents the "did I break it?" confusion. - Ex6: note the practice view is scaled down; the assignment's vw_fact_trips also casts pickup_datetime::TIMESTAMP. Flag it so students do not under-build the assignment deliverable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix double precision to numeric type casting issue for ROUND() in week 9 exercise solutions * Restructure Week 9 practice: move EXPLAIN to Ex5, shift subsequent exercises, and add Exercise 8 (Deduplicate Fan-out Join) * Correct exercise numbering comments in SQL files to match renamed directories * Add starter queries and skeletons to all Week 9 exercise files --------- Co-authored-by: Lasse Benninga <devops.pipeline@example.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7aef86c commit bcbaf77

10 files changed

Lines changed: 99 additions & 32 deletions

File tree

data-track/week-9/exercise_1/exercise.sql

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,10 @@
1414
-- five rows are the same every run (a bare LIMIT returns an arbitrary
1515
-- sample that can change between runs).
1616

17-
-- TODO: select the four columns and join nyc_taxi.raw_trips to nyc_taxi.raw_zones, then limit to 5 rows.
17+
-- Starter query: exploring raw_trips columns
18+
SELECT pickup_datetime, trip_distance, fare_amount, pickup_location_id
19+
FROM nyc_taxi.raw_trips
20+
ORDER BY pickup_datetime
21+
LIMIT 5;
22+
23+
-- TODO: rewrite the query above to join nyc_taxi.raw_trips to nyc_taxi.raw_zones on pickup_location_id = location_id and select the zone name instead of the ID.

data-track/week-9/exercise_2/exercise.sql

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,11 @@
1111
-- Hint: Join to nyc_taxi.raw_zones to get borough, then GROUP BY z.borough. Every
1212
-- non-aggregated column in the SELECT must appear in the GROUP BY.
1313

14-
-- TODO: join nyc_taxi.raw_trips to nyc_taxi.raw_zones, group by borough, and count trips plus average fare.
14+
-- Starter query: counting raw trips by location ID (no join)
15+
SELECT t.pickup_location_id, COUNT(*) AS trips, AVG(t.fare_amount) AS avg_fare
16+
FROM nyc_taxi.raw_trips t
17+
GROUP BY t.pickup_location_id
18+
ORDER BY trips DESC
19+
LIMIT 5;
20+
21+
-- TODO: rewrite the query above to join to nyc_taxi.raw_zones, group by the borough name, and order by total trips descending.

data-track/week-9/exercise_3/exercise.sql

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,11 @@
1111
-- Hint: Use pickup_datetime::date to drop the time part. Define the daily counts
1212
-- in a WITH block, then ORDER BY ... DESC LIMIT 1 over the CTE.
1313

14-
-- TODO: define a CTE of daily trip counts, then select the single busiest day from it.
14+
-- Starter query: counting trips per day without a CTE
15+
SELECT t.pickup_datetime::date AS trip_date, COUNT(*) AS trips
16+
FROM nyc_taxi.raw_trips t
17+
GROUP BY trip_date
18+
ORDER BY trips DESC
19+
LIMIT 5;
20+
21+
-- TODO: rewrite the query above to use a CTE (WITH block) containing the daily counts, and select the single busiest day from the CTE.

data-track/week-9/exercise_4/exercise.sql

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,14 @@ FROM (
2929
GROUP BY borough
3030
ORDER BY avg_fare DESC;
3131

32-
-- TODO: rewrite the query above using CTEs (one to filter, one to join, then aggregate).
32+
-- Starter skeleton: refactoring into CTEs
33+
WITH positive_trips AS (
34+
-- TODO: select trips where fare_amount > 0 from nyc_taxi.raw_trips
35+
SELECT * FROM nyc_taxi.raw_trips LIMIT 1
36+
),
37+
joined_trips AS (
38+
-- TODO: join positive_trips to nyc_taxi.raw_zones
39+
SELECT * FROM positive_trips
40+
)
41+
-- TODO: final group by and aggregate over joined_trips
42+
SELECT 1;

data-track/week-9/exercise_5/exercise.sql

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
-- Exercise 7 (stretch): Compare a cartesian join to a filtered join
1+
-- Exercise 5 (stretch): Compare a cartesian join to a filtered join
22
--
33
-- A missing join condition produces a cartesian product: every trip matched to
44
-- every zone. Use EXPLAIN to see how the planner treats the two queries
@@ -16,7 +16,12 @@
1616

1717
-- Cartesian product: no join condition
1818
-- TODO: EXPLAIN a CROSS JOIN of nyc_taxi.raw_trips and nyc_taxi.raw_zones (no ON clause).
19+
-- Starter skeleton: (uncomment and prepend EXPLAIN)
20+
-- SELECT * FROM nyc_taxi.raw_trips t CROSS JOIN nyc_taxi.raw_zones z;
1921

2022

2123
-- Filtered join: one zone per trip
2224
-- TODO: EXPLAIN an INNER JOIN of nyc_taxi.raw_trips and nyc_taxi.raw_zones on pickup_location_id = location_id.
25+
-- Starter skeleton: (uncomment and prepend EXPLAIN)
26+
-- SELECT * FROM nyc_taxi.raw_trips t INNER JOIN nyc_taxi.raw_zones z ON t.pickup_location_id = z.location_id;
27+

data-track/week-9/exercise_5/solutions/exercise.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
-- Exercise 7 solution (stretch): Compare a cartesian join to a filtered join
1+
-- Exercise 5 solution (stretch): Compare a cartesian join to a filtered join
22

33
-- Cartesian product: no join condition, ~57K trips x 265 zones
44
EXPLAIN -- WHY EXPLAIN (not EXPLAIN ANALYZE): EXPLAIN only estimates and prints the plan; it never runs the query, so the ~15M-row cartesian product never actually materializes and cannot hang your session
Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
-- Exercise 5: Validate the raw data
1+
-- Exercise 6: Validate the raw data
22
--
33
-- Raw data is rarely clean. Write three checks that the assignment's audit task
44
-- expects you to run:
5-
-- 5a. Count trips with a NULL pickup_location_id.
6-
-- 5b. Find duplicate trips: rows that share the same vendor_id, pickup_datetime,
5+
-- 6a. Count trips with a NULL pickup_location_id.
6+
-- 6b. Find duplicate trips: rows that share the same vendor_id, pickup_datetime,
77
-- and dropoff_datetime.
8-
-- 5c. Find orphaned pickup IDs: pickup_location_id values in nyc_taxi.raw_trips that do
8+
-- 6c. Find orphaned pickup IDs: pickup_location_id values in nyc_taxi.raw_trips that do
99
-- not exist in nyc_taxi.raw_zones.
1010
--
1111
-- Dataset: nyc_taxi.raw_trips (~57K green-taxi rows, Jan 2024) and nyc_taxi.raw_zones (265 rows).
@@ -15,15 +15,28 @@
1515
-- HAVING COUNT(*) > 1. For orphans, a LEFT JOIN to nyc_taxi.raw_zones with
1616
-- WHERE z.location_id IS NULL surfaces the unmatched IDs.
1717

18-
-- 5a. Trips with a missing pickup location
18+
-- 6a. Trips with a missing pickup location
1919
-- TODO: count rows where pickup_location_id IS NULL.
20-
-- Expect 0 here: the pickup IDs are complete. A 0 is the check passing,
21-
-- not a mistake. The real dirt is duplicates (5b) and negative fares.
20+
-- Expect 5 here: some pickup IDs are intentionally NULL. The real dirt is
21+
-- duplicates (6b), negative fares, and orphans (6c).
22+
-- Starter skeleton:
23+
SELECT COUNT(*) FROM nyc_taxi.raw_trips WHERE 1=0; -- replace 1=0 with filter
2224

2325

24-
-- 5b. Duplicate trips (same vendor + pickup + dropoff time)
26+
-- 6b. Duplicate trips (same vendor + pickup + dropoff time)
2527
-- TODO: group by the three columns and keep groups with more than one row.
28+
-- Starter skeleton:
29+
SELECT vendor_id, pickup_datetime, dropoff_datetime, COUNT(*)
30+
FROM nyc_taxi.raw_trips
31+
GROUP BY 1, 2, 3
32+
LIMIT 1; -- replace LIMIT with HAVING count filter
2633

2734

28-
-- 5c. Orphaned pickup IDs not present in nyc_taxi.raw_zones
35+
-- 6c. Orphaned pickup IDs not present in nyc_taxi.raw_zones
2936
-- TODO: LEFT JOIN nyc_taxi.raw_trips to nyc_taxi.raw_zones and keep rows with no matching zone.
37+
-- Starter skeleton:
38+
SELECT DISTINCT t.pickup_location_id
39+
FROM nyc_taxi.raw_trips t
40+
LEFT JOIN nyc_taxi.raw_zones z ON t.pickup_location_id = z.location_id
41+
LIMIT 1; -- replace LIMIT with WHERE clause to find unmatched zones
42+
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
-- Exercise 5 solution: Validate the raw data
1+
-- Exercise 6 solution: Validate the raw data
22

3-
-- 5a. Trips with a missing pickup location
3+
-- 6a. Trips with a missing pickup location
44
SELECT COUNT(*) AS null_pickup_location
55
FROM nyc_taxi.raw_trips t
66
WHERE t.pickup_location_id IS NULL;
77
-- WHY IS NULL (not = NULL): in SQL, NULL is never equal to anything, even NULL.
88
-- You must test for absence with IS NULL, not with = NULL, or the filter matches nothing.
99

1010

11-
-- 5b. Duplicate trips (same vendor + pickup + dropoff time)
11+
-- 6b. Duplicate trips (same vendor + pickup + dropoff time)
1212
SELECT
1313
t.vendor_id,
1414
t.pickup_datetime,
@@ -20,12 +20,12 @@ HAVING COUNT(*) > 1 -- WHY HAVING (not WHERE): WHERE filters individu
2020
ORDER BY copies DESC;
2121

2222

23-
-- 5c. Orphaned pickup IDs not present in nyc_taxi.raw_zones
23+
-- 6c. Orphaned pickup IDs not present in nyc_taxi.raw_zones
2424
SELECT DISTINCT t.pickup_location_id
2525
FROM nyc_taxi.raw_trips t
2626
LEFT JOIN nyc_taxi.raw_zones z -- WHY LEFT JOIN: keeps every trip even when no zone matches; an INNER JOIN would silently drop the unmatched (orphan) rows we are hunting for
2727
ON t.pickup_location_id = z.location_id
2828
WHERE z.location_id IS NULL -- WHY IS NULL here: after a LEFT JOIN, an unmatched trip has NULL zone columns; filtering on z.location_id IS NULL isolates exactly the orphans
2929
ORDER BY t.pickup_location_id;
3030

31-
-- An empty result for 5b or 5c means the check passed. Any rows returned are the problems to report.
31+
-- Any rows returned for 6b or 6c are the problems to report.
Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
-- Exercise 6: Build views, then query them
1+
-- Exercise 7: Build views, then query them
22
--
33
-- Wrap the cleaned-up logic in two views, then query them. This is exactly the
44
-- star-schema deliverable the assignment asks for, scaled down to practice once.
5-
-- 6a. Create vw_dim_zones from nyc_taxi.raw_zones and vw_fact_trips from nyc_taxi.raw_trips,
5+
-- 7a. Create vw_dim_zones from nyc_taxi.raw_zones and vw_fact_trips from nyc_taxi.raw_trips,
66
-- excluding rows where fare_amount is negative.
7-
-- 6b. Using your views, find which borough had the highest total fare revenue.
8-
-- 6c. Using your views, find the top 5 pickup zones by trip count.
7+
-- 7b. Using your views, find which borough had the highest total fare revenue.
8+
-- 7c. Using your views, find the top 5 pickup zones by trip count.
99
--
1010
-- Dataset: nyc_taxi.raw_trips (~57K green-taxi rows, Jan 2024) and nyc_taxi.raw_zones (265 rows).
1111
-- Run these against your OWN schema on the shared Azure PostgreSQL, not public.
12-
-- NOTE: 6a creates views in YOUR OWN schema. CREATE OR REPLACE VIEW is safe to re-run.
12+
-- NOTE: 7a creates views in YOUR OWN schema. CREATE OR REPLACE VIEW is safe to re-run.
1313
-- NOTE: this practice view is scaled down. The Week 9 assignment's vw_fact_trips
1414
-- also casts pickup_datetime to a TIMESTAMP (pickup_datetime::TIMESTAMP).
1515
-- Add that cast when you build the assignment version.
@@ -19,14 +19,33 @@
1919
-- vw_fact_trips.pickup_location_id = vw_dim_zones.location_id for any
2020
-- name-level breakdown.
2121

22-
-- 6a. The two views
22+
-- 7a. The two views
2323
-- TODO: CREATE OR REPLACE VIEW vw_dim_zones from nyc_taxi.raw_zones.
2424
-- TODO: CREATE OR REPLACE VIEW vw_fact_trips from nyc_taxi.raw_trips, excluding negative fares.
25+
-- Starter skeletons:
26+
CREATE OR REPLACE VIEW vw_dim_zones AS
27+
SELECT 1; -- replace with columns from raw_zones
2528

29+
CREATE OR REPLACE VIEW vw_fact_trips AS
30+
SELECT 1; -- replace with columns from raw_trips and filter negative fares
2631

27-
-- 6b. Highest total fare revenue by borough
32+
33+
-- 7b. Highest total fare revenue by borough
2834
-- TODO: join the two views, sum fare per borough, and return the top borough.
35+
-- Starter skeleton:
36+
SELECT d.borough, SUM(f.fare_amount) AS total_revenue
37+
FROM vw_fact_trips f
38+
INNER JOIN vw_dim_zones d ON f.pickup_location_id = d.location_id
39+
GROUP BY 1
40+
LIMIT 1; -- replace with correct ORDER BY and final touches
2941

3042

31-
-- 6c. Top 5 pickup zones by trip count
43+
-- 7c. Top 5 pickup zones by trip count
3244
-- TODO: join the two views, count trips per zone, and return the top 5 zones.
45+
-- Starter skeleton:
46+
SELECT d.zone, COUNT(*) AS trips
47+
FROM vw_fact_trips f
48+
INNER JOIN vw_dim_zones d ON f.pickup_location_id = d.location_id
49+
GROUP BY 1
50+
LIMIT 5; -- replace with correct ORDER BY and final touches
51+

data-track/week-9/exercise_7/solutions/exercise.sql

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
-- Exercise 6 solution: Build views, then query them
1+
-- Exercise 7 solution: Build views, then query them
22

3-
-- 6a. The two views
3+
-- 7a. The two views
44
CREATE OR REPLACE VIEW vw_dim_zones AS -- WHY CREATE OR REPLACE: lets you re-run this script while iterating without first DROPping the view; a plain CREATE VIEW would error if the view already exists
55
SELECT
66
location_id,
@@ -14,7 +14,7 @@ FROM nyc_taxi.raw_trips
1414
WHERE fare_amount >= 0; -- WHY filter in the view: the cleaning rule (no negative fares) lives in one place, so every query against the view is automatically clean
1515

1616

17-
-- 6b. Highest total fare revenue by borough
17+
-- 7b. Highest total fare revenue by borough
1818
SELECT
1919
d.borough,
2020
ROUND(SUM(f.fare_amount)::numeric, 2) AS total_revenue -- WHY SUM: revenue is the total of all fares in the borough, not an average
@@ -26,7 +26,7 @@ ORDER BY total_revenue DESC
2626
LIMIT 1; -- WHY LIMIT 1: the question asks for the single highest-revenue borough
2727

2828

29-
-- 6c. Top 5 pickup zones by trip count
29+
-- 7c. Top 5 pickup zones by trip count
3030
SELECT
3131
d.zone,
3232
COUNT(*) AS trips

0 commit comments

Comments
 (0)