🏋️ 300+ exercises · 10 free solves · skill-tracked

SQL Practice Questions — 300+ exercises, full solutions

Browse by topic, difficulty, company, or industry. Every exercise has sample data, expected output, AI-explained hints, and a worked solution. The Skillmap tracks where you're strong and where you need more reps.

Open the Exercise Bank → See the categories ↓

Or let the Coach pick — 1 click, the next exercise targets your weakest skill.

300+
Exercises
10
Free challenge solves
9
Skill categories
22
Company tags
3
Industry tracks
Worked solutions

SQL exercises with solutions

Five exercises from the bank, solved in full: the task, the tables, the query and why it works. Each query is the exercise's reference solution and runs in the browser editor on the same data.

EasyFiltering and sorting

1. Comparison Operators

From the movies table, list the title, year, rating and genre of every movie rated above 7.5 and released between 2010 and 2016, both years included. Highest rating first.

Tables

movies(id, title, year, genre, rating, votes, revenue_millions, runtime, director)

Solution

SELECT
  title,
  year,
  rating,
  genre
FROM movies
WHERE rating > 7.5 AND year BETWEEN 2010 AND 2016
ORDER BY rating DESC;

Why it works

WHERE keeps a row only when both conditions hold, and BETWEEN includes both of its ends, so 2010 and 2016 are in. ORDER BY runs after the filter, so only the rows that survived are sorted, and DESC puts the highest rating at the top.

MediumGROUP BY and HAVING

2. GROUP BY + HAVING

Find the genres that have at least 5 movies and an average rating above 6.5. Show the genre, its movie count, its average rating to two decimals and its total revenue, best-rated genre first.

Tables

movies(id, title, year, genre, rating, votes, revenue_millions, runtime, director)

Solution

SELECT
  genre,
  COUNT(*) AS movie_count,
  ROUND(AVG(rating), 2) AS avg_rating,
  ROUND(SUM(revenue_millions), 0) AS total_revenue
FROM movies
GROUP BY genre
HAVING COUNT(*) >= 5 AND AVG(rating) > 6.5
ORDER BY avg_rating DESC;

Why it works

WHERE cannot see COUNT(*) or AVG(rating): it runs before the rows are grouped. HAVING runs after GROUP BY, so it can drop a whole genre by its size and its average. The same two aggregates sit in the SELECT list, which makes the result easy to check by eye.

MediumJOIN

3. LEFT JOIN: Keep Everyone

Show every customer with their membership, the number of orders they placed and the amount they spent, including the customers who never ordered (0 for both). Biggest spender first, then by name.

Tables

customers(customer_id, name, email, signup_date, membership, total_orders)
orders(order_id, customer_id, product, category, quantity, price, total, order_date, country, status)

Solution

SELECT
  c.name,
  c.membership,
  COUNT(o.order_id) AS order_count,
  COALESCE(ROUND(SUM(o.total), 2), 0) AS total_spent
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.name, c.membership
ORDER BY total_spent DESC, c.name;

Why it works

LEFT JOIN keeps every customer and fills the order columns with NULL where nothing matches. COUNT(o.order_id) skips those NULLs and returns 0, where COUNT(*) would count the empty row as 1. SUM over no orders is NULL, so COALESCE turns it into 0.

MediumCTE

4. Your First CTE (WITH Clause)

In a CTE named dept_stats, work out each department's headcount and its average salary, rounded. Then list the departments from it, highest average salary first.

Tables

employees(emp_id, name, department, position, salary, hire_date, manager_id, performance_rating)

Solution

WITH dept_stats AS (
  SELECT
    department,
    COUNT(*) AS headcount,
    ROUND(AVG(salary)) AS avg_salary
  FROM employees
  GROUP BY department
)
SELECT
  department,
  headcount,
  avg_salary
FROM dept_stats
ORDER BY avg_salary DESC;

Why it works

WITH gives the grouped query a name, and the final SELECT reads from dept_stats as if it were a table. It runs the same as a subquery in FROM; the gain is that each step has a name and the query reads top to bottom. A later step that needs the same figures reuses the name instead of repeating the query.

MediumWindow function

5. Window Functions: ROW_NUMBER

Number the employees inside each department by salary, the highest paid as 1, with the name breaking ties. Show name, department, salary and row_num, sorted by department and then by row_num.

Tables

employees(emp_id, name, department, position, salary, hire_date, manager_id, performance_rating)

Solution

SELECT
  name,
  department,
  salary,
  ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC, name) AS row_num
FROM employees
ORDER BY department, row_num;

Why it works

ROW_NUMBER() numbers rows without collapsing them, so every employee stays in the result. PARTITION BY department restarts the count in each department, and the ORDER BY inside OVER decides who gets 1. The name in that ORDER BY breaks salary ties, so the numbering is the same on every run.

Every other exercise shows its worked solution in the app once you have solved it. Open the exercise bank

Browse

Four ways to slice the exercise bank

Pick the lens that matches what you're trying to do — close a topic gap, prep for a specific company, or just drill at a difficulty.

Or drill one topic on its own page: GROUP BY & aggregation · Window functions · Joins · CTEs · CASE WHEN · Subqueries · Date functions · NULL handling · String functions · Ranking functions · Advanced SQL interview questions — or compare them side by side on SQL practice by topic, or open every question as its own page.

By topic

Practice one skill at a time

Each topic page lists the runnable set for one line of the Skillmap, from the first Easy to the last Hard.

Window Functions

RANK, ROW_NUMBER, LAG/LEAD, running totals

Practice this topic →

Joins

INNER, LEFT, anti-joins, self-joins

Practice this topic →

CTEs

WITH, chained and recursive CTEs

Practice this topic →

Subqueries

Correlated, EXISTS, IN, derived tables

Practice this topic →

Aggregation & Grouping

GROUP BY, HAVING, conditional aggregates

Practice this topic →

Conditional Logic

CASE WHEN inside SELECT, WHERE and ORDER BY

Practice this topic →

Date Functions

Date math, truncation, month-over-month

Practice this topic →

NULL Handling

COALESCE, IS NULL, NULLs in joins and aggregates

Practice this topic →

String Functions

LIKE, SUBSTR, TRIM, pattern matching

Practice this topic →

The trap each topic's exercises teach

By difficulty

Exercises by difficulty — beginner, intermediate, advanced

300+ exercises as of September 2026, counted from the live bank: 195 core problems plus 109 on the three industry tracks. Every exercise shows its worked solution once you've solved it.

BankEasyMediumHardTotal
Core exercises558357195
Sector tracks (banking · real estate · manufacturing)394525109
All exercises9412882300+

Industry tracks: Banking & Finance (57 exercises), Real Estate (20), Manufacturing (20). Any of their Easy or Medium problems can be one of your 10 free challenge solves. The Coach gates Hard until you've shown mastery on Medium; browse mode lets you open any exercise directly.

What an exercise looks like

A sample walked through

Every exercise on SQL Quest has the same anatomy: prompt, sample data, expected output, your editor, instant feedback. Here's a Medium-difficulty Window Function exercise.

Top 3 Highest-Paid Employees per Department

MediumWindow FunctionsPARTITION BY

📋 The prompt

For each department, find the top 3 highest-paid employees. If two employees are tied at the same salary, both should appear (and they share the rank). Output columns: name, department, salary, ordered by department, then salary descending.

📊 Sample data — employees

idnamedepartmentsalary
1AliceEngineering180000
2BobEngineering170000
3CarolEngineering170000
4DanSales120000
5EveSales110000

✅ Expected output

namedepartmentsalary
AliceEngineering180000
BobEngineering170000
CarolEngineering170000
DanSales120000
EveSales110000

💡 Hint progression

Hint 1: "Ranking ties — which window function keeps both Bob and Carol at rank 2 instead of giving one rank 3?"
Hint 2: "Wrap your ranked query in a CTE. You can't filter on a window function in the same WHERE."
Show Structure: skeleton with WITH/PARTITION/ORDER BY blanks.

🔓 Solution (revealed only when you ask)

WITH ranked AS ( SELECT name, department, salary, DENSE_RANK() OVER ( PARTITION BY department ORDER BY salary DESC ) AS rk FROM employees ) SELECT name, department, salary FROM ranked WHERE rk <= 3 ORDER BY department, salary DESC;
See 61 Window Function Exercises →
Skill tracking

Every exercise feeds the Skillmap

SQL Quest tracks your proficiency across 9 canonical skills. Each exercise contributes points to the skills it exercises — weighted by difficulty, your speed, and how many hints you used.

More points

Cold solve, no hints

First-attempt solve on a Hard exercise without revealing structure or asking the AI Coach. The clearest signal of real mastery.

Some points

Solve with hints

Used Show Structure or asked the Coach. Still counts — you learned something — but at a discount. The radar reflects guided solves differently than cold ones.

Mastery check

Fresh-solve on unseen exercise

After a lesson, the Coach drops you on 2-3 unseen exercises in the same skill. Recognition isn't enough; you have to produce. That's what gates advancement.

Frequently Asked

300+ exercises waiting — your first 10 solves free.
Open the bank.

Free to start, browser-only, no signup. The Coach can pick the next exercise — or you can browse.

Open SQL Quest — Free ⚡