Ad – 728×90
🎯 Interview Prep

SQL Practical Exercises – Hands-On SQL Challenges

The best way to prepare for SQL interviews is to write real SQL. This page contains 15 progressive exercises — 5 beginner, 5 intermediate, and 5 advanced — each with a scenario, table structure, task, expected output, and a hint. Work through them before looking at the solutions. Most can be run against a PostgreSQL or MySQL database you set up locally.

⏱️ 60 min practice 🎯 All Levels 📅 Updated 2026

All exercises use this shared schema. Create it before you start:

SQL – Setup Schema
CREATE TABLE departments (
  id   INT PRIMARY KEY,
  name VARCHAR(50)
);

CREATE TABLE employees (
  id            INT PRIMARY KEY,
  name          VARCHAR(100),
  department_id INT REFERENCES departments(id),
  salary        DECIMAL(10,2),
  hire_date     DATE,
  job_title     VARCHAR(100),
  manager_id    INT REFERENCES employees(id)
);

CREATE TABLE customers (
  id    INT PRIMARY KEY,
  name  VARCHAR(100),
  email VARCHAR(100)
);

CREATE TABLE orders (
  id          INT PRIMARY KEY,
  customer_id INT REFERENCES customers(id),
  total       DECIMAL(10,2),
  created_at  DATE
);

-- Sample inserts
INSERT INTO departments VALUES (1,'Engineering'),(2,'Sales'),(3,'HR'),(4,'Marketing');
INSERT INTO employees VALUES
  (1,'Alice',1,120000,'2020-03-15','Senior Engineer',NULL),
  (2,'Bob',1,95000,'2021-07-01','Engineer',1),
  (3,'Carol',2,80000,'2019-11-01','Sales Rep',NULL),
  (4,'Dave',2,85000,'2023-02-20','Sales Rep',3),
  (5,'Eve',2,90000,'2022-05-10','Sales Lead',3),
  (6,'Frank',3,70000,'2021-01-05','HR Manager',NULL),
  (7,'Grace',1,105000,'2022-09-01','Engineer',1),
  (8,'Henry',4,75000,'2023-01-10','Marketing Exec',NULL),
  (9,'Iris',2,78000,'2022-04-15','Sales Rep',5),
  (10,'Jack',1,88000,'2023-08-01','Junior Engineer',2);

Beginner Exercises

Exercise 1 – Employees in Engineering

Task: Select all employees in the Engineering department. Show their name, salary, and hire date.

💡
Hint

JOIN the employees table to departments, then filter with WHERE.

SQL – Solution
SELECT e.name, e.salary, e.hire_date
FROM employees e
JOIN departments d ON d.id = e.department_id
WHERE d.name = 'Engineering'
ORDER BY e.salary DESC;

Exercise 2 – Top 5 Highest-Paid Employees

Task: Find the top 5 highest-paid employees. Show name, department name, and salary.

💡
Hint

ORDER BY salary DESC, then LIMIT 5.

SQL – Solution
SELECT e.name, d.name AS department, e.salary
FROM employees e
JOIN departments d ON d.id = e.department_id
ORDER BY e.salary DESC
LIMIT 5;

Exercise 3 – Employee Count per Department

Task: Count the number of employees in each department. Show department name and headcount, sorted by headcount descending.

💡
Hint

GROUP BY department_id after joining departments. Use COUNT(*).

SQL – Solution
SELECT d.name AS department, COUNT(*) AS headcount
FROM employees e
JOIN departments d ON d.id = e.department_id
GROUP BY d.name
ORDER BY headcount DESC;

Exercise 4 – Employees Hired in 2023

Task: Find all employees hired in the year 2023. Show their name and hire date.

💡
Hint

Use EXTRACT(YEAR FROM hire_date) = 2023 in PostgreSQL or YEAR(hire_date) = 2023 in MySQL.

SQL – Solution
-- PostgreSQL
SELECT name, hire_date FROM employees
WHERE EXTRACT(YEAR FROM hire_date) = 2023
ORDER BY hire_date;

-- MySQL
SELECT name, hire_date FROM employees
WHERE YEAR(hire_date) = 2023
ORDER BY hire_date;

Exercise 5 – Distinct Job Titles

Task: List all distinct job titles across the company, sorted alphabetically.

SQL – Solution
SELECT DISTINCT job_title
FROM employees
ORDER BY job_title;
Ad – 336×280

Intermediate Exercises

Exercise 6 – Employees Earning Above Department Average

Task: Find employees whose salary is above the average salary of their own department. Show name, department, salary, and the department average.

💡
Hint

Use a correlated subquery or a CTE that first calculates the average salary per department, then join it back to employees.

SQL – Solution
WITH dept_avg AS (
  SELECT department_id, AVG(salary) AS avg_salary
  FROM employees
  GROUP BY department_id
)
SELECT e.name, d.name AS department, e.salary,
       ROUND(da.avg_salary, 2) AS dept_average
FROM employees e
JOIN departments d  ON d.id = e.department_id
JOIN dept_avg da    ON da.department_id = e.department_id
WHERE e.salary > da.avg_salary
ORDER BY d.name, e.salary DESC;

Exercise 7 – Rank Employees by Salary Within Each Department

Task: Show each employee's name, department, salary, and their salary rank within their department (1 = highest paid). Use a window function.

💡
Hint

Use RANK() or DENSE_RANK() with PARTITION BY department_id ORDER BY salary DESC.

SQL – Solution
SELECT
  e.name,
  d.name        AS department,
  e.salary,
  RANK() OVER (PARTITION BY e.department_id ORDER BY e.salary DESC) AS salary_rank
FROM employees e
JOIN departments d ON d.id = e.department_id
ORDER BY d.name, salary_rank;

Exercise 8 – Customers with No Orders

Task: Find all customers who have never placed an order. (For this exercise, populate the customers and orders tables with a few rows of sample data.)

💡
Hint

Use a LEFT JOIN from customers to orders, then filter WHERE orders.id IS NULL.

SQL – Solution
SELECT c.id, c.name, c.email
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL
ORDER BY c.name;

Exercise 9 – Departments with More Than 3 Employees (using CTE)

Task: Write a CTE that counts employees per department, then SELECT from it to return only departments with more than 3 employees.

SQL – Solution
WITH dept_counts AS (
  SELECT department_id, COUNT(*) AS headcount
  FROM employees
  GROUP BY department_id
)
SELECT d.name AS department, dc.headcount
FROM dept_counts dc
JOIN departments d ON d.id = dc.department_id
WHERE dc.headcount > 3
ORDER BY dc.headcount DESC;

Exercise 10 – Give Sales a 10% Raise (Conditional UPDATE)

Task: Give all employees in the Sales department who were hired before 2022 a 10% salary raise.

💡
Hint

Use UPDATE with a subquery or join to identify the correct employees. Always run a SELECT first to verify which rows will be affected.

SQL – Solution
-- Verify first
SELECT e.name, e.salary, e.hire_date
FROM employees e
JOIN departments d ON d.id = e.department_id
WHERE d.name = 'Sales' AND e.hire_date < '2022-01-01';

-- Then update (MySQL syntax)
UPDATE employees e
JOIN departments d ON d.id = e.department_id
SET e.salary = e.salary * 1.10
WHERE d.name = 'Sales'
  AND e.hire_date < '2022-01-01';

-- PostgreSQL syntax
UPDATE employees
SET salary = salary * 1.10
WHERE department_id = (SELECT id FROM departments WHERE name = 'Sales')
  AND hire_date < '2022-01-01';

Advanced Exercises

Exercise 11 – Running Total of Monthly Revenue

Task: Using the orders table, compute the total revenue per month AND the running total of revenue up to and including that month. Show month, monthly revenue, and cumulative revenue.

💡
Hint

First aggregate to month with DATE_TRUNC / DATE_FORMAT, then use SUM() OVER (ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW).

SQL – Solution (PostgreSQL)
WITH monthly AS (
  SELECT
    DATE_TRUNC('month', created_at) AS month,
    SUM(total) AS monthly_revenue
  FROM orders
  GROUP BY 1
)
SELECT
  month,
  monthly_revenue,
  SUM(monthly_revenue) OVER (
    ORDER BY month
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS cumulative_revenue
FROM monthly
ORDER BY month;

Exercise 12 – Second-Highest Salary

Task: Find the second-highest salary in the employees table. Your solution should work even if there are duplicate salary values at the top.

💡
Hint

Use DENSE_RANK() to assign ranks and then filter for rank = 2. Avoid LIMIT/OFFSET — it's unreliable with duplicates.

SQL – Solution
WITH ranked AS (
  SELECT name, salary,
         DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
  FROM employees
)
SELECT name, salary
FROM ranked
WHERE rnk = 2;

Exercise 13 – Recursive CTE: Employee Hierarchy

Task: Use a recursive CTE to build the full management hierarchy starting from the top-level manager (employees with no manager_id). Show each employee's name, their manager's name, and their depth in the hierarchy.

💡
Hint

Anchor: WHERE manager_id IS NULL. Recursive part: JOIN employees on manager_id = previous level's id. Add a depth counter.

SQL – Solution (PostgreSQL)
WITH RECURSIVE org_chart AS (
  -- Anchor: top-level managers
  SELECT id, name, manager_id, 1 AS depth,
         name::TEXT AS path
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  -- Recursive: each employee under a manager
  SELECT e.id, e.name, e.manager_id, oc.depth + 1,
         oc.path || ' > ' || e.name
  FROM employees e
  JOIN org_chart oc ON oc.id = e.manager_id
)
SELECT
  REPEAT('  ', depth - 1) || name AS employee,
  depth,
  path
FROM org_chart
ORDER BY path;

Exercise 14 – Pivot: Headcount by Department and Hire Year

Task: Create a pivot table showing employee headcount broken down by hire year (columns: 2019, 2020, 2021, 2022, 2023) with one row per department.

💡
Hint

Use conditional aggregation: SUM(CASE WHEN EXTRACT(YEAR FROM hire_date) = 2020 THEN 1 ELSE 0 END) for each year column.

SQL – Solution
SELECT
  d.name AS department,
  SUM(CASE WHEN EXTRACT(YEAR FROM e.hire_date) = 2019 THEN 1 ELSE 0 END) AS "2019",
  SUM(CASE WHEN EXTRACT(YEAR FROM e.hire_date) = 2020 THEN 1 ELSE 0 END) AS "2020",
  SUM(CASE WHEN EXTRACT(YEAR FROM e.hire_date) = 2021 THEN 1 ELSE 0 END) AS "2021",
  SUM(CASE WHEN EXTRACT(YEAR FROM e.hire_date) = 2022 THEN 1 ELSE 0 END) AS "2022",
  SUM(CASE WHEN EXTRACT(YEAR FROM e.hire_date) = 2023 THEN 1 ELSE 0 END) AS "2023"
FROM employees e
JOIN departments d ON d.id = e.department_id
GROUP BY d.name
ORDER BY d.name;

Exercise 15 – Find and Delete Duplicate Rows

Task: The employees table might have duplicate rows (same name and department_id). Find any duplicates, then delete all but the row with the lowest id.

💡
Hint

Use ROW_NUMBER() OVER (PARTITION BY name, department_id ORDER BY id) to assign row numbers within each duplicate group. Delete rows where row_num > 1.

SQL – Solution (PostgreSQL)
-- Step 1: Identify duplicates
WITH numbered AS (
  SELECT id,
         ROW_NUMBER() OVER (
           PARTITION BY name, department_id
           ORDER BY id
         ) AS rn
  FROM employees
)
SELECT id FROM numbered WHERE rn > 1;

-- Step 2: Delete duplicates (keep the lowest id in each group)
WITH numbered AS (
  SELECT id,
         ROW_NUMBER() OVER (
           PARTITION BY name, department_id
           ORDER BY id
         ) AS rn
  FROM employees
)
DELETE FROM employees
WHERE id IN (SELECT id FROM numbered WHERE rn > 1);

FAQ

Where can I run these exercises? +

The easiest options: (1) db-fiddle.com — paste the schema and query, choose PostgreSQL or MySQL, run in browser. (2) sqlfiddle.com — similar browser-based SQL playground. (3) Install PostgreSQL locally (free, runs on Windows/Mac/Linux) and use pgAdmin or psql. (4) DBeaver is a free universal database client that connects to both PostgreSQL and MySQL — great for local practice.

Are recursive CTEs supported in MySQL? +

Yes — recursive CTEs using WITH RECURSIVE are supported from MySQL 8.0 onwards. Earlier versions (5.7 and below) do not support them. In MySQL 8.0+, the syntax is identical to PostgreSQL for basic cases. If you're on MySQL 5.7, you would need to use a stored procedure with a loop, or use a different approach like a closure table pattern for hierarchical data.

How do I know if my solution is correct? +

Run the provided solution and compare output with yours. For exercises without a fixed expected row count (like the hierarchy or pivot exercises), verify the structure is correct: all rows are present, values match expectations. For UPDATE/DELETE exercises, always SELECT first with the same WHERE condition to see which rows would be affected before running the destructive statement.

What's the best way to practice SQL for interviews? +

Practice writing SQL by hand — not copy-pasting. Read a question, think about the approach, write the query from scratch. Check it against the solution. Review the interview questions page to understand the conceptual answers. Then do timed practice on platforms like LeetCode (SQL section), HackerRank (SQL challenges), or Mode Analytics' SQL tutorial. Focus on JOINs, aggregation + HAVING, window functions (ROW_NUMBER, RANK, LAG), and CTEs — these appear in almost every data/backend SQL interview.