Hello! Let's dive into our next lesson on subqueries.
In our last session, we explored how to use a scalar subquery in the WHERE clause. This allowed us to filter data based on a single, dynamically calculated value, like finding products with a price greater than the overall average. This was powerful, but it came with a key limitation: the subquery had to return exactly one value.
So, what happens when your filtering logic depends on a list of values? For example, how do you find all employees who work in offices located in the USA? Your subquery would need to return a list of all office codes in the USA, not just one. A simple > or = operator won't work here.
This lesson tackles that exact scenario. Our goal is to use IN and NOT IN with a subquery for set-based filtering. These operators are designed to check if a value exists within a list, making them the perfect tools for working with subqueries that return multiple rows.
Filtering with a List: The IN Operator
The IN operator allows you to check if a value from your main query's table matches any value in a list of values provided by your subquery. This is incredibly useful for filtering based on a set of criteria that is itself determined by data in another table.
The basic structure looks like this:
SELECT column_name(s)
FROM table1
WHERE column_name IN (SELECT column_name FROM table2 WHERE condition);
Let's break this down with a practical example: "Show the details of all orders made by customers who are from Germany."
This requires two logical steps:
- First, get the list of all
customer_ids for customers located in Germany. - Then, select all orders from the
orderstable where thecustomer_idis in that list.
A subquery with IN combines these steps into one elegant query.
SQL Subquery (Visually Explained) | Complete Guide with Correlated Subquery | #SQL Course 27
The following video from the 'Data with Baraa' channel provides an excellent visual walkthrough of this exact problem. It starts by using a static list and then seamlessly transitions to a dynamic subquery.
Watch the segment from 00:41:37 to 00:47:19. Pay close attention to how the hardcoded list IN (1, 4) is replaced by a subquery that dynamically fetches the customer IDs.
As the video demonstrated, the query would look like this:
SELECT order_id, order_date, customer_id
FROM orders
WHERE customer_id IN (SELECT customer_id
FROM customers
WHERE country = 'Germany');
How It Works: The Execution Flow
Given your interest in low-level systems, let's look at the execution flow for this type of query. It's a logical, step-by-step process:
- Execute the Subquery: The database engine first runs the inner query:
SELECT customer_id FROM customers WHERE country = 'Germany'. - Store the Results: The result of this subquery—a list of customer IDs (e.g.,
1,4)—is held in a temporary, in-memory list. - Execute the Main Query: The main query then runs, using the list from the subquery to filter its results. For each row in the
orderstable, it checks if thecustomer_idis present in the in-memory list (1,4). If it is, the row is included in the final result set.
This visual explanation will help solidify the concept.
SQL Subquery (Visually Explained) | Complete Guide with Correlated Subquery | #SQL Course 27
This next clip from the same video visually breaks down the execution process. It shows how the subquery is executed first to create an 'intermediate result' that the main query uses for filtering.
Watch from 00:48:24 to 00:50:08. This visualization of the step-by-step process is a great mental model for understanding how the database engine operates.
The image below illustrates the same concept using a different schema for finding employees in USA-based offices, which should help you generalize the pattern.

Excluding a List: The NOT IN Operator
As you might guess, NOT IN does the exact opposite. It filters for rows where the value does not match any value in the list returned by the subquery. This is useful for finding records that don't have a corresponding entry in another table based on some criteria.
For example, to "find all products that are not finished bicycles," you could write a query like this.
The official Microsoft SQL Server documentation provides a clear, concise explanation and example for using NOT IN.
Read the section titled 'Subqueries with NOT IN'. It gives a straightforward example of finding products that don't belong to a set of subcategories.
The query from the article finds products that are not in the 'Mountain Bikes', 'Road Bikes', or 'Touring Bikes' subcategories.
SELECT [Name]
FROM Production.Product
WHERE ProductSubcategoryID NOT IN (SELECT ProductSubcategoryID
FROM Production.ProductSubcategory
WHERE [Name] = 'Mountain Bikes'
OR [Name] = 'Road Bikes'
OR [Name] = 'Touring Bikes');
The logic is the same as with IN: the subquery runs first to generate a list of subcategory IDs, and then the outer query selects all products whose ProductSubcategoryID is not in that list.
Test your understanding!
You have two tables:
employees(with columnsemployee_id,name,department_id)departments(with columnsdepartment_id,department_name)
Write a single query to find the names of all employees who work in the 'IT' or 'Finance' departments, using a subquery with IN.
Show answer
SELECT name
FROM employees
WHERE department_id IN (SELECT department_id
FROM departments
WHERE department_name = 'IT' OR department_name = 'Finance');
Explanation:
- The subquery finds the
department_ids for 'IT' and 'Finance'. - The outer query then selects all employees whose
department_idis in the list returned by the subquery.
The NULL Trap: A Critical Detail About NOT IN
Here is a detail that is crucial for writing correct and robust queries, and it taps right into the logical, systems-level thinking you enjoy. The NOT IN operator has a "trap" when dealing with NULL values.
If the list of values returned by your subquery contains even one NULL, the NOT IN condition will always evaluate to UNKNOWN (effectively false), and your entire query will return an empty set.
This is a common source of bugs and a great concept to understand for technical interviews. Let's dig into the "why." SQL uses three-valued logic: TRUE, FALSE, and UNKNOWN. Any comparison with NULL yields UNKNOWN.
The expression value NOT IN (a, b, NULL) is logically equivalent to:value <> a AND value <> b AND value <> NULL
Since value <> NULL is UNKNOWN, the entire chain of AND conditions evaluates to UNKNOWN. The WHERE clause only includes rows that evaluate to TRUE, so the row is discarded. This happens for every row, resulting in an empty result set.
SQL 'select where not in subquery' returns no results
This Stack Overflow discussion is one of the best explanations of this behavior you will find. It breaks down the logic clearly and discusses the best ways to avoid the problem.
First, read the answer that begins 'The long answer (From T-SQL Fundamentals...)'. It explains the three-valued logic in detail. Then, read the answer that begins 'Let's suppose these values for common_id:'. It provides a simple step-by-step evaluation of the logic that makes the concept very clear.
How to Safely Use NOT IN
To avoid this trap, you must ensure your subquery does not return any NULL values. The safest way to do this is to explicitly filter them out in the subquery's WHERE clause.
Unsafe Query:
SELECT * FROM Customers
WHERE customer_id NOT IN (SELECT customer_id FROM Orders); -- Fails if any order has a NULL customer_id
Safe Query:
SELECT * FROM Customers
WHERE customer_id NOT IN (SELECT customer_id
FROM Orders
WHERE customer_id IS NOT NULL); -- Correctly filters out NULLs
An even safer alternative, which we will explore in the next lesson, is to use NOT EXISTS. The EXISTS operator uses two-valued logic and handles NULLs more intuitively, making it a preferred choice for this kind of "anti-join" pattern.
Conclusion
In this lesson, we've moved beyond scalar subqueries and learned how to filter data against a dynamically generated list of values. This is a fundamental pattern for querying relational data.
Key Takeaways:
- Use the
INoperator with a subquery to check if a value exists within a set of values returned by the subquery. - Use the
NOT INoperator to check if a value is absent from the set returned by the subquery. - Crucially, be aware that if a subquery for a
NOT INclause returns anyNULLvalues, the entire query will return an empty set. Always handle potentialNULLs by filtering them out in the subquery.
Preview of the Next Lesson:
The NULL trap with NOT IN hinted at a safer, and often more performant, alternative: the EXISTS operator. In our next lesson, we will dive deep into using EXISTS and NOT EXISTS. You'll learn how they check for the existence of related rows without needing to return the data itself, providing another powerful tool for your SQL arsenal.