SQL Query Optimization: Stop Scanning 400k Rows for a 4k Result | Supriya

Supriya shows two analytics queries that return the same 4,459 rows while one reads 400,000 rows and the other about 11,000. EXPLAIN QUERY PLAN reveals the difference, and wrapping an indexed column in a function is the cause.

3:01 video3 min readWatch on YouTube

Two SQL queries can return exactly the same output and still differ enormously in how much work they do. Supriya opens with a concrete case: the same 4,459 rows, byte for byte, but one query reads 11,000 rows to get there and the other reads 400,000. Nothing in the SQL text tells you which is which. The query plan does.

The shape of an analytics query

The example is a table of 400,000 postings, and the goal is one month of them. About 11,000 rows survive the date filter, and grouping by account leaves 4,459 rows. The answer is tiny. In Supriya's framing, optimization is the argument about how many of those 400,000 rows the database has to touch to find it.

Ask the database with EXPLAIN QUERY PLAN

Instead of guessing, the video runs EXPLAIN QUERY PLAN. It costs nothing to run, and it does not run your query. It only reports what the engine intends to do. In a real terminal, query A shows a scan of the postings table, and scan means every row, all 400,000. Query B shows a search of postings using an index, with the date range in parentheses, which means the index was walked to January and stopped.

The plan said so before either query ran. Read as a comparison on a log scale, that is 400,000 rows against 11,369, which is 35 times fewer. Because the answer is only 4,459 rows, plan B sits near the floor, while plan A is two orders of magnitude away.

Why the function breaks the index

Both queries filter on the same indexed column, so what is it about query A's WHERE clause that takes the index away? The answer is the function. Query A wraps the posting date in strftime, so the database no longer has a column to look up. It has an expression, and the only way to evaluate it is to visit every row. Query B leaves the column bare and compares it to constants.

The insight is that an index works on the column, not on a function of the column.

Rewrite to a bare range

The fix the video demonstrates is to rewrite the predicate as a bare column range comparison. That one move took the read from 400,000 rows down to 11,000. The same trap applies to other wrappers: strftime, CAST, and UPPER are named as common culprits.

Key takeaways

  • Two queries with identical output can differ 35 times in rows read.
  • EXPLAIN QUERY PLAN shows the engine's intent without running the query.
  • The word SCAN means every row is read; SEARCH using an index means the index narrowed the work.
  • Wrapping an indexed column in a function turns the predicate into an expression the index cannot serve.
  • strftime, CAST, and UPPER are common ways this happens.
  • Rewrite the predicate as a bare column compared to constants, such as a date range.
  • Ask for the smallest rewrite so you can see which single change moved the plan.

Try it yourself

Run EXPLAIN on your slowest dashboard query tonight and look for the word SCAN on a table you thought was indexed. Then give Claude the table schema and the query with this prompt: walk me through its explain plan step by step, tell me which steps read every row, find any predicate that a function or cast has made unusable by the index, and give me the smallest rewrite that turns a scan into a search.

Chapters

  1. 0:00Two identical SQL outputs, 35x difference in rows read
  2. 0:20Running `EXPLAIN QUERY PLAN` to spot full table scans
  3. 0:45Why wrapping indexed columns in functions disables index lookups
  4. 1:10Bare column range comparisons: Turning scans into index searches
  5. 1:35Prompting Claude to diagnose and rewrite slow analytics queries
Full transcript(auto-generated, with timestamps)

Two identical SQL outputs, 35x difference in rows read

[0:00]Hi, I am Sapriya, and this video is about SQL query optimization for analytics. Two queries, identical output, the same 4,459 rows, byte for byte. One of them reads 11,000 rows to get there. The other reads 400,000. Nothing in the SQL tells you which is which. The query plan does. Here is the

Running `EXPLAIN QUERY PLAN` to spot full table scans

[0:22]Shape of every analytics query. 400,000 postings. You want 1 month. 11,000 rows survive the filter, and grouping by account leaves 4,459. The answer is tiny. Optimization is the argument about how many of those 400,000 the database touches to find it. So, stop guessing and ask the database. Explain query plan

Why wrapping indexed columns in functions disables index lookups

[0:46]Costs nothing to run, and it does not run your query. It just tells you what the engine intends to do. There it is from a real terminal. Query A, scan postings. Scan means every row, all 400,000. Query B, search postings using index with the date range in the parentheses. Search means the index walked to January and stopped. Same answer, and the plan

Bare column range comparisons: Turning scans into index searches

[1:11]Said so before either query ran. On a log scale, the gap stops being an opinion. 400,000 rows read against 11,369, 35 times fewer. And the answer itself is only 4,459 rows, so plan B is near the floor. Plan A is two decades away. Before I show you why, commit to an answer. Both queries filter on the same indexed

Prompting Claude to diagnose and rewrite slow analytics queries

[1:37]Column. So, what is it about query A's where clause that takes the index away? The function. Query A wraps posted at an STRF time, so the database no longer has a column to look up. It has an expression, and the only way to evaluate it is to visit every row. Query B leaves the column bare and compares it to constants. That is the insight. An index works on the column, not on a function of the column. Let's recap with Claude. One takeaway you can use today. Before you optimize an analytics query, run explain and look for the word scan. If it shows up on a table you thought was indexed, check your where clause for a function wrapped around the

Indexed column, strftime, cast, upper, and rewrite it as a bare range. That one move took 400,000 rows down to 11,000. I'm learning to analyze analytics SQL. Here is the schema for one of my tables and the slowest query I run against it. Walk me through its explain plan step by step. Tell me which steps read every row. Find any predicate that a function or a cast has made unusable by the index. And give me the smallest rewrite that turns a scan into a search. Ask for the smallest rewrite, not a rewritten query. You want to see which single change moved the plan. Run it on your slowest dashboard query tonight. SQL read fewer rows. Thanks for watching. Supriya for Humanitarian's AI.

More from Humanitarians AI Fellows

Humanitarians AI Lyrical Literacy Project