Skip to content
8 changes: 7 additions & 1 deletion data-track/week-9/exercise_1/exercise.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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.
9 changes: 8 additions & 1 deletion data-track/week-9/exercise_2/exercise.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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.
9 changes: 8 additions & 1 deletion data-track/week-9/exercise_3/exercise.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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.
12 changes: 11 additions & 1 deletion data-track/week-9/exercise_4/exercise.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;
7 changes: 6 additions & 1 deletion data-track/week-9/exercise_5/exercise.sql
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;

2 changes: 1 addition & 1 deletion data-track/week-9/exercise_5/solutions/exercise.sql
Original file line number Diff line number Diff line change
@@ -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
Expand Down
31 changes: 22 additions & 9 deletions data-track/week-9/exercise_6/exercise.sql
Original file line number Diff line number Diff line change
@@ -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).
Expand All @@ -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

10 changes: 5 additions & 5 deletions data-track/week-9/exercise_6/solutions/exercise.sql
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
-- 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;
-- WHY IS NULL (not = NULL): in SQL, NULL is never equal to anything, even 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,
Expand All @@ -20,12 +20,12 @@ 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
ON t.pickup_location_id = z.location_id
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.
35 changes: 27 additions & 8 deletions data-track/week-9/exercise_7/exercise.sql
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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

8 changes: 4 additions & 4 deletions data-track/week-9/exercise_7/solutions/exercise.sql
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading