LearnContact
Lesson 7419 min read

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.

What You Will Learn
  • 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;
Result
+----------+--------+
| 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.

sql
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.

sql
SELECT * FROM orders
WHERE order_date >= '2024-01-01'
  AND order_date <  '2025-01-01';
Rule of Thumb

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.

sql
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.

sql
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;
Note

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.

PatternCan Use a Normal Index?
'text%'Yes — narrows to a range
'%text'No — full scan required
'%text%'No — full scan required
Watch Out

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

Avoid These 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.

Lesson 74 Completed
  • 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.
Next Lesson →

EXPLAIN Statement