Hello! Welcome back to our series on query performance.
In our last lesson, we focused on optimizing the WHERE clause by making our conditions "SARGable." You learned that the way you write a query can prevent the database from using an index, and the ultimate proof is always found in the execution plan.
Today, we're moving from optimizing filters on a single table to optimizing how we combine data from multiple tables. This brings us to a very common performance question that you're likely to face in technical interviews and on the job: when should you use a JOIN versus a correlated subquery?
Your goal for this lesson is to determine when to use a JOIN instead of a correlated subquery for better performance. We'll look at the fundamental difference in how they operate, examine real-world benchmarks, and solidify a practical framework for making the right choice.
The Core Difference: Set-Based vs. Row-by-Row
At a conceptual level, the performance difference between a JOIN and a correlated subquery comes down to how they process data. Given your background in C and system programming, you can think of this as the difference between an optimized library function and a manual nested loop.
-
A
JOINis a set-based operation. You declare the relationship between two sets of data (tables), and the database's query optimizer chooses the most efficient physical algorithm (like a Hash Join, Merge Join, or Nested Loop Join) to combine them in one go. -
A Correlated Subquery behaves like a row-by-row operation. For every single row processed by the outer query, the inner subquery is executed again. This is logically similar to writing a nested loop in a procedural language, which can be very inefficient if the outer table is large.
Let's see what this looks like with a large-scale, real-world example.
PostgreSQL Performance Benchmark: Correlated Subquery vs. LEFT JOIN
This article provides a clear benchmark comparing a correlated subquery against a LEFT JOIN on a database with millions of rows. It's a perfect illustration of the performance impact of the 'row-by-row' approach.
Read the article from the beginning down to the 'Benchmark Results' and 'Why JOIN Is Faster' sections. Pay close attention to the problem setup (1 million customers, 10 million orders), the two query approaches, and the clear explanation for the performance gap.
As the article demonstrates, the LEFT JOIN was over three times faster (4.5s vs 14.7s). The reason is straightforward: the correlated subquery had to re-run a lookup for each of the 10 million rows in the orders table. The JOIN operation, using an optimized hash join, processed the entire dataset in a single, much more efficient pass.
The Mighty Optimizer: When Theory and Practice Diverge
So, is the rule "always use a JOIN"? Not quite.
Modern query optimizers are incredibly sophisticated. They are designed to analyze your SQL, understand your intent, and often rewrite your query into a more optimal form behind the scenes. This means that you might write a query using a subquery, but the optimizer can automatically transform it into an efficient JOIN, making the performance identical.
However, this isn't guaranteed. The optimizer's ability to do this depends on the complexity of the query.
This next video does an excellent job demonstrating this nuance. It shows one case where the optimizer makes the two approaches equal, and another where the JOIN is clearly superior.
What to choose for performance SubQuery or Joins Part 62
This video provides a practical demonstration of how a subquery and a JOIN can sometimes have identical performance, thanks to the query optimizer. It also shows a case where they don't, reinforcing that you can't always rely on the optimizer to fix an inefficient query.
Watch from the beginning up to 07:24, and then pay close attention to the summary that begins around 08:53. Notice how in the first example (finding products that were sold), the subquery and JOIN perform the same. In the second, more complex example (finding products not sold), the JOIN performs better.
The key takeaway is exactly what we learned in the last lesson: you cannot know for sure without checking the execution plan. Theory and general rules are a starting point, but the plan is the ground truth of how the database will execute your query.
A Deeper Look: Why a JOIN Plan Can Be More Efficient
When the optimizer does generate a different, more efficient plan for a JOIN, what is it actually doing? Often, it's about reducing the amount of data that needs to be processed at each step.
A JOIN structure can give the optimizer more flexibility to filter or aggregate data before the expensive step of combining the tables.
Correlated Subqueries vs Derived Tables
Let's dig into the 'why' at a lower level. This video provides a fantastic look at two execution plans, showing how rewriting a correlated subquery as a JOIN allows the database to work with significantly less data during the join phase, resulting in better CPU performance.
Watch from the beginning up to 06:27. The narrator does an excellent job explaining the data flow through the execution plans. Focus on the core difference: the correlated subquery joins 8 million rows with 1 million rows, while the rewritten JOIN version first reduces both inputs to 1 million rows and then joins them. This is a much cheaper operation.
This example perfectly illustrates a key optimization principle: do less work. By restructuring the query, the optimizer was able to filter the data down first, meaning the most expensive part of the query (the hash match join) had much less data to process.
Test your understanding!
You have two tables: employees (10,000 rows) and assignments (500,000 rows). You need to find all employees who have never been assigned a "High Priority" task.
Query A (Correlated Subquery):
SELECT e.employee_name
FROM employees e
WHERE NOT EXISTS (
SELECT 1
FROM assignments a
WHERE a.employee_id = e.id
AND a.priority = 'High Priority'
);
Query B (LEFT JOIN):
SELECT e.employee_name
FROM employees e
LEFT JOIN assignments a ON e.id = a.employee_id AND a.priority = 'High Priority'
WHERE a.employee_id IS NULL;
Based on what you've learned, which query is more likely to be faster, and why? Explain it in terms of the conceptual operations (set-based vs. row-by-row).
Show answer
Query B, the LEFT JOIN version, is more likely to be faster.
Here's the reasoning:
- Query A (Correlated Subquery) operates row-by-row. For each of the 10,000 employees, it must execute a subquery to scan the
assignmentstable to check if a 'High Priority' task exists. This could result in 10,000 separate lookups into theassignmentstable. - Query B (LEFT JOIN) is a single set-based operation. The database will likely build a hash table of all 'High Priority' assignments and then stream through the
employeestable, checking for matches in that hash table. This single pass is typically much more efficient than executing 10,000 individual subqueries.
While a modern optimizer might be able to rewrite Query A into a plan similar to Query B, the LEFT JOIN syntax more directly expresses the set-based logic that is usually more performant for this type of anti-join problem.
Guidelines for the Real World (and Interviews)
To bring this all together for your job-seeking goal, let's distill it into practical rules of thumb and potential interview questions.
SQL Joins vs Subqueries: Introduction and Performance Differences Explained
This article provides an excellent summary of when to use which approach, along with a list of common interview questions on the topic. This is perfect for reinforcing the concepts and preparing for technical discussions.
Read the sections titled 'Choosing the Right Approach in Production Workloads', 'Benchmark Summary Table', and 'Interview Questions'. These provide the concise, actionable advice that is invaluable for real-world application and interviews.
The guidelines from the article are a great summary:
-
Choose
JOINs when:- You need to retrieve columns from multiple tables.
- You are combining large, indexed datasets.
- You want predictable, set-based performance.
-
Choose Subqueries when:
- You are performing a simple existence check (
EXISTS) where the subquery can stop as soon as it finds one match. - The logic is easier to read and express as a subquery (but always verify performance!).
- You need to pass an aggregated value to the outer query.
- You are performing a simple existence check (
Conclusion
You now have a solid framework for deciding between JOINs and correlated subqueries. While the "it depends" answer is technically correct, you now understand what it depends on: the query's logic, the data's scale, and most importantly, the execution plan generated by the query optimizer.
Key Takeaways:
- Conceptually,
JOINs are set-based while correlated subqueries are row-by-row (like a nested loop). - For retrieving related columns from large tables, a
JOINis generally the more performant and scalable choice. - Modern query optimizers can often rewrite a simple subquery into an efficient
JOIN, but you should not assume they will, especially for complex logic. - The golden rule remains: Trust the execution plan. Write your query, then use
EXPLAIN ANALYZEto see what the database is actually doing.
Preview of the Next Lesson:
Throughout this lesson and the last, we've repeatedly said that the query optimizer and its execution plan are the ultimate source of truth. But how does the optimizer make its decisions? In our next lesson, we will formally explain the role of the query optimizer in choosing an execution plan, diving into concepts like cost-based optimization and statistics.