15 SQL Subquery Practice Problems with Solutions

Subqueries — queries nested inside other queries — are one of the most powerful and frequently tested concepts in SQL. This practice set covers 15 intermediate problems progressing from subqueries in WHERE with comparison operators, through IN and NOT IN, up to subqueries in FROM as derived tables. Full solutions and explanations included. Click any problem to reveal the solution.

Part 1 — Subqueries in WHERE with Comparison Operators (Problems 1–5)

The most common use of a subquery is inside a WHERE clause. The inner query runs first and returns a single value, which the outer query then uses as a filter condition.

💡 How it works: SELECT * FROM table WHERE column = (SELECT value FROM other_table WHERE condition); — The subquery in parentheses runs first and must return a single value when used with =, >, <, etc.
Problem 1 Find all employees who earn more than the average salary across all employees. Return all columns. Table: employees
Columns: employee_id, first_name, last_name, department, salary
Show solution ↓

Solution

SELECT *
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);

Explanation

The subquery SELECT AVG(salary) FROM employees runs first and returns a single number — the average salary. The outer query then uses that number as the filter threshold, keeping only employees whose salary exceeds it. This is the classic introductory subquery pattern — using an aggregate from the same table as a dynamic filter value.

Problem 2 Find all products that cost more than the most expensive product in the Accessories category. Return the product name, category, and price. Table: products
Columns: product_id, product_name, category, price_usd, stock_quantity
Show solution ↓

Solution

SELECT product_name, category, price_usd
FROM products
WHERE price_usd > (
  SELECT MAX(price_usd)
  FROM products
  WHERE category = 'Accessories'
);

Explanation

The subquery finds the maximum price within the Accessories category. The outer query then returns all products — from any category — priced above that value. Notice the subquery filters by category while the outer query does not, so the results can include products from any category that exceed the Accessories price ceiling.

Problem 3 Find all orders with a total greater than the average order total. Return the order ID, customer ID, and total. Table: orders
Columns: order_id, customer_id, order_date, total_usd, status
Show solution ↓

Solution

SELECT order_id, customer_id, total_usd
FROM orders
WHERE total_usd > (SELECT AVG(total_usd) FROM orders);

Explanation

The subquery calculates the average order total across all orders. The outer query then filters to only orders that exceed that average. Because the subquery references the same table as the outer query, this pattern is sometimes called a self-referencing subquery.

Problem 4 Find all properties with a nightly price below the minimum price of any property in London. Return all columns. Table: rental_listings
Columns: listing_id, property_type, city, bedrooms, price_per_night, rating
Show solution ↓

Solution

SELECT *
FROM rental_listings
WHERE price_per_night < (
  SELECT MIN(price_per_night)
  FROM rental_listings
  WHERE city = 'London'
);

Explanation

The subquery finds the cheapest London listing. The outer query then returns all listings — from any city — priced below that value. This means the results may include listings from London itself if they somehow undercut the minimum, but more typically will surface cheaper listings in other cities.

Problem 5 Find all employees who earn more than the highest salary in the Support department. Return the first name, last name, department, and salary. Table: employees
Columns: employee_id, first_name, last_name, department, salary
Show solution ↓

Solution

SELECT first_name, last_name, department, salary
FROM employees
WHERE salary > (
  SELECT MAX(salary)
  FROM employees
  WHERE department = 'Support'
);

Explanation

The subquery returns the maximum salary within the Support department. The outer query then finds all employees — from any department — who out-earn even the highest-paid Support employee. The results will not include Support employees themselves since no one in that department can exceed their own department's maximum.

Part 2 — Subqueries with IN and NOT IN (Problems 6–10)

When a subquery returns multiple values rather than one, use IN or NOT IN instead of = or >. IN matches any value in the list. NOT IN excludes any value in the list.

💡 When to use IN: Use IN when your subquery returns a list of values rather than a single value. WHERE column IN (SELECT col FROM table) is equivalent to WHERE column = val1 OR column = val2 OR ... for every value the subquery returns.
Problem 6 Find all orders placed by customers who live in New York. Return the order ID, customer ID, and order total. Tables: orders (order_id, customer_id, order_date, total_usd, status) · customers (customer_id, first_name, last_name, city, email)
Show solution ↓

Solution

SELECT order_id, customer_id, total_usd
FROM orders
WHERE customer_id IN (
  SELECT customer_id
  FROM customers
  WHERE city = 'New York'
);

Explanation

The subquery returns a list of customer IDs for all customers in New York. The outer query then uses IN to keep only orders where the customer_id appears in that list. This achieves the same result as a JOIN but using a subquery instead — a common pattern when you only need data from one of the tables in the final result.

Problem 7 Find all products that have never been ordered. Return the product name and category. Tables: products (product_id, product_name, category, price_usd) · order_items (item_id, order_id, product_id, quantity)
Show solution ↓

Solution

SELECT product_name, category
FROM products
WHERE product_id NOT IN (
  SELECT product_id
  FROM order_items
);

Explanation

The subquery returns all product IDs that appear in the order_items table — meaning they have been ordered at least once. NOT IN then excludes those products from the outer query results, leaving only products that have never appeared in an order. This is a very common real-world pattern for finding unmatched records.

Problem 8 Find all employees who work in a department that has at least one Manager. Return the first name, last name, and department. Table: employees
Columns: employee_id, first_name, last_name, department, job_title, salary
Show solution ↓

Solution

SELECT first_name, last_name, department
FROM employees
WHERE department IN (
  SELECT department
  FROM employees
  WHERE job_title = 'Manager'
);

Explanation

The subquery returns a list of departments that contain at least one Manager. The outer query then returns all employees — regardless of their own job title — who work in one of those departments. This includes the managers themselves as well as all their colleagues in the same department.

Problem 9 Find all customers who have never placed an order. Return the first name, last name, and email. Tables: customers (customer_id, first_name, last_name, city, email) · orders (order_id, customer_id, order_date, total_usd, status)
Show solution ↓

Solution

SELECT first_name, last_name, email
FROM customers
WHERE customer_id NOT IN (
  SELECT customer_id
  FROM orders
);

Explanation

The subquery returns all customer IDs that appear in the orders table — customers who have placed at least one order. NOT IN excludes those customers, leaving only customers with no orders at all. This is a clean alternative to a LEFT JOIN with a NULL check, and often easier to read.

Problem 10 Find all rental listings in cities that have at least one listing with a rating above 4.8. Return all columns. Table: rental_listings
Columns: listing_id, property_type, city, bedrooms, price_per_night, rating
Show solution ↓

Solution

SELECT *
FROM rental_listings
WHERE city IN (
  SELECT city
  FROM rental_listings
  WHERE rating > 4.8
);

Explanation

The subquery returns a list of cities that have at least one listing rated above 4.8. The outer query then returns all listings — regardless of their own rating — in any of those cities. So a 3.5-rated listing in a high-performing city would still be included, because the filter is on the city, not the individual listing's rating.

Part 3 — Subqueries in FROM as Derived Tables (Problems 11–15)

A subquery in the FROM clause creates a temporary table — called a derived table — that the outer query treats like a regular table. This lets you run a query on top of an already-aggregated or transformed result set.

💡 Key requirement: A subquery in FROM must always be given an alias — SELECT * FROM (SELECT ...) AS alias_name. Without the alias, the query will throw an error.
Problem 11 Find all departments where the average salary is above $65,000. Return the department and average salary. Table: employees
Columns: employee_id, first_name, last_name, department, salary
Show solution ↓

Solution

SELECT department, avg_salary
FROM (
  SELECT department, AVG(salary) AS avg_salary
  FROM employees
  GROUP BY department
) AS dept_averages
WHERE avg_salary > 65000;

Explanation

The inner query calculates the average salary per department and aliases the result as dept_averages. The outer query then treats that result like a table and filters it with WHERE avg_salary > 65000. Note that this could also be written using HAVING — but using a derived table is a useful pattern to know, especially when the filtering logic is more complex than a simple aggregate comparison.

Problem 12 Find all customers whose total spend across all orders is greater than $500. Return the customer ID and total spend. Table: orders
Columns: order_id, customer_id, order_date, total_usd, status
Show solution ↓

Solution

SELECT customer_id, total_spend
FROM (
  SELECT customer_id, SUM(total_usd) AS total_spend
  FROM orders
  GROUP BY customer_id
) AS customer_totals
WHERE total_spend > 500;

Explanation

The inner query sums each customer's orders and groups by customer_id, producing one row per customer with their total spend. The outer query then filters that derived table to only customers who spent more than $500. Again, HAVING could achieve the same result — but as queries grow more complex, wrapping aggregations in a derived table often makes the logic easier to read and extend.

Problem 13 Find the top 3 cities by average nightly rental price. Return the city and average price, sorted highest to lowest. Table: rental_listings
Columns: listing_id, property_type, city, bedrooms, price_per_night, rating
Show solution ↓

Solution

SELECT city, avg_price
FROM (
  SELECT city, AVG(price_per_night) AS avg_price
  FROM rental_listings
  GROUP BY city
) AS city_averages
ORDER BY avg_price DESC
LIMIT 3;

Explanation

The inner query calculates the average nightly price per city. The outer query then sorts those averages from highest to lowest and limits the result to 3 rows. This is a good example of where a derived table adds clarity — the aggregation and the sorting/limiting logic are cleanly separated into two layers.

Problem 14 Find all products where the total quantity ordered is between 10 and 50 units. Return the product ID and total quantity ordered. Table: order_items
Columns: item_id, order_id, product_id, quantity
Show solution ↓

Solution

SELECT product_id, total_quantity
FROM (
  SELECT product_id, SUM(quantity) AS total_quantity
  FROM order_items
  GROUP BY product_id
) AS product_totals
WHERE total_quantity BETWEEN 10 AND 50;

Explanation

The inner query totals the quantity ordered per product. The outer query filters the derived table using BETWEEN 10 AND 50, which is inclusive on both ends — products with exactly 10 or exactly 50 units ordered are included. BETWEEN is a cleaner alternative to writing WHERE total_quantity >= 10 AND total_quantity <= 50.

Problem 15 Find all customers whose average order total is higher than the overall average order total across all customers. Return the customer ID and their average order total. Table: orders
Columns: order_id, customer_id, order_date, total_usd, status
Show solution ↓

Solution

SELECT customer_id, avg_order_total
FROM (
  SELECT customer_id, AVG(total_usd) AS avg_order_total
  FROM orders
  GROUP BY customer_id
) AS customer_averages
WHERE avg_order_total > (SELECT AVG(total_usd) FROM orders);

Explanation

This problem combines both patterns from this set. The FROM subquery calculates each customer's average order total. The WHERE clause then uses a second subquery to calculate the overall average across all orders — and filters to only customers whose personal average exceeds it. Using two subqueries in a single query is common in real-world SQL and a strong signal of intermediate-to-advanced fluency.

Ready to practice subqueries interactively? Write real queries and get instant feedback in the SQL Drills exercise tool.

Practice Subqueries Interactively →

Want to keep leveling up? Try the advanced SQL exercises which include correlated subqueries and CTEs, or head back to the SQL Drills Learn hub for more practice sets and guides.