Introduction
Welcome to your second lesson in the "Intermediate SQL for Analytics" module. In our last session, you learned how to clean and prepare data using COALESCE, NULLIF, and CAST. That was about getting your raw materials ready. Now, we'll start building more complex analytical structures with them.
Many business questions can't be answered in a single step. For example, "Which sales representatives in the top-performing region exceeded the company-wide average sales amount?" This question involves at least three steps:
- Find the top-performing region.
- Find the company-wide average sales amount.
- Filter for reps in that region who exceeded that average.
This is where subqueries come in. A subquery, or inner query, is a SQL query nested inside a larger query. It allows you to break down complex problems into logical, manageable steps.
In this lesson, you will learn how to write subqueries in the SELECT, FROM, and WHERE clauses to answer these kinds of multi-step analytical questions. Mastering this technique is fundamental to advancing your SQL skills from basic data retrieval to sophisticated data analysis.
1. The Foundation: Subqueries in the FROM Clause
The most intuitive place to start with subqueries is in the FROM clause. A subquery in the FROM clause is often called a derived table. The logic is simple:
- The database runs the inner query (the subquery) first.
- The result set of this inner query is treated as a temporary, virtual table.
- The outer query then runs, using this virtual table as its source.
This pattern is essential when you need to perform aggregations in multiple stages. For instance, you can't write AVG(SUM(sales)) in a single query. You must first calculate the SUM in a subquery and then calculate the AVG of those sums in the outer query.
To see the basic structure and a practical example, let's turn to a helpful article from ThoughtSpot.
This resource provides a clear introduction to subqueries, starting with their use in the FROM clause for multi-stage aggregations.
Please read the sections "Subquery basics" and "Using subqueries to aggregate in multiple stages". Pay close attention to two key rules: The inner query must be able to run on its own. The derived table created by the subquery must be given a name (an alias).
As you saw, a good practice is to write and test your inner query first to ensure it produces the intermediate result you expect. Then, you can wrap the outer query around it.
Let's look at a common analytical scenario: finding the average number of sales per day.
-- Step 1: Find the number of sales for each day (this is the inner query)
SELECT
sale_date,
COUNT(order_id) AS sales_count
FROM sales
GROUP BY sale_date;
-- Step 2: Use the above as a derived table to find the average
SELECT
AVG(daily_sales.sales_count) AS average_daily_sales
FROM
(
SELECT
sale_date,
COUNT(order_id) AS sales_count
FROM sales
GROUP BY sale_date
) AS daily_sales; -- The alias 'daily_sales' is mandatory
This two-step logic is impossible to execute in a single aggregation but becomes straightforward with a subquery.
2. Dynamic Filtering: Subqueries in the WHERE Clause
Subqueries are also incredibly powerful when used in the WHERE clause to create dynamic filters. Instead of comparing a column to a fixed value (e.g., WHERE price > 100), you can compare it to a value or set of values returned by another query.
There are two main categories:
-
Subqueries returning a single value: The inner query returns one row and one column. You can use standard comparison operators like
=,>,<, etc.- Use case: "Find all products with a price higher than the average price."
-
Subqueries returning multiple values: The inner query returns a list of values (one column, multiple rows). You must use operators designed for lists, such as
IN,NOT IN,ANY, orALL.- Use case: "Find all employees who work in the 'Sales' or 'Marketing' departments."
The following resource provides examples for both types.
This section of the ThoughtSpot article demonstrates how to use subqueries for conditional logic, covering both single-value and multi-value results.
Read the section titled "Subqueries in conditional logic". Focus on the distinction between using = for a single-cell result and IN for a multi-cell result.
Let's illustrate with a clear example. Imagine we have employees and departments tables.
Example 1: Single-Value Subquery
Find employees who earn more than the company's average salary.
SELECT
employee_name,
salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
The inner query (SELECT AVG(salary) FROM employees) runs first, returns a single number (e.g., 60000), and the outer query becomes WHERE salary > 60000.
Example 2: Multi-Value Subquery
Find all employees who work in a department located in 'New York'.
SELECT
employee_name
FROM employees
WHERE department_id IN (SELECT department_id FROM departments WHERE location = 'New York');
The inner query (SELECT department_id FROM departments ...) could return a list of department IDs (e.g., 3, 7). The outer query then finds all employees whose department_id is in that list.
3. On-the-Fly Calculations: Subqueries in the SELECT Clause
Finally, you can place a subquery in the SELECT clause to calculate a new column. This type of subquery is often called a scalar subquery because it must return a single value (one row, one column) for each row of the outer query's result set.
Scalar subqueries are often correlated, meaning the inner query's calculation depends on the current row being processed by the outer query.
Let's explore this with an article that classifies subquery types well.
Join With Subquery SQL: Key Concepts
This article from DataForgeLabs gives a structured overview of different subquery types. We'll focus on the 'Scalar subquery' which is what you use in a SELECT clause.
Read the subsection titled "Scalar subqueries". Notice how the inner query (SELECT AVG(unit_price) ...) refers to p.product_id from the outer query. This makes it a correlated subquery.
Example: Calculating an average for each product
Let's find each product and compare its price to the average price of all products in its category.
SELECT
p.product_name,
p.price,
p.category,
(SELECT AVG(price) FROM products WHERE category = p.category) AS avg_category_price
FROM
products AS p;
For each product p in the outer query, the inner subquery runs and calculates the average price for that specific product's category.
The image below shows a real-world example combining subqueries in both the SELECT and FROM clauses.

Let's break down the query in the image:
FROMClause Subquery:(select top 10 Id, DisplayName from dbo.Users order by Reputation desc)runs first. It finds the 10 users with the highest reputation and creates a derived table with theirIdandDisplayName.SELECTClause Subquery: For each of the 10 users from the derived table, the scalar subquery(select top 1 Name from dbo.Badges where UserId = Users.Id order by Date desc)runs. It looks up theBadgestable and finds the single most recent badge name for that specificUserId.- Outer Query: The main query selects the
IdandDisplayNamefrom the derived table and theLatest_Badgecalculated by the scalar subquery.
Practice Exercise
Time to put your knowledge to the test. Imagine you work for an e-commerce company and have access to three tables: customers, orders, and products.
Tables:
customers(customer_id,name,city)orders(order_id,customer_id,product_id,quantity,order_date)products(product_id,product_name,price)
Your Task:
Write a single SQL query to find the names of all customers who live in 'São Paulo' and have placed an order for a product that costs more than the average price of all products.
Hints:
- You will need to filter customers by city.
- You will need to find which customers have placed an order for an expensive product. This involves a
JOINand another filter. - The definition of "expensive" is dynamic (above average), so you'll need a subquery for that.
Click here for one possible solution
SELECT DISTINCT
c.name
FROM
customers AS c
JOIN
orders AS o ON c.customer_id = o.customer_id
WHERE
c.city = 'São Paulo'
AND
o.product_id IN (
SELECT
product_id
FROM
products
WHERE
price > (SELECT AVG(price) FROM products)
);
Explanation:
- Innermost Subquery:
(SELECT AVG(price) FROM products)runs first. It calculates the single average price of all products. WHEREClause Subquery:(SELECT product_id FROM products WHERE price > ...)runs next. It finds the list ofproduct_ids for all products whose price is above the average calculated in the previous step.- Outer Query: Finally, the main query joins
customersandorders. It filters for customers in 'São Paulo' (c.city = 'São Paulo') and checks if theproduct_idin their order isINthe list of expensive products returned by the subquery.SELECT DISTINCTis used to avoid listing the same customer multiple times if they placed multiple qualifying orders.
Conclusion
Great job! You've just unlocked a technique that dramatically increases the complexity of questions you can answer with SQL. By nesting queries within each other, you can create logical, sequential steps to solve analytical problems that would be impossible in a single-layer query.
Key Takeaways:
- Subqueries are queries within queries that help break down complex problems.
FROMClause (Derived Tables): Use these when you need to perform multi-stage aggregations. The inner query creates a temporary table for the outer query to use. Remember to give it an alias!WHEREClause: Use these to create dynamic filters. The inner query can return a single value (for use with=,>, etc.) or a list of values (for use withIN).SELECTClause (Scalar Subqueries): Use these to compute a new column for each row in your result set. The inner query must return a single value and is often correlated with the outer query.
In this lesson, you saw how subqueries can get nested and sometimes become difficult to read. In our next lesson, we will explore Common Table Expressions (CTEs). CTEs serve a similar purpose to derived tables but provide a way to name your subqueries and structure your complex queries in a much cleaner and more readable way.