SQL Curriculum Roadmap
Master SaaS Analytics with our complete, structured 60-step curriculum. Click any lesson below to explore its foundational theory, practical real-world scenario, and solution schema. Ready to practice? Jump directly into our interactive playground!
Beginner Roadmap
Lessons 1 - 201Retrieving All Columns
Retrieving All Columns
Theory & Lesson Concept
Practical Scenario
Retrieve all information from the users table to get a high-level overview of the data we collect during onboarding.
Solution Schema
SELECT * FROM users;2Retrieving Specific Columns
Retrieving Specific Columns
Theory & Lesson Concept
Practical Scenario
We need to build a mailing list for an upcoming product update. Retrieve just the name and email columns from the users table.
Solution Schema
SELECT name, email FROM users;3Sorting Data with ORDER BY
Sorting Data with ORDER BY
Theory & Lesson Concept
Practical Scenario
Find out who our newest customers are by retrieving all records from the subscriptions table sorted by the signup_date from newest to oldest.
Solution Schema
SELECT * FROM subscriptions ORDER BY signup_date DESC;4Limiting Results
Limiting Results
Theory & Lesson Concept
Practical Scenario
We want to quickly inspect the format of our customer support logs. Retrieve just the first 5 records from the support_tickets table.
Solution Schema
SELECT * FROM support_tickets LIMIT 5;5Filtering Data with WHERE
Filtering Data with WHERE
Theory & Lesson Concept
Practical Scenario
Identify all users who have canceled their service. Retrieve all columns from the subscriptions table where the status is 'canceled'.
Solution Schema
SELECT * FROM subscriptions WHERE status = 'canceled';6Combining Conditions with AND
Combining Conditions with AND
Theory & Lesson Concept
Practical Scenario
Find all customer support tickets where the status is 'open' and the priority is 'high' in the support_tickets table.
Solution Schema
SELECT * FROM support_tickets WHERE status = 'open' AND priority = 'high';7Broadening Searches with OR
Broadening Searches with OR
Theory & Lesson Concept
Practical Scenario
Retrieve all users from the users table whose role is either 'admin' or 'editor'.
Solution Schema
SELECT * FROM users WHERE role = 'admin' OR role = 'editor';8Managing Evaluation Order with Parentheses
Managing Evaluation Order with Parentheses
Theory & Lesson Concept
Practical Scenario
Find all subscriptions that are either 'canceled' or 'paused', but ONLY if they were created in the year 2026.
Solution Schema
SELECT * FROM subscriptions WHERE (status = 'canceled' OR status = 'paused') AND signup_date BETWEEN '2026-01-01' AND '2026-12-31';9Filtering Ranges with BETWEEN
Filtering Ranges with BETWEEN
Theory & Lesson Concept
Practical Scenario
Retrieve all records from the subscriptions table where the monthly_price is between 50 and 100 dollars.
Solution Schema
SELECT * FROM subscriptions WHERE monthly_price BETWEEN 50 AND 100;10Matching a List of Values with IN
Matching a List of Values with IN
Theory & Lesson Concept
Practical Scenario
Retrieve all users from the users table whose country is in a list of 'USA', 'Canada', or 'UK'.
Solution Schema
SELECT * FROM users WHERE country IN ('USA', 'Canada', 'UK');11Counting Rows with COUNT
Counting Rows with COUNT
Theory & Lesson Concept
Practical Scenario
Calculate the total number of registered users in our database to understand our current user base size.
Solution Schema
SELECT COUNT(*) FROM users;12Summing Values with SUM
Summing Values with SUM
Theory & Lesson Concept
Practical Scenario
The finance team needs to know the total revenue generated so far. Calculate the sum of all amounts in the payments table.
Solution Schema
SELECT SUM(amount) FROM payments;13Calculating Averages with AVG
Calculating Averages with AVG
Theory & Lesson Concept
Practical Scenario
Find the average payment amount to understand our typical customer transaction value.
Solution Schema
SELECT AVG(amount) FROM payments;14Finding Minimum Values with MIN
Finding Minimum Values with MIN
Theory & Lesson Concept
Practical Scenario
Find the earliest signup date among all our subscriptions to determine when our platform first launched.
Solution Schema
SELECT MIN(signup_date) FROM subscriptions;15Finding Maximum Values with MAX
Finding Maximum Values with MAX
Theory & Lesson Concept
Practical Scenario
Identify the most recent support ticket created to see the latest customer inquiry.
Solution Schema
SELECT MAX(created_at) FROM support_tickets;16Pattern Matching with LIKE
Pattern Matching with LIKE
Theory & Lesson Concept
Practical Scenario
Find all users who signed up with a Gmail address so we can analyze our user email providers.
Solution Schema
SELECT * FROM users WHERE email LIKE '%@gmail.com';17Case-Insensitive Matching with ILIKE
Case-Insensitive Matching with ILIKE
Theory & Lesson Concept
Practical Scenario
Find all support tickets that mention 'billing' in their description, regardless of capitalization.
Solution Schema
SELECT * FROM support_tickets WHERE description ILIKE '%billing%';18Single Character Wildcards with '_'
Single Character Wildcards with '_'
Theory & Lesson Concept
Practical Scenario
Retrieve all payments where the invoice code follows the format 'INV-202X' where X is any single character.
Solution Schema
SELECT * FROM payments WHERE invoice_code LIKE 'INV-202_';19Aliasing Columns with AS
Aliasing Columns with AS
Theory & Lesson Concept
Practical Scenario
Count all support tickets and name the resulting column 'total_tickets' for a cleaner report.
Solution Schema
SELECT COUNT(*) AS total_tickets FROM support_tickets;20Aliasing Tables with AS
Aliasing Tables with AS
Theory & Lesson Concept
Practical Scenario
Retrieve all columns from the subscriptions table using 'sub' as a table alias.
Solution Schema
SELECT * FROM subscriptions AS sub;Intermediate Roadmap
Lessons 21 - 3521Introduction to INNER JOIN
Introduction to INNER JOIN
Theory & Lesson Concept
Practical Scenario
Retrieve the name of the user and their subscription status by joining the users and subscriptions tables.
Solution Schema
SELECT users.name, subscriptions.status FROM users INNER JOIN subscriptions ON users.user_id = subscriptions.user_id;22Using Table Aliases with Joins
Using Table Aliases with Joins
Theory & Lesson Concept
Practical Scenario
Join users and payments to find the email of the user and the amount paid, assigning 'u' and 'p' as table aliases.
Solution Schema
SELECT u.email, p.amount FROM users u INNER JOIN payments p ON u.user_id = p.user_id;23Joining Multiple Tables
Joining Multiple Tables
Theory & Lesson Concept
Practical Scenario
Retrieve the user's name, their subscription tier, and the invoice_code from payments by joining users, subscriptions, and payments.
Solution Schema
SELECT u.name, s.tier, p.invoice_code FROM users u INNER JOIN subscriptions s ON u.user_id = s.user_id INNER JOIN payments p ON u.user_id = p.user_id;24Filtering Joined Data
Filtering Joined Data
Theory & Lesson Concept
Practical Scenario
Find the email addresses of all users who have submitted a 'high' priority support ticket.
Solution Schema
SELECT u.email FROM users u INNER JOIN support_tickets st ON u.user_id = st.user_id WHERE st.priority = 'high';25Joining on Multiple Conditions
Joining on Multiple Conditions
Theory & Lesson Concept
Practical Scenario
Join users and subscriptions on user_id, but only join the records if the subscription status is 'active' right inside the ON clause.
Solution Schema
SELECT u.name, s.tier FROM users u INNER JOIN subscriptions s ON u.user_id = s.user_id AND s.status = 'active';26Preserving Rows with LEFT JOIN
Preserving Rows with LEFT JOIN
Theory & Lesson Concept
Practical Scenario
Retrieve a list of all users and their subscription status, ensuring users without a subscription are still included in the result.
Solution Schema
SELECT u.name, s.status FROM users u LEFT JOIN subscriptions s ON u.user_id = s.user_id;27Finding Missing Data with LEFT JOIN
Finding Missing Data with LEFT JOIN
Theory & Lesson Concept
Practical Scenario
Identify users who have never made a payment by returning user emails where the payment_id is NULL.
Solution Schema
SELECT u.email FROM users u LEFT JOIN payments p ON u.user_id = p.user_id WHERE p.payment_id IS NULL;28Understanding RIGHT JOIN
Understanding RIGHT JOIN
Theory & Lesson Concept
Practical Scenario
Retrieve all support tickets and the associated user's name, returning the ticket even if the user record has been deleted or is missing.
Solution Schema
SELECT u.name, st.ticket_id FROM users u RIGHT JOIN support_tickets st ON u.user_id = st.user_id;29Aggregating Data with GROUP BY
Aggregating Data with GROUP BY
Theory & Lesson Concept
Practical Scenario
Calculate the total revenue generated per user_id by summing the amount from the payments table.
Solution Schema
SELECT user_id, SUM(amount) FROM payments GROUP BY user_id;30Grouping by Multiple Columns
Grouping by Multiple Columns
Theory & Lesson Concept
Practical Scenario
Count the total number of support tickets grouped by both their priority level and status.
Solution Schema
SELECT priority, status, COUNT(*) FROM support_tickets GROUP BY priority, status;31Filtering Aggregates with HAVING
Filtering Aggregates with HAVING
Theory & Lesson Concept
Practical Scenario
Find all user_ids that have submitted 3 or more support tickets.
Solution Schema
SELECT user_id, COUNT(*) FROM support_tickets GROUP BY user_id HAVING COUNT(*) >= 3;32Combining WHERE and HAVING
Combining WHERE and HAVING
Theory & Lesson Concept
Practical Scenario
Count the number of active subscriptions per tier, but only show tiers that have more than 10 active subscriptions.
Solution Schema
SELECT tier, COUNT(*) FROM subscriptions WHERE status = 'active' GROUP BY tier HAVING COUNT(*) > 10;33Queries Within Queries
Queries Within Queries
Theory & Lesson Concept
Practical Scenario
Retrieve the payment_id and amount for all payments that are greater than the overall average payment amount.
Solution Schema
SELECT payment_id, amount FROM payments WHERE amount > (SELECT AVG(amount) FROM payments);34Filtering with List Subqueries
Filtering with List Subqueries
Theory & Lesson Concept
Practical Scenario
Find the names of all users who have an active subscription by using a subquery on the subscriptions table.
Solution Schema
SELECT name FROM users WHERE user_id IN (SELECT user_id FROM subscriptions WHERE status = 'active');35Using Subqueries in the FROM Clause
Using Subqueries in the FROM Clause
Theory & Lesson Concept
Practical Scenario
Calculate the maximum total amount ever spent by a single user by querying a derived table that sums payments per user.
Solution Schema
SELECT MAX(total_spent) FROM (SELECT user_id, SUM(amount) AS total_spent FROM payments GROUP BY user_id) AS user_totals;36Subqueries in the SELECT Clause
Subqueries in the SELECT Clause
Theory & Lesson Concept
Practical Scenario
Retrieve the invoice_code, the payment amount, and a third column showing the maximum payment amount ever recorded across the whole table.
Solution Schema
SELECT invoice_code, amount, (SELECT MAX(amount) FROM payments) AS max_amount FROM payments;37String Concatenation
String Concatenation
Theory & Lesson Concept
Practical Scenario
Create a formatted string for our marketing team that combines the user's name and email in the format: 'Name (email)'.
Solution Schema
SELECT CONCAT(name, ' (', email, ')') AS formatted_contact FROM users;38Manipulating Dates with EXTRACT
Manipulating Dates with EXTRACT
Theory & Lesson Concept
Practical Scenario
Find out how many subscriptions started in each month by extracting the month from signup_date.
Solution Schema
SELECT EXTRACT(MONTH FROM signup_date) AS signup_month, COUNT(*) FROM subscriptions GROUP BY EXTRACT(MONTH FROM signup_date);39Default Values using COALESCE
Default Values using COALESCE
Theory & Lesson Concept
Practical Scenario
Retrieve user names and their phone numbers. If the phone number is NULL, display the word 'Unknown'.
Solution Schema
SELECT name, COALESCE(phone, 'Unknown') AS phone_number FROM users;40Standardizing Text Case
Standardizing Text Case
Theory & Lesson Concept
Practical Scenario
Retrieve all email addresses from the users table, forcing them to be completely lowercase to ensure standardization.
Solution Schema
SELECT LOWER(email) FROM users;Advanced & SaaS Analytics
Lessons 36 - 6041Organizing Queries with CTEs
Organizing Queries with CTEs
Theory & Lesson Concept
Practical Scenario
Create a CTE named 'active_subs' that retrieves all 'active' subscriptions, then query this CTE to count the total number of active subscriptions per tier.
Solution Schema
WITH active_subs AS (SELECT * FROM subscriptions WHERE status = 'active') SELECT tier, COUNT(*) FROM active_subs GROUP BY tier;42Chaining Multiple CTEs
Chaining Multiple CTEs
Theory & Lesson Concept
Practical Scenario
Define a CTE 'user_revenue' summing payments per user, and a CTE 'user_tickets' counting tickets per user. Join them to show user_id, total revenue, and total tickets.
Solution Schema
WITH user_revenue AS (SELECT user_id, SUM(amount) AS total_rev FROM payments GROUP BY user_id), user_tickets AS (SELECT user_id, COUNT(*) AS total_tix FROM support_tickets GROUP BY user_id) SELECT r.user_id, r.total_rev, t.total_tix FROM user_revenue r INNER JOIN user_tickets t ON r.user_id = t.user_id;43Replacing Subqueries with CTEs
Replacing Subqueries with CTEs
Theory & Lesson Concept
Practical Scenario
Find users who spend above the average. First, define a CTE 'avg_payment' calculating the average payment amount, then select payments greater than that value.
Solution Schema
WITH avg_payment AS (SELECT AVG(amount) AS avg_amt FROM payments) SELECT payment_id, amount FROM payments WHERE amount > (SELECT avg_amt FROM avg_payment);44CTEs for Cohort Analysis
CTEs for Cohort Analysis
Theory & Lesson Concept
Practical Scenario
Create a CTE 'first_payment' that finds the minimum created_at date per user_id in the payments table. Join this to the users table to see each user's name and their first payment date.
Solution Schema
WITH first_payment AS (SELECT user_id, MIN(created_at) AS first_date FROM payments GROUP BY user_id) SELECT u.name, fp.first_date FROM users u INNER JOIN first_payment fp ON u.user_id = fp.user_id;45Introduction to Recursive CTEs
Introduction to Recursive CTEs
Theory & Lesson Concept
Practical Scenario
Generate a sequential list of numbers from 1 to 5 to act as a baseline dimension. Use a recursive CTE named 'num_seq'.
Solution Schema
WITH RECURSIVE num_seq AS (SELECT 1 AS n UNION ALL SELECT n + 1 FROM num_seq WHERE n < 5) SELECT n FROM num_seq;46Window Functions and ROW_NUMBER
Window Functions and ROW_NUMBER
Theory & Lesson Concept
Practical Scenario
Assign a sequential row number to every payment made by each user, ordered chronologically by the created_at date.
Solution Schema
SELECT user_id, amount, created_at, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS payment_seq FROM payments;47Handling Ties with RANK and DENSE_RANK
Handling Ties with RANK and DENSE_RANK
Theory & Lesson Concept
Practical Scenario
Rank our subscriptions based on their monthly_price from highest to lowest, ensuring no gaps in the rank numbers if prices tie.
Solution Schema
SELECT user_id, monthly_price, DENSE_RANK() OVER (ORDER BY monthly_price DESC) AS price_rank FROM subscriptions;48Calculating Running Totals
Calculating Running Totals
Theory & Lesson Concept
Practical Scenario
Calculate a cumulative running total of all payment amounts in the payments table, ordered chronologically by created_at.
Solution Schema
SELECT payment_id, created_at, amount, SUM(amount) OVER (ORDER BY created_at) AS running_total FROM payments;49Controlling the Window Frame
Controlling the Window Frame
Theory & Lesson Concept
Practical Scenario
Calculate a 3-order moving average of payment amounts (the current order and the 2 strictly preceding it) ordered by created_at.
Solution Schema
SELECT payment_id, created_at, amount, AVG(amount) OVER (ORDER BY created_at ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg FROM payments;50Time-Series Analysis with LAG
Time-Series Analysis with LAG
Theory & Lesson Concept
Practical Scenario
For each payment, show the current amount and the amount of the previous payment made by the same user, ordered by created_at.
Solution Schema
SELECT user_id, created_at, amount, LAG(amount) OVER (PARTITION BY user_id ORDER BY created_at) AS previous_amount FROM payments;51Advanced Conditional Logic
Advanced Conditional Logic
Theory & Lesson Concept
Practical Scenario
Categorize subscriptions as 'Premium' if the tier is 'Enterprise', 'Standard' if 'Pro', and 'Basic' for anything else.
Solution Schema
SELECT subscription_id, tier, CASE WHEN tier = 'Enterprise' THEN 'Premium' WHEN tier = 'Pro' THEN 'Standard' ELSE 'Basic' END AS sub_category FROM subscriptions;52Pivoting Data with Conditional Aggregation
Pivoting Data with Conditional Aggregation
Theory & Lesson Concept
Practical Scenario
Count the number of 'open' and 'closed' support tickets for each user_id as two separate columns in a single row per user.
Solution Schema
SELECT user_id, SUM(CASE WHEN status = 'open' THEN 1 ELSE 0 END) AS open_tickets, SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END) AS closed_tickets FROM support_tickets GROUP BY user_id;53Safe Math and Logic with COALESCE
Safe Math and Logic with COALESCE
Theory & Lesson Concept
Practical Scenario
Calculate total net revenue by summing amount minus discount. Use COALESCE to ensure a NULL discount is safely treated as 0.
Solution Schema
SELECT SUM(amount - COALESCE(discount, 0)) AS net_revenue FROM payments;54Preventing Division by Zero with NULLIF
Preventing Division by Zero with NULLIF
Theory & Lesson Concept
Practical Scenario
Calculate the ratio of amount to tax_amount for all payments, using NULLIF to prevent a division by zero error if tax_amount is 0.
Solution Schema
SELECT payment_id, amount / NULLIF(tax_amount, 0) AS tax_ratio FROM payments;55Custom Sorting with CASE WHEN
Custom Sorting with CASE WHEN
Theory & Lesson Concept
Practical Scenario
Retrieve all support_tickets, but strictly sort them so 'urgent' priority tickets always appear at the very top of the list.
Solution Schema
SELECT ticket_id, priority, status FROM support_tickets ORDER BY CASE WHEN priority = 'urgent' THEN 1 ELSE 2 END, created_at DESC;56Explicit Data Type Conversion
Explicit Data Type Conversion
Theory & Lesson Concept
Practical Scenario
We imported external logs where the 'amount_str' column is stored as text. Convert it to a DECIMAL so we can accurately sum the total.
Solution Schema
SELECT SUM(CAST(amount_str AS DECIMAL)) AS total_imported_revenue FROM external_payments;57Trimming and Standardizing Strings
Trimming and Standardizing Strings
Theory & Lesson Concept
Practical Scenario
Retrieve all user emails from the users table, ensuring any accidental leading or trailing spaces are completely removed for clean reporting.
Solution Schema
SELECT user_id, TRIM(email) AS clean_email FROM users;58Combining Data with UNION ALL
Combining Data with UNION ALL
Theory & Lesson Concept
Practical Scenario
Stack the records from the 'payments' table on top of the 'archived_payments' table to create a master list of all historical transactions.
Solution Schema
SELECT payment_id, amount, created_at FROM payments UNION ALL SELECT payment_id, amount, created_at FROM archived_payments;59UNION vs UNION ALL Performance
UNION vs UNION ALL Performance
Theory & Lesson Concept
Practical Scenario
We want a strictly unique list of all email addresses collected across both our 'users' table and our marketing 'newsletter_leads' table.
Solution Schema
SELECT email FROM users UNION SELECT email FROM newsletter_leads;60Understanding Execution Plans
Understanding Execution Plans
Theory & Lesson Concept
Practical Scenario
Generate the database execution plan to investigate the performance of a slow query retrieving all active subscriptions.
Solution Schema
EXPLAIN SELECT * FROM subscriptions WHERE status = 'active';