Query Optimization
Learn practical techniques for writing efficient SQL queries, from index-friendly WHERE clauses to avoiding unnecessary joins and wildcard searches.
Introduction
Two queries can ask for the exact same data and return the exact same result, yet one finishes in a few milliseconds while the other takes seconds. The difference is almost always in how the query is written — whether it gives MySQL a chance to use its indexes efficiently, or forces it to work far harder than necessary.
This lesson covers practical, everyday habits for writing efficient queries. None of these require exotic features — just an awareness of how MySQL actually executes what you write.
- How to write WHERE clauses that MySQL can satisfy using an index.
- Why wrapping an indexed column in a function prevents the index from being used.
- Why unnecessary joins slow queries down even when their result is discarded.
- When and why to use LIMIT.
- Why leading-wildcard LIKE searches are slow, and what to do instead.
- When EXISTS can outperform IN for correlated subqueries.
Writing Index-Friendly WHERE Clauses
For MySQL to use an index on a column, the WHERE clause generally needs to compare that column directly against a value, without transforming the column first. A plain, direct comparison lets MySQL jump straight to the matching rows using the index.
-- Assuming an index exists on customer_id
SELECT order_id, total
FROM orders
WHERE customer_id = 42;+----------+--------+
| order_id | total |
+----------+--------+
| 1042 | 59.99 |
| 1187 | 120.00 |
+----------+--------+Avoid Wrapping Indexed Columns in Functions
It is tempting to write something like WHERE YEAR(order_date) = 2024 to find all orders from a given year. The problem is that MySQL must calculate YEAR(order_date) for every single row before it can compare it — the index on order_date cannot be used, because the index stores the raw date values, not the result of a function applied to them.
Function Wraps the Column
Forces a full table scan even if order_date is indexed.
SELECT * FROM orders
WHERE YEAR(order_date) = 2024;Column Used Directly
MySQL can use the index on order_date to jump straight to the matching range.
SELECT * FROM orders
WHERE order_date >= '2024-01-01'
AND order_date < '2025-01-01';Keep the indexed column bare on one side of the comparison. Put any calculation on the other side instead, applied to your fixed value rather than to every row's column.
Avoiding Unnecessary Joins
Every JOIN gives MySQL more work: it must match rows between tables, which costs time and memory, especially on large tables. If a query joins a table but never actually uses any column from it — not in the SELECT list, not in the WHERE clause — that join is pure overhead that can simply be removed.
Unused Join
Joins to customers, but never reads or filters on any of its columns.
SELECT o.order_id, o.total
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id;Join Removed
Returns the identical result without the unnecessary work.
SELECT o.order_id, o.total
FROM orders o;Using LIMIT
If your application only ever displays the first 10 results — a page of search results, a "recent orders" widget — asking MySQL for every matching row and then discarding all but 10 in your application code wastes both database and network effort. LIMIT tells MySQL to stop as soon as it has enough rows.
SELECT order_id, order_date, total
FROM orders
ORDER BY order_date DESC
LIMIT 10;LIMIT is especially powerful combined with an index that already matches your ORDER BY column, since MySQL can then read rows in the needed order and stop early, instead of sorting the entire table first.
Wildcard LIKE Searches
The LIKE operator with a wildcard on the right, like 'text%', can still use an index — MySQL can jump to the first row starting with "text" and read forward. A leading wildcard, like '%text%' or '%text', cannot use a normal index at all, because a matching value could start with anything; MySQL has no choice but to scan every row and check each one.
| Pattern | Can Use a Normal Index? |
|---|---|
| 'text%' | Yes — narrows to a range |
| '%text' | No — full scan required |
| '%text%' | No — full scan required |
If your application genuinely needs fast "contains" searches on large text columns (like searching product descriptions), a normal index will not help — that is what MySQL's full-text search indexes (FULLTEXT) are designed for instead.
EXISTS vs IN
For correlated subqueries — where the inner query refers back to the outer query's row — EXISTS can outperform IN, because EXISTS can stop as soon as it finds a single matching row, while IN, in some cases, may need to build out the full list of inner results first.
-- Find customers who have placed at least one order
SELECT c.name
FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id
);This is a subtle, workload-dependent difference rather than a universal rule — MySQL's optimizer has improved significantly at handling IN efficiently too — but for correlated existence checks, EXISTS is a reasonable default to reach for.
Common Mistakes
- Wrapping an indexed column in a function inside WHERE, silently disabling the index.
- Joining tables purely out of habit, without checking whether any of their columns are actually used.
- Fetching every matching row when only a handful will ever be displayed.
- Using '%search%' style queries on large tables without realizing they force a full scan.
- Assuming EXISTS or IN is always faster without testing against your actual data and query.
Key Takeaways
- Write WHERE clauses that compare indexed columns directly, without wrapping them in functions.
- Remove joins to tables whose columns are never actually used by the query.
- Use LIMIT whenever only a subset of results is needed.
- Leading-wildcard LIKE searches ('%text%') cannot use a normal index and force a full scan.
- EXISTS can be more efficient than IN for correlated subqueries that just check for existence.
Summary
Writing efficient queries is mostly about giving MySQL's optimizer every chance to use the indexes and shortcuts already available to it, instead of accidentally working against them.
In this lesson, you learned how to write index-friendly WHERE clauses, avoid unnecessary joins, use LIMIT, handle wildcard searches carefully, and choose between EXISTS and IN. Next, you will learn to use the EXPLAIN statement to actually see how MySQL executes your queries, so you can confirm these techniques are working as expected.
- You can write WHERE clauses that stay index-friendly.
- You know to remove joins that add no value to a query.
- You understand the cost of leading-wildcard LIKE searches.
- You are ready to measure query performance directly with EXPLAIN.