skip to content

questions

5

How do you make WHERE email LIKE '%@example.com' index-friendly?

level: middleimportance: must knowfreq 60%

answer

  1. the pattern gives no starting point
  2. an index seek needs a known prefix
  3. a suffix is a prefix backwards
  4. or model the searched part as a column
  5. reversing does nothing for both-ends wildcards

basics

~20 s

Turn the suffix match into a prefix match. Either store the part you actually search on in its own column and compare with =, or keep an indexed reversed copy of the value and match the reversed pattern, which anchors the search at the start of the string.

solid answer

~60 s

A pattern beginning with `%` gives the engine no known prefix, so an ordered index has no range to seek and the pattern is evaluated per row. Two portable rewrites recover a prefix: **Model the thing you search for.** If you always search by domain, store `domain` as its own column, index it, and write `WHERE domain = 'example.com'`. Equality on a real column beats any pattern trick, and the column can be maintained by the application or a generated column. **Reverse the string.** For a genuine suffix search, keep an indexed reversed copy and search a reversed prefix: `WHERE email_reversed LIKE 'moc.elpmaxe@%'`. The literal is reversed once when the query is built. This only works because a suffix is a prefix of the reversed value, so it does nothing for `'%example%'`, where neither end is anchored. Also check the pattern is not cargo cult: if the requirement was really "starts with", `LIKE 'term%'` is seekable as written. True infix search is a different problem that engines solve with specialised search facilities.

code

sql · 8 lines
sql
-- Anti-pattern: no prefix for the index to seek on
SELECT id, email FROM users WHERE email LIKE '%@example.com';

-- Fix 1: model the searched attribute, then use equality
SELECT id, email FROM users WHERE domain = 'example.com';

-- Fix 2: indexed reversed copy turns the suffix into a prefix
SELECT id, email FROM users WHERE email_reversed LIKE 'moc.elpmaxe@%';

go deeper

for a junior

Recall that a pattern starting with % has no prefix for an index to start from, and that a pattern like 'term%' does. Know that searching by domain is better served by a domain column.

for a middle

Produce both rewrites and explain the identity behind the reversed column: a suffix of a value is a prefix of its reverse. State clearly that this does nothing for patterns wildcarded at both ends.

for a senior

Weigh the maintenance cost of a duplicated, always-in-sync column against the measured cost of the scan, and decide when modelling the searched attribute as a first-class column is the real fix.

for a principal

Own the boundary: which search requirements the relational schema should serve directly, and which belong to a dedicated search capability rather than being simulated with pattern tricks.

## Why the leading % is the problem A B-tree index stores values in sorted order, and a seek works by narrowing to a range of that order. `LIKE 'abc%'` is a range: every match sorts between `abc` and the next value after the `abc` prefix. `LIKE '%abc'` is not: matching values are scattered throughout the sort order, because sorting is by first character and the pattern says nothing about the first character. With no prefix to anchor on, the predicate can only be checked value by value. So the rewrite goal is precise and mechanical: **manufacture a known prefix**. ## Rewrite 1 — store what you actually search for The strongest fix is usually schema, not syntax. If the application searches by email domain, the domain is a real attribute of the row and deserves a column: ```sql ALTER TABLE users ADD COLUMN domain VARCHAR(255); -- populated on write, or as a generated column where the engine supports it CREATE INDEX idx_users_domain ON users (domain); SELECT id, email FROM users WHERE domain = 'example.com'; ``` This is equality on a bare column: the most index-friendly predicate there is. It also makes the intent readable, allows a composite index with other filters, and lets you count domains, group by them, and constrain them. Generated columns are standard SQL, though the exact syntax and whether the value is stored or computed on read varies by engine; a plain column maintained by the application is the fully portable version. ## Rewrite 2 — the reversed column When the search is genuinely "ends with" and you cannot decompose the value, exploit a small identity: a suffix of a string is a prefix of the reversed string. Keep a reversed copy of the column, index it, and reverse the search literal when you build the query: ```sql -- email_reversed holds 'moc.elpmaxe@ecila' for '[email protected]' CREATE INDEX idx_users_email_rev ON users (email_reversed); SELECT id, email FROM users WHERE email_reversed LIKE 'moc.elpmaxe@%'; ``` The pattern now has a known prefix, so a range seek is possible. Points to raise unprompted: - The SQL standard does not define a `REVERSE` function; several engines provide one, so maintaining the column may be the application's job for full portability. - The reversed value must be kept in sync on every write. A generated column does that for you where available; otherwise a write path can drift, and drift here means silently missing rows. - The literal must be reversed by whatever builds the query, which is easy to get wrong by hand and belongs in one helper. - It costs storage and index maintenance on a second copy of the data. ## Rewrite 3 — check the pattern was needed at all A surprising share of leading wildcards are habit. A "search by name" box implemented as `LIKE '%' || :term || '%'` often only ever needed "starts with". If the product requirement allows it, `LIKE :term || '%'` is seekable exactly as written, with no schema change and no trick. Ask before you engineer. ## What none of these fix Infix search — `LIKE '%example%'` — has no anchor at either end, so neither the original nor the reversed column offers a prefix. Reversing helps suffixes only. Genuine substring search over text is what engines' dedicated search facilities exist for, and choosing among those is an engine-specific decision outside the portable-SQL toolbox. Say that plainly rather than pretending a B-tree trick covers it. A related non-fix: adding `DISTINCT`, changing `LIKE` to a comparison operator, or reordering the `WHERE` clause. None of them create a prefix. ## Choosing between them Ask what the search means. If the suffix corresponds to a real concept — domain, file extension, country code at the end of a code, area code — model it as a column; you will want to filter, group and constrain on it eventually anyway. If it is an arbitrary tail of an opaque string and the table is large enough that a scan hurts, the reversed column earns its keep. If the table is small, or the query runs once a day in a report, do nothing: an anti-pattern that costs 30 milliseconds on 20,000 rows is not worth a second copy of a column. ## In an interview State the mechanism in one sentence (a leading wildcard leaves no prefix to seek), then give the two rewrites in order of preference, then the honest limit: reversing handles suffixes, not infixes. Volunteering the maintenance cost of the reversed column, and the question of whether the leading `%` was ever required, is what distinguishes an engineer from someone reciting a trick.

  • Does the reversed-column trick help WHERE email LIKE '%example%'?
    No. That pattern is anchored at neither end, so the reversed value has no known prefix either — you have simply built a second column that also needs scanning. Reversal converts a suffix search into a prefix search and nothing more. Substring search across a large text column is what an engine's dedicated search facilities exist for.
  • What has to be true for a reversed column to stay correct?
    Every write path must maintain it: inserts, updates, bulk loads and any direct data fixes. A generated column enforces that in the engine where supported; otherwise application code owns it and one forgotten path silently makes rows unfindable. Also keep the reversal of the search literal in a single helper, so query-building code cannot disagree with the stored form.
  • When would you leave the leading-wildcard LIKE alone?
    When the scanned set is small — a lookup table, a filtered subset already narrowed by another indexed predicate, or a table of a few thousand rows — or when the query is rare and not latency-sensitive. Both rewrites add a maintained copy of data or a schema change, so they need a measured cost to justify them.

An index is a phone book sorted by surname. "Everyone whose surname starts with Mc" is a page range you can flip to; "everyone whose surname ends in -son" means reading every page. Keeping a second book sorted by reversed surname turns the second question back into a page range.

saying these in an interview costs you the question

  • Says any index makes LIKE '%x' fast
  • Claims reversing the column also fixes '%x%' searches
  • Forgets to reverse the search literal too
  • Leaves the reversed column unmaintained on updates
  • Adds an index to the original column and calls it fixed

context

open as a page

How do you rewrite WHERE city = 'Berlin' OR zip = '10115' so each predicate can use its index?

level: middleimportance: must knowfreq 55%

basics

~20 s

Split the OR into two separate SELECT statements, each with a single-column predicate, and combine them with UNION. Each branch can then be served by its own index, while UNION removes the rows that satisfy both conditions and would otherwise appear twice.

open as a page

Why is adding SELECT DISTINCT to make duplicate rows disappear a performance smell?

level: juniorimportance: should knowfreq 55%

basics

~20 s

SELECT DISTINCT deduplicates the entire result after the joins have already run, so the engine sorts or hashes every row produced just to throw most of them away. It hides the real cause, usually a fan-out join, instead of removing duplicates at the source.

open as a page

What goes wrong when an application sends WHERE id IN (...) with 50,000 literal values?

level: middleimportance: should knowfreq 42%

basics

~20 s

A huge literal IN list makes the statement text enormous, so it must be transmitted and parsed on every call, each different list length is a different statement, some engines impose a hard limit on list size, and row estimates degrade. Pass the set as a table and join instead.

open as a page

A search endpoint uses WHERE (:city IS NULL OR city = :city) for each optional filter — what does that cost, and how do you fix it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

One statement covering every combination of supplied and omitted filters forces a single plan that must be valid when any parameter is NULL, so no index can be committed to and the engine typically scans. Build the statement from the filters actually supplied, binding values as parameters.

open as a page