Skip to content
wannacode

Slow queries

In short: A query stays slow despite an index when the column is inside a function or LIKE, when it selects every column, when an OR also tests a column without an index, or when it counts everything to answer "is there any".

Problem

The index is there, and it is still slow

The yearly report counts the visits in 2026. There is an index on seen_at. Run the query and look at the time:

SQLdatabase: shop

And the plan:

SQLdatabase: shop

SCAN visits USING COVERING INDEX. The database reads the index because it is smaller than the table, but it reads all of it: all 300,000 entries. There is no search.

The idea

The column has to stand alone

The index is sorted by seen_at. It is not sorted by substr(seen_at, 1, 4). To check the condition, the database has to compute the function for every entry, and for that it has to go through all of them.

The same condition can be written with bounds, where the column stands alone on one side:

SQLdatabase: shop
SQLdatabase: shop

The result is the same, and the plan is SEARCH. Compare the times of the two queries.

The same goes for arithmetic. customer_id + 0 = 1234 gives the same as customer_id = 1234, but the index does not help:

SQLdatabase: shop

The rule: do not touch the column in WHERE. Arithmetic moves to the other side (price > 10 / 1.2, not price * 1.2 > 10), and a function on a date becomes a range.

Try it

October

The field holds the query from the yearly report. The monthly report wants the visits in October 2026. Write the condition so that it uses the index on seen_at.

SQLdatabase: shop

See it

LIKE and the index

It looks as if LIKE '2026-10%' should use the index: the start is known, as in a phone book. Run the plan:

SQLdatabase: shop

In SQLite it is SCAN. The reason: LIKE in SQLite ignores the difference between upper and lower case, while the index is sorted with that difference. SQLite uses an index for LIKE 'start%' only under certain conditions, for example when the column or the index is COLLATE NOCASE. For dates the reliable way is the range from the previous card.

A pattern that starts with % cannot use an ordinary index in any database. The start is unknown, so the search has nowhere to begin:

SQLdatabase: shop
More precisely, if you are curious

The exception to the rule about functions is an index on an expression: CREATE INDEX idx_visits_year ON visits(substr(seen_at, 1, 4)) makes WHERE substr(seen_at, 1, 4) = '2026' a SEARCH again. The index only helps queries with the same expression.

In SQLite, GLOB '2026-10*' is case-sensitive and does use the index (the plan is SEARCH). So does PRAGMA case_sensitive_like = ON, but it changes the behaviour of every LIKE on the connection.

In PostgreSQL an ordinary index helps with LIKE 'start%' only with the C collation or when it is created with text_pattern_ops. For searching in the middle of a text there are separate kinds of indexes (the pg_trgm extension). Full-text search is in the module on kinds of databases.

The idea

Only the columns you need

The program shows on which dates customers 1000 to 1100 visited. Two queries with the same rows. Compare the times:

SQLdatabase: shop
SQLdatabase: shop

The second one is faster, because customer_id and seen_at are in the index (customer_id, seen_at), and it is covering: the database does not open the table. SELECT * also wants product_id, so for each of the 6,060 entries the database goes to the table too. Put in SELECT only the columns the program actually uses. That way less data travels to the program as well.

The idea

OR and IN

OR on the same column is the same as IN. SQLite runs them the same way:

SQLdatabase: shop

The trap is OR across different columns. There is no index on product_id. Rows with product_id = 4 can be anywhere in the table, so the database reads everything, and the index for the customer does not help:

SQLdatabase: shop

If both columns have an index, SQLite searches each one separately and combines the results (the plan says MULTI-INDEX OR). Without an index for one half it stays SCAN.

The idea

“Is there any”, not “how many”

The page of product 13 shows “Popular” if anyone has viewed it. Counting finds all 12,000 visits:

SQLdatabase: shop

The page needs only “yes” or “no”. EXISTS stops at the first row it finds:

SQLdatabase: shop

There is no index on product_id, but EXISTS is still fast, because the first visit of the product is near the start of the table. If there is no such row, EXISTS will also read everything. So here too an index helps. Another way is SELECT 1 FROM … WHERE … LIMIT 1: an empty result means “no”.

Exercise

Two tasks

1. The field holds the old query for 2026. Customer 1234 asks how many times they looked at products in 2025. Write it so that the plan is SEARCH on both the customer and the date.

Customer 1234’s visits in 2025, with SEARCH on both columns:

SQLdatabase: shop

2. The “View again” button is shown if customer 4321 has viewed a product from 1 October 2026 onwards. Return 1 or 0 in a column seen.

Does customer 4321 have visits from 2026-10-01 onwards?

SQLdatabase: shop

Test

Check yourself

1. There is an index on seen_at. Which condition can use it for a search?
2. Why does WHERE customer_id + 0 = 1234 not use the index on customer_id?
3. The program needs to know whether the customer has viewed the product at all. Which query fits best?
4. There is an index on customer_id and none on product_id. What is the plan of WHERE customer_id = 1234 OR product_id = 4?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·