Lesson 21 of 45 advanced

SQL: Joins, Indexes and Execution Plans

Why one query is fast, and its twin with a tiny change is slow

Guess first

Pick an answer. You will see if you were right at the end of the lesson.

A LEFT JOIN query adds WHERE o.Status = 'Paid' on the right table. What changes?

Open interactive version (quiz + challenge)

Real-world analogy

Think of a thick textbook. Without an index you read every page to find "photosynthesis". With the index at the back you look up the word, read a page number and jump straight there. The index only works if you look up the word as it is written: asking for "every word ending in -synthesis" means reading the whole index again. The index at the back is a database index, and your choice between using it and reading every page is the execution plan.

What is it?

A SQL join combines rows from two tables by a matching column. An index is a separate sorted structure that lets the database find rows without reading the whole table. The execution plan is the database's own report of how it ran your query: which indexes it used, whether it seeked or scanned, and how many rows it expected versus found. Knowing how to read it is how you move from "the query is slow" to "this exact step is slow, and here is the fix".

Real-world relevance

A report that ran in half a second on a small table takes a minute in production. The developer opens the actual execution plan, sees a scan on a table with millions of rows, and finds a function wrapped around the filter column. Rewriting the filter as a range and adding one covering index brings it back under a second. The code change is one line. Finding it required the plan.

Key points

Quick check

Answer from memory — scrolling up first makes it stick less.

In SQL Server you have an index on (CustomerId, CreatedAt). Which query can use it for a seek?

What does a Key Lookup in an execution plan tell you?

Code example

-- SQL Server (T-SQL). Id is the clustered primary key; CreatedAt is datetime2.

-- Non-sargable: a function on the column forces a scan
SELECT o.Id, o.Total
FROM dbo.Orders AS o
WHERE YEAR(o.CreatedAt) = 2025;

-- Sargable: the bare column against a range can use an index seek
SELECT o.Id, o.Total
FROM dbo.Orders AS o
WHERE o.CreatedAt >= '2025-01-01' AND o.CreatedAt < '2026-01-01';

-- The index that serves it; INCLUDE makes it covering for this query
CREATE NONCLUSTERED INDEX IX_Orders_CreatedAt
    ON dbo.Orders (CreatedAt)
    INCLUDE (Total);

-- LEFT JOIN: Status in WHERE would turn this into an INNER JOIN
SELECT c.Name, o.Id
FROM dbo.Customers AS c
LEFT JOIN dbo.Orders AS o
    ON o.CustomerId = c.Id AND o.Status = 'Paid';

-- With an index on Name: a leading wildcard cannot seek; a prefix can
SELECT Id FROM dbo.Customers WHERE Name LIKE '%son';
SELECT Id FROM dbo.Customers WHERE Name LIKE 'Ander%';

Line-by-line walkthrough

  1. 1. The first query wraps CreatedAt in YEAR(). The database must compute the year for each row, so it scans.
  2. 2. The second query compares the bare column to a date range. That is sargable, so an index seek is possible.
  3. 3. The CREATE INDEX builds a separate structure sorted by CreatedAt, with Total stored in it. The table itself is not re-sorted, and this query never visits it.
  4. 4. In the LEFT JOIN, the Status test sits in the ON clause. Customers with no paid orders still appear, with NULL.
  5. 5. LIKE with a leading % cannot use the sort order, so it scans. LIKE with a prefix can seek, if Name has an index.

Spot the bug

-- SQL Server (T-SQL)
CREATE INDEX IX_Orders_CreatedAt ON dbo.Orders (CreatedAt) INCLUDE (Total);

-- Orders has 20 million rows, but this still takes 15 seconds
SELECT Id, Total
FROM dbo.Orders
WHERE YEAR(CreatedAt) = 2025;
Need a hint?
Can the database use a sorted index when the column is wrapped in a function?
Show answer
YEAR(CreatedAt) is non-sargable: the database cannot search the sorted index and must compute the year for every row. Fix: filter the bare column with a range, WHERE CreatedAt >= '2025-01-01' AND CreatedAt < '2026-01-01', so the index can seek.

Explain like I'm 5

An index is like the list at the back of a book. It tells you the page, so you do not read every page. It only helps if you look up the word exactly as written, not after twisting it.

Fun fact

In SQL Server, a nonclustered index automatically carries the clustered key in every entry. That is why the query above can return o.Id from the index without listing it.

Hands-on challenge

Say it out loud in 30 seconds: "A SQL query is slow. Walk me through how you would find and fix it." Then check: did you mention the execution plan, seek versus scan, a missing or non-sargable condition, and the write cost of adding an index?

More resources

Open interactive version (quiz + challenge) Download the whole course as PDF / Word

Lock it in

← Back to course: .NET + React Playbook