Course topics

By WebNest Studio Engineering Team

Database and SQL Tutorial

SQL Query Optimization

Query optimization is the process of understanding how the database engine actually executes a query and adjusting the query, schema, or indexes so it runs faster. Two identical-looking queries can have wildly different performance depending on whether the engine can use an index, how many rows it has to examine, and how it joins tables together.

The starting point for almost all optimization work is asking the database to show you its execution plan — the actual step-by-step strategy it intends to use (or used) to answer a query.

Why it matters

A query that is fast on a hundred rows in development can bring a site down on ten million in production. Optimisation is the skill of finding out why a query is slow and fixing the cause. The N+1 problem alone, which is easy to create with an ORM, is behind a large share of slow pages.

EXPLAIN and EXPLAIN ANALYZE

EXPLAIN asks the database to show the execution plan it would use for a query — which indexes (if any) it plans to use, in what order it joins tables, and its estimated cost — without actually running the query. EXPLAIN ANALYZE goes further: it actually executes the query and reports the real number of rows processed and real time spent at each step, alongside the planned estimates. Comparing the estimated rows to the actual rows in EXPLAIN ANALYZE output is one of the fastest ways to spot a bad assumption the optimizer made, such as outdated table statistics.

Why an Index Isn't Used

Having an index does not guarantee the database will use it. Common reasons an index gets skipped: the table is small enough that a full scan is actually cheaper than the overhead of using the index; the query applies a function or type conversion to the indexed column (e.g. WHERE LOWER(email) = ... against a plain index on email); a leading wildcard is used in a LIKE pattern (LIKE '%gmail.com' cannot use a standard B-tree index efficiently, while LIKE 'asha%' can); the query's WHERE clause doesn't match the leftmost columns of a composite index; or the table's statistics are stale, causing the optimizer to misjudge how selective the index actually is.

The N+1 Query Problem

The N+1 problem is a very common performance bug, especially in applications using an ORM (object-relational mapper). It happens when code runs one query to fetch a list of N parent records, and then, for each of those N records, runs a separate query to fetch related data — resulting in 1 + N total queries instead of 2. For example, fetching 100 blog posts and then looping through them to fetch each post's author with a separate query results in 101 round trips to the database instead of a single JOIN or a single batched query.

Examples

Reading an EXPLAIN ANALYZE plan to confirm index usage

SQL
EXPLAIN ANALYZE
SELECT id, name FROM customers WHERE email = 'asha@example.com';
Output
Index Scan using idx_customers_email on customers
  (cost=0.42..8.44 rows=1 width=36)
  (actual time=0.031..0.033 rows=1 loops=1)
  Index Cond: (email = 'asha@example.com'::text)
Planning Time: 0.112 ms
Execution Time: 0.052 ms

-- Confirms the index is used (Index Scan, not Seq Scan) and estimated rows (1)
-- closely match actual rows (1) — a healthy, well-optimized plan.

Fixing an N+1 problem by replacing per-row queries with a single JOIN

SQL
-- N+1 pattern: 1 query for posts, then N queries for authors (bad)
-- SELECT id, title, author_id FROM posts;
-- (then, for each post) SELECT name FROM authors WHERE id = ?;

-- Fixed: a single query using JOIN
SELECT p.id, p.title, a.name AS author_name
FROM posts p
JOIN authors a ON a.id = p.author_id;
Output
id: 1 | title: 'Intro to Indexes'    | author_name: 'Asha Verma'
id: 2 | title: 'Understanding Joins'  | author_name: 'Rahul Mehta'
(2 rows returned in a single round trip instead of 1 + N separate queries)

Common Mistakes

  • Optimizing a query without ever looking at its EXPLAIN plan — guessing at the bottleneck instead of measuring it directly.
  • Adding an index and assuming it will automatically be used, without verifying with EXPLAIN that the optimizer actually chose it.
  • Writing ORM code that loops over a result set and issues a new query per item, silently causing the N+1 problem, especially with lazy-loaded relationships.
  • Wrapping an indexed column in a function in the WHERE clause (e.g. UPPER(name) = 'ASHA') without a matching functional/expression index, which forces a full scan.

Exercise

An application lists books by running one query for the books and then a separate query for the author of each book, which is the N+1 problem. Replace all of that with a single query that returns each book's title with its author's name, ordered by title.

Expected output: title | author, Clean Code | Robert Martin, Refactoring | Martin Fowler (one per line)

SQL · starter code
CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, author_id INTEGER REFERENCES authors(id));
INSERT INTO authors (id, name) VALUES (1, 'Robert Martin'), (2, 'Martin Fowler');
INSERT INTO books (id, title, author_id) VALUES (1, 'Clean Code', 1), (2, 'Refactoring', 2);

-- The slow way: SELECT * FROM books, then for each book
-- SELECT name FROM authors WHERE id = <author_id>

-- TODO: one query returning title and author name, ordered by title
Show hints
  • Join books to authors on author_id.
  • Name the second column with AS author.
Show solution
SQL · solution
CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, author_id INTEGER REFERENCES authors(id));
INSERT INTO authors (id, name) VALUES (1, 'Robert Martin'), (2, 'Martin Fowler');
INSERT INTO books (id, title, author_id) VALUES (1, 'Clean Code', 1), (2, 'Refactoring', 2);

SELECT b.title, a.name AS author
FROM books b
JOIN authors a ON a.id = b.author_id
ORDER BY b.title;

Quick quiz

1. What does EXPLAIN show?

  • A. The data in the table
  • B. The definition of the table
  • C. The users connected to the database
  • D. The plan the database will use to execute the query
Show answer

D. EXPLAIN ANALYZE also runs the query and reports the actual times and row counts.

2. In a query plan, what does a sequential scan (full table scan) mean?

  • A. The database reads every row of the table
  • B. An index was used
  • C. The query was cached
  • D. The table is empty
Show answer

A. On a large table with a selective filter, it usually signals a missing index.

3. Why can WHERE LOWER(email) = 'a@b.com' not use an ordinary index on email?

  • A. LOWER is not valid SQL
  • B. Indexes only work with numbers
  • C. The index stores the original values, not the result of the function
  • D. Email columns cannot be indexed
Show answer

C. An index on the expression LOWER(email) solves it.

Interview questions

How do you approach a slow query?

Show answer

Run it with EXPLAIN ANALYZE and read the plan: look for full table scans on large tables, large differences between estimated and actual row counts, and expensive sorts or joins. Then address the cause: add or adjust an index for the filter and join columns, rewrite conditions that prevent index use, select only the columns needed, and limit the rows returned. Measure again after each change to confirm that it helped.

What is the N+1 query problem?

Show answer

It happens when code runs one query to fetch a list of N rows and then one further query per row to fetch related data, making N+1 queries where one or two would do. Each query is fast, but the total, with a network round trip for each, is slow. It is common with ORMs that load related objects lazily. The fix is to fetch the related data in the same query with a join, or in one extra query using IN, which ORMs call eager loading.

Key Points to Remember

  • EXPLAIN shows the planned execution strategy; EXPLAIN ANALYZE actually runs the query and reports real timing and row counts.
  • An index can be skipped by the optimizer for many reasons: small table size, functions applied to the column, leading wildcards, mismatched composite index order, or stale statistics.
  • The N+1 problem replaces one efficient query with 1 + N separate queries, usually fixed with a JOIN or a single batched query.
  • Query optimization should always start from measured evidence (EXPLAIN ANALYZE), not assumptions about what "should" be fast.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs your SQL on SQLite in your browser. Standard SQL works; a few features that exist only in PostgreSQL or MySQL do not. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering