Colab Practice: Problem Solving with SQL
Step 0: PostgreSQL Playground
Open PostgreSQL Colab Playground
Instructions:
-
Run the setup cell to create the
Users,Songs, andListenstables. -
Run
sql-practice-dataset.sqlto add the extended songs, listens, andRecommendationstable. -
Run one
SELECTto confirm the tables loaded.
Practice 1: Writing Debug Tables
Creating a Debug Table forces you to evaluate logical execution ordering: FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT.
Tip: Check your work: All answers and interactive logic traces are available in the Practice Solutions Colab.
1.1: Filtering Strategies (JOIN vs IN vs EXISTS)
Analyze these four queries. Do they all pull the same genres? Which is the most efficient for checking existence in a large dataset?
-- Query 1: INNER JOIN
SELECT DISTINCT s.genre FROM Songs s
INNER JOIN Listens l ON s.song_id = l.song_id;
-- Query 2: INNER JOIN (No Distinct - what happens?)
SELECT s.genre FROM Songs s
INNER JOIN Listens l ON s.song_id = l.song_id;
-- Query 3: IN Clause
SELECT DISTINCT s.genre FROM Songs s
WHERE s.song_id IN (SELECT song_id FROM Listens);
-- Query 4: EXISTS Clause
SELECT DISTINCT s.genre FROM Songs s
WHERE EXISTS (SELECT 1 FROM Listens l WHERE l.song_id = s.song_id);
1.2: Set Operations (UNION vs. EXCEPT)
Mickey (user 1) has a listening history and a set of recommendations. Load sql-practice-dataset.sql so the Recommendations table exists (see The Practice Dataset below), then compare the genres he plays against the genres he is recommended.
-- Query 5: Mickey's Listening History
SELECT s.genre FROM Songs s INNER JOIN Listens l ON s.song_id = l.song_id WHERE l.user_id = 1;
-- Query 6: Mickey's Recommendations
SELECT s.genre FROM Songs s INNER JOIN Recommendations r ON s.song_id = r.song_id WHERE r.user_id = 1;
-- Query 7: UNION (History OR Recommendations)
SELECT s.genre FROM Songs s INNER JOIN Listens l ON s.song_id = l.song_id WHERE l.user_id = 1
UNION
SELECT s.genre FROM Songs s INNER JOIN Recommendations r ON s.song_id = r.song_id WHERE r.user_id = 1;
-- Query 8: EXCEPT (History BUT NOT recommended)
SELECT s.genre FROM Songs s INNER JOIN Listens l ON s.song_id = l.song_id WHERE l.user_id = 1
EXCEPT
SELECT s.genre FROM Songs s INNER JOIN Recommendations r ON s.song_id = r.song_id WHERE r.user_id = 1;
1.3: The LEFT JOIN
Check this query for User 1. Does it yield more or fewer rows than an INNER JOIN? Why?
-- Query 9: All genres, matched against Mickey's listens
SELECT DISTINCT s.genre
FROM Songs s
LEFT JOIN Listens l ON s.song_id = l.song_id AND l.user_id = 1;
The Practice Dataset
The advanced challenges run on an extended dataset: it adds songs, listens, and a Recommendations table on top of the canonical 4 users, 10 songs, and 9 listens. Load it by running sql-practice-dataset.sql in the Colab after the base tables. The canonical rows are unchanged, so the four users stand and Pluto is still the only user with no listens.
Added Songs (song_id 11 to 18)
| song_id | title | artist | genre |
|---|---|---|---|
| 11 | Anti-Hero | Taylor Swift | Pop |
| 12 | Cruel Summer | Taylor Swift | Pop |
| 13 | Perfect | Ed Sheeran | Rock |
| 14 | Castle on the Hill | Ed Sheeran | Rock |
| 15 | Let It Be | Beatles | Classic |
| 16 | Come Together | Beatles | Classic |
| 17 | Hello | Adele | Pop |
| 18 | Rolling in the Deep | Adele | Pop |
Genres stay consistent with the base data: Taylor Swift is Pop (Bad Blood, song 9, is her one Rock track), Ed Sheeran is Rock, the Beatles are Classic, and Adele joins as a second Pop artist. The added listens come from users 1 to 3, so Pluto keeps zero listens; run qry_count_songs in the Colab to see each song's totals.
Recommendations (a new table)
Recommendations(user_id, song_id) records songs the system suggests to a user. Queries 5 to 8 in 1.2 read Mickey's rows:
| user_id | song_id | title | genre |
|---|---|---|---|
| 1 | 3 | Shape of You | Rock |
| 1 | 9 | Bad Blood | Rock |
| 1 | 11 | Anti-Hero | Pop |
| 1 | 17 | Hello | Pop |
Practice 2: Building Intuition with Window Functions
Goal: Compare near-identical ranking queries and see why their outputs differ.
2.1: The Base Count (qry_count_songs)
Find the total listens for each song, grouped by genre, song ID, and title. This is your baseline for ranking queries.
2.2: Ranking by Genre (qry_top_songs_genre)
Identify the top-k ranked songs per genre based on listens (try k = 1, 2, and 3).
- How does competition shift when ranking within a genre versus the entire dataset?
2.3: Ranking by Artist (qry_top_songs_artist)
Identify the top-k ranked songs per artist based on listens.
- If an artist has multiple songs, which one leads?
2.4: Finding the Global Peak (qry_max_listens)
Find the song with the highest number of listens across the platform.
2.5: The Artist Champion (qry_max_listens_artist)
Identify the song with the most listens for each artist.
- This is a "Top-1 per Artist" query. Compare this to 2.3.
2.6: Multi-Dimensional Ranking (qry_top_songs_genre_artist)
Find the top-k ranked songs per (genre, artist) based on listens.
- Observe how the partitions shrink. What happens to the rank values?
Challenge: When Do These Match?
Run these two queries in the Colab and compare their row counts.
-- Query A: Rank 1 per Genre
runSql('Query top_songs_genre', qry_top_songs_genre.format(k=1))
-- Query B: Rank 1 per (Genre, Artist)
runSql('Query top_songs_genre_artist', qry_top_songs_genre_artist.format(k=1))
Query A returns one song per genre (3 rows). Query B returns one per (genre, artist), so it returns more (5 rows): Pop has both Taylor Swift and Adele, and Rock has both Ed Sheeran and Taylor Swift, so each of those genres yields two rows.
-
The two queries agree only on a genre with a single artist. Classic (Beatles only) is the one genre where Query A and Query B return the same song.
-
What would you change in the data to make Query A and Query B return identical rows for every genre?