Rewrite MySQL Subqueries as JOINs for Performance
In MySQL databases, subqueries can often lead to performance
bottlenecks because the query optimizer may execute them as nested loops
or generate inefficient temporary tables. Rewriting these subqueries as
JOIN operations is a highly effective optimization
technique that allows the MySQL engine to utilize indexes better and
execute queries more efficiently. This article provides a
straightforward guide on how to identify subqueries suitable for
conversion and the step-by-step process of rewriting them into
JOIN statements to boost database performance.
Why Rewrite Subqueries as JOINs?
While subqueries are highly readable and easy to write, older versions of MySQL (and even modern versions under certain conditions) struggle to optimize them. MySQL often evaluates subqueries for every single row processed in the outer query, leading to an \(O(N^2)\) execution time.
By contrast, JOIN operations allow the MySQL query
optimizer to create a better execution plan. The optimizer can choose
the best join order, use indexes more effectively, and avoid the
creation of internal temporary tables, resulting in significantly faster
execution times.
Scenario 1: Rewriting IN Subqueries as INNER JOINs
One of the most common subquery patterns involves filtering records
using the IN operator.
The Subquery Approach
This query finds all employees who work in departments located in ‘New York’:
SELECT name
FROM employees
WHERE department_id IN (
SELECT id
FROM departments
WHERE location = 'New York'
);The JOIN Rewrite
To rewrite this, turn the subquery table into an
INNER JOIN and move the subquery’s filtering condition to
the ON clause or a WHERE clause:
SELECT e.name
FROM employees e
INNER JOIN departments d
ON e.department_id = d.id
WHERE d.location = 'New York';Why it performs better: The INNER JOIN
allows MySQL to look up matching department IDs using indexes on
departments.id and employees.department_id in
a single pass, rather than evaluating the IN list for every
row.
Scenario 2: Rewriting NOT IN or NOT EXISTS as LEFT JOINs
Checking for the absence of records using NOT IN or
NOT EXISTS can be particularly slow in MySQL because it
often requires a full table scan.
The Subquery Approach
This query finds all customers who have never placed an order:
SELECT customer_name
FROM customers
WHERE id NOT IN (
SELECT customer_id
FROM orders
);The JOIN Rewrite (The Exclusion Join)
You can rewrite this as a LEFT JOIN combined with a
WHERE ... IS NULL check. This is known as an “exclusion
join” or “antijoin”:
SELECT c.customer_name
FROM customers c
LEFT JOIN orders o
ON c.id = o.customer_id
WHERE o.customer_id IS NULL;Why it performs better: The LEFT JOIN
attempts to match all customers with orders. If a customer has no
orders, the columns from the orders table will return
NULL. Filtering for o.customer_id IS NULL
instantly targets customers without orders. MySQL can optimize this
using index lookups on orders.customer_id and stop
searching as soon as it finds a match.
Scenario 3: Rewriting Correlated Subqueries
Correlated subqueries refer to columns from the outer query. This forces MySQL to execute the subquery once for every row in the outer table.
The Subquery Approach
This query retrieves the most recent order date for every customer:
SELECT c.customer_name, (
SELECT MAX(o.order_date)
FROM orders o
WHERE o.customer_id = c.id
) AS latest_order
FROM customers c;The JOIN Rewrite
You can rewrite this by creating a derived table (a subquery in the
FROM clause) that aggregates the data first, and then
joining it to the main table:
SELECT c.customer_name, o.latest_order
FROM customers c
LEFT JOIN (
SELECT customer_id, MAX(order_date) AS latest_order
FROM orders
GROUP BY customer_id
) o ON c.id = o.customer_id;Why it performs better: Instead of querying the
orders table repeatedly for each customer, MySQL aggregates
the orders table once and performs a single, highly
efficient join.
Key Rules for Converting Queries
- Ensure Indexes Exist: Joins rely heavily on
indexes. Ensure that the foreign keys and columns used in the
ONclauses are properly indexed. - Watch Out for Duplicates: When converting an
INsubquery to anINNER JOIN, ensure that the joined table does not introduce duplicate rows. If the joined table has a one-to-many relationship, useDISTINCTorGROUP BYto maintain the original row count. - Analyze the Execution Plan: Always verify
performance improvements by running
EXPLAINon both the subquery and the rewrittenJOINquery to see how MySQL processes the data.