diff --git a/data-track/week-9/exercise_1/exercise.sql b/data-track/week-9/exercise_1/exercise.sql index d6bd727..d25f960 100644 --- a/data-track/week-9/exercise_1/exercise.sql +++ b/data-track/week-9/exercise_1/exercise.sql @@ -14,4 +14,10 @@ -- five rows are the same every run (a bare LIMIT returns an arbitrary -- sample that can change between runs). --- TODO: select the four columns and join nyc_taxi.raw_trips to nyc_taxi.raw_zones, then limit to 5 rows. +-- Starter query: exploring raw_trips columns +SELECT pickup_datetime, trip_distance, fare_amount, pickup_location_id +FROM nyc_taxi.raw_trips +ORDER BY pickup_datetime +LIMIT 5; + +-- 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. diff --git a/data-track/week-9/exercise_2/exercise.sql b/data-track/week-9/exercise_2/exercise.sql index 025925a..2a48ea9 100644 --- a/data-track/week-9/exercise_2/exercise.sql +++ b/data-track/week-9/exercise_2/exercise.sql @@ -11,4 +11,11 @@ -- Hint: Join to nyc_taxi.raw_zones to get borough, then GROUP BY z.borough. Every -- non-aggregated column in the SELECT must appear in the GROUP BY. --- TODO: join nyc_taxi.raw_trips to nyc_taxi.raw_zones, group by borough, and count trips plus average fare. +-- Starter query: counting raw trips by location ID (no join) +SELECT t.pickup_location_id, COUNT(*) AS trips, AVG(t.fare_amount) AS avg_fare +FROM nyc_taxi.raw_trips t +GROUP BY t.pickup_location_id +ORDER BY trips DESC +LIMIT 5; + +-- TODO: rewrite the query above to join to nyc_taxi.raw_zones, group by the borough name, and order by total trips descending. diff --git a/data-track/week-9/exercise_3/exercise.sql b/data-track/week-9/exercise_3/exercise.sql index bc9cd8d..c4c6342 100644 --- a/data-track/week-9/exercise_3/exercise.sql +++ b/data-track/week-9/exercise_3/exercise.sql @@ -11,4 +11,11 @@ -- Hint: Use pickup_datetime::date to drop the time part. Define the daily counts -- in a WITH block, then ORDER BY ... DESC LIMIT 1 over the CTE. --- TODO: define a CTE of daily trip counts, then select the single busiest day from it. +-- Starter query: counting trips per day without a CTE +SELECT t.pickup_datetime::date AS trip_date, COUNT(*) AS trips +FROM nyc_taxi.raw_trips t +GROUP BY trip_date +ORDER BY trips DESC +LIMIT 5; + +-- TODO: rewrite the query above to use a CTE (WITH block) containing the daily counts, and select the single busiest day from the CTE. diff --git a/data-track/week-9/exercise_4/exercise.sql b/data-track/week-9/exercise_4/exercise.sql index a946718..e0382f2 100644 --- a/data-track/week-9/exercise_4/exercise.sql +++ b/data-track/week-9/exercise_4/exercise.sql @@ -29,4 +29,14 @@ FROM ( GROUP BY borough ORDER BY avg_fare DESC; --- TODO: rewrite the query above using CTEs (one to filter, one to join, then aggregate). +-- Starter skeleton: refactoring into CTEs +WITH positive_trips AS ( + -- TODO: select trips where fare_amount > 0 from nyc_taxi.raw_trips + SELECT * FROM nyc_taxi.raw_trips LIMIT 1 +), +joined_trips AS ( + -- TODO: join positive_trips to nyc_taxi.raw_zones + SELECT * FROM positive_trips +) +-- TODO: final group by and aggregate over joined_trips +SELECT 1; diff --git a/data-track/week-9/exercise_5/exercise.sql b/data-track/week-9/exercise_5/exercise.sql index 439e9ac..f792da2 100644 --- a/data-track/week-9/exercise_5/exercise.sql +++ b/data-track/week-9/exercise_5/exercise.sql @@ -1,4 +1,4 @@ --- Exercise 7 (stretch): Compare a cartesian join to a filtered join +-- Exercise 5 (stretch): Compare a cartesian join to a filtered join -- -- A missing join condition produces a cartesian product: every trip matched to -- every zone. Use EXPLAIN to see how the planner treats the two queries @@ -16,7 +16,12 @@ -- Cartesian product: no join condition -- TODO: EXPLAIN a CROSS JOIN of nyc_taxi.raw_trips and nyc_taxi.raw_zones (no ON clause). +-- Starter skeleton: (uncomment and prepend EXPLAIN) +-- SELECT * FROM nyc_taxi.raw_trips t CROSS JOIN nyc_taxi.raw_zones z; -- Filtered join: one zone per trip -- TODO: EXPLAIN an INNER JOIN of nyc_taxi.raw_trips and nyc_taxi.raw_zones on pickup_location_id = location_id. +-- Starter skeleton: (uncomment and prepend EXPLAIN) +-- SELECT * FROM nyc_taxi.raw_trips t INNER JOIN nyc_taxi.raw_zones z ON t.pickup_location_id = z.location_id; + diff --git a/data-track/week-9/exercise_5/solutions/exercise.sql b/data-track/week-9/exercise_5/solutions/exercise.sql index 1b72745..fdba4f5 100644 --- a/data-track/week-9/exercise_5/solutions/exercise.sql +++ b/data-track/week-9/exercise_5/solutions/exercise.sql @@ -1,4 +1,4 @@ --- Exercise 7 solution (stretch): Compare a cartesian join to a filtered join +-- Exercise 5 solution (stretch): Compare a cartesian join to a filtered join -- Cartesian product: no join condition, ~57K trips x 265 zones 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 diff --git a/data-track/week-9/exercise_6/exercise.sql b/data-track/week-9/exercise_6/exercise.sql index 627e10b..36a87a4 100644 --- a/data-track/week-9/exercise_6/exercise.sql +++ b/data-track/week-9/exercise_6/exercise.sql @@ -1,11 +1,11 @@ --- Exercise 5: Validate the raw data +-- Exercise 6: Validate the raw data -- -- Raw data is rarely clean. Write three checks that the assignment's audit task -- expects you to run: --- 5a. Count trips with a NULL pickup_location_id. --- 5b. Find duplicate trips: rows that share the same vendor_id, pickup_datetime, +-- 6a. Count trips with a NULL pickup_location_id. +-- 6b. Find duplicate trips: rows that share the same vendor_id, pickup_datetime, -- and dropoff_datetime. --- 5c. Find orphaned pickup IDs: pickup_location_id values in nyc_taxi.raw_trips that do +-- 6c. Find orphaned pickup IDs: pickup_location_id values in nyc_taxi.raw_trips that do -- not exist in nyc_taxi.raw_zones. -- -- Dataset: nyc_taxi.raw_trips (~57K green-taxi rows, Jan 2024) and nyc_taxi.raw_zones (265 rows). @@ -15,15 +15,28 @@ -- HAVING COUNT(*) > 1. For orphans, a LEFT JOIN to nyc_taxi.raw_zones with -- WHERE z.location_id IS NULL surfaces the unmatched IDs. --- 5a. Trips with a missing pickup location +-- 6a. Trips with a missing pickup location -- TODO: count rows where pickup_location_id IS NULL. --- Expect 0 here: the pickup IDs are complete. A 0 is the check passing, --- not a mistake. The real dirt is duplicates (5b) and negative fares. +-- Expect 5 here: some pickup IDs are intentionally NULL. The real dirt is +-- duplicates (6b), negative fares, and orphans (6c). +-- Starter skeleton: +SELECT COUNT(*) FROM nyc_taxi.raw_trips WHERE 1=0; -- replace 1=0 with filter --- 5b. Duplicate trips (same vendor + pickup + dropoff time) +-- 6b. Duplicate trips (same vendor + pickup + dropoff time) -- TODO: group by the three columns and keep groups with more than one row. +-- Starter skeleton: +SELECT vendor_id, pickup_datetime, dropoff_datetime, COUNT(*) +FROM nyc_taxi.raw_trips +GROUP BY 1, 2, 3 +LIMIT 1; -- replace LIMIT with HAVING count filter --- 5c. Orphaned pickup IDs not present in nyc_taxi.raw_zones +-- 6c. Orphaned pickup IDs not present in nyc_taxi.raw_zones -- TODO: LEFT JOIN nyc_taxi.raw_trips to nyc_taxi.raw_zones and keep rows with no matching zone. +-- Starter skeleton: +SELECT DISTINCT t.pickup_location_id +FROM nyc_taxi.raw_trips t +LEFT JOIN nyc_taxi.raw_zones z ON t.pickup_location_id = z.location_id +LIMIT 1; -- replace LIMIT with WHERE clause to find unmatched zones + diff --git a/data-track/week-9/exercise_6/solutions/exercise.sql b/data-track/week-9/exercise_6/solutions/exercise.sql index d558fb4..b31c3a2 100644 --- a/data-track/week-9/exercise_6/solutions/exercise.sql +++ b/data-track/week-9/exercise_6/solutions/exercise.sql @@ -1,6 +1,6 @@ --- Exercise 5 solution: Validate the raw data +-- Exercise 6 solution: Validate the raw data --- 5a. Trips with a missing pickup location +-- 6a. Trips with a missing pickup location SELECT COUNT(*) AS null_pickup_location FROM nyc_taxi.raw_trips t WHERE t.pickup_location_id IS NULL; @@ -8,7 +8,7 @@ WHERE t.pickup_location_id IS NULL; -- You must test for absence with IS NULL, not with = NULL, or the filter matches nothing. --- 5b. Duplicate trips (same vendor + pickup + dropoff time) +-- 6b. Duplicate trips (same vendor + pickup + dropoff time) SELECT t.vendor_id, t.pickup_datetime, @@ -20,7 +20,7 @@ HAVING COUNT(*) > 1 -- WHY HAVING (not WHERE): WHERE filters individu ORDER BY copies DESC; --- 5c. Orphaned pickup IDs not present in nyc_taxi.raw_zones +-- 6c. Orphaned pickup IDs not present in nyc_taxi.raw_zones SELECT DISTINCT t.pickup_location_id FROM nyc_taxi.raw_trips t 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 @@ -28,4 +28,4 @@ LEFT JOIN nyc_taxi.raw_zones z -- WHY LEFT JOIN: keeps every trip even 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 ORDER BY t.pickup_location_id; --- An empty result for 5b or 5c means the check passed. Any rows returned are the problems to report. +-- Any rows returned for 6b or 6c are the problems to report. diff --git a/data-track/week-9/exercise_7/exercise.sql b/data-track/week-9/exercise_7/exercise.sql index 688410e..3758772 100644 --- a/data-track/week-9/exercise_7/exercise.sql +++ b/data-track/week-9/exercise_7/exercise.sql @@ -1,15 +1,15 @@ --- Exercise 6: Build views, then query them +-- Exercise 7: Build views, then query them -- -- Wrap the cleaned-up logic in two views, then query them. This is exactly the -- star-schema deliverable the assignment asks for, scaled down to practice once. --- 6a. Create vw_dim_zones from nyc_taxi.raw_zones and vw_fact_trips from nyc_taxi.raw_trips, +-- 7a. Create vw_dim_zones from nyc_taxi.raw_zones and vw_fact_trips from nyc_taxi.raw_trips, -- excluding rows where fare_amount is negative. --- 6b. Using your views, find which borough had the highest total fare revenue. --- 6c. Using your views, find the top 5 pickup zones by trip count. +-- 7b. Using your views, find which borough had the highest total fare revenue. +-- 7c. Using your views, find the top 5 pickup zones by trip count. -- -- Dataset: nyc_taxi.raw_trips (~57K green-taxi rows, Jan 2024) and nyc_taxi.raw_zones (265 rows). -- Run these against your OWN schema on the shared Azure PostgreSQL, not public. --- NOTE: 6a creates views in YOUR OWN schema. CREATE OR REPLACE VIEW is safe to re-run. +-- NOTE: 7a creates views in YOUR OWN schema. CREATE OR REPLACE VIEW is safe to re-run. -- NOTE: this practice view is scaled down. The Week 9 assignment's vw_fact_trips -- also casts pickup_datetime to a TIMESTAMP (pickup_datetime::TIMESTAMP). -- Add that cast when you build the assignment version. @@ -19,14 +19,33 @@ -- vw_fact_trips.pickup_location_id = vw_dim_zones.location_id for any -- name-level breakdown. --- 6a. The two views +-- 7a. The two views -- TODO: CREATE OR REPLACE VIEW vw_dim_zones from nyc_taxi.raw_zones. -- TODO: CREATE OR REPLACE VIEW vw_fact_trips from nyc_taxi.raw_trips, excluding negative fares. +-- Starter skeletons: +CREATE OR REPLACE VIEW vw_dim_zones AS +SELECT 1; -- replace with columns from raw_zones +CREATE OR REPLACE VIEW vw_fact_trips AS +SELECT 1; -- replace with columns from raw_trips and filter negative fares --- 6b. Highest total fare revenue by borough + +-- 7b. Highest total fare revenue by borough -- TODO: join the two views, sum fare per borough, and return the top borough. +-- Starter skeleton: +SELECT d.borough, SUM(f.fare_amount) AS total_revenue +FROM vw_fact_trips f +INNER JOIN vw_dim_zones d ON f.pickup_location_id = d.location_id +GROUP BY 1 +LIMIT 1; -- replace with correct ORDER BY and final touches --- 6c. Top 5 pickup zones by trip count +-- 7c. Top 5 pickup zones by trip count -- TODO: join the two views, count trips per zone, and return the top 5 zones. +-- Starter skeleton: +SELECT d.zone, COUNT(*) AS trips +FROM vw_fact_trips f +INNER JOIN vw_dim_zones d ON f.pickup_location_id = d.location_id +GROUP BY 1 +LIMIT 5; -- replace with correct ORDER BY and final touches + diff --git a/data-track/week-9/exercise_7/solutions/exercise.sql b/data-track/week-9/exercise_7/solutions/exercise.sql index ea6f9ab..0b749ef 100644 --- a/data-track/week-9/exercise_7/solutions/exercise.sql +++ b/data-track/week-9/exercise_7/solutions/exercise.sql @@ -1,6 +1,6 @@ --- Exercise 6 solution: Build views, then query them +-- Exercise 7 solution: Build views, then query them --- 6a. The two views +-- 7a. The two views 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 SELECT location_id, @@ -14,7 +14,7 @@ FROM nyc_taxi.raw_trips 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 --- 6b. Highest total fare revenue by borough +-- 7b. Highest total fare revenue by borough SELECT d.borough, 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 LIMIT 1; -- WHY LIMIT 1: the question asks for the single highest-revenue borough --- 6c. Top 5 pickup zones by trip count +-- 7c. Top 5 pickup zones by trip count SELECT d.zone, COUNT(*) AS trips