What makes a WHERE predicate sargable, and what shape must the filtered column take?
answer
- short for search argument able
- look at which side the column sits on
- functions and arithmetic hide the column
- the other side must be a constant
- keep the column bare
basics
~20 sA predicate is sargable when the column appears bare on one side of a comparison and the other side evaluates to a constant, so the engine can turn it into an index range. Wrapping the column in a function or arithmetic destroys that.
solid answer
~50 s"Sargable" is short for *search argument able*: the predicate can be reduced to a **search argument** the engine hands to an index — "start here, walk until there". That requires the column to stand alone on one side of `=`, `<`, `<=`, `>`, `>=`, `BETWEEN`, `IN`, or a left-anchored `LIKE 'abc%'`, with the other side an expression that does not mention that column and can be evaluated before the scan begins. `WHERE created_at >= DATE '2024-01-01'` qualifies; `WHERE YEAR(created_at) = 2024`, `WHERE price * 100 > 500` and `WHERE UPPER(name) = 'SMITH'` do not, because the index stores the column's values, not the values of some function of them. Non-sargable predicates are still *correct* — they just force the engine to produce every row and test it, and they usually wreck its row estimates too.
go deeper
Be ready to look at a WHERE clause and say whether the column stands alone or is wrapped in something. Knowing the rewrite for a function on a date column already puts you ahead of most screening candidates.
Explain why the index cannot help: it stores column values, not the values of a function of them, and the engine cannot invert an arbitrary expression. Name the four standard rewrites and which functions have no rewrite at all.
Show the diagnosis habit: spot the wrapped column in a slow report, confirm from the plan that the predicate became a per-row filter, and quantify the estimate damage that follows a filter the optimizer has no statistics for.
Own the prevention side. Decide whether normalizing on write, indexing an expression, or a review rule is the right lever for a given codebase, and be honest that sargability is a necessary condition, not a performance strategy on its own.
## What the word means "Sargable" is a contraction of **search argument able**, terminology inherited from early relational-engine work. A *search argument* is something the engine can hand to an index to position itself: a starting point and a stopping condition. A predicate is sargable when it can be reduced to that shape. Anything else is a **filter**: the engine must first produce the row by some other means, then evaluate the expression on it and throw the row away if it fails. This is a property of how you *wrote the predicate*, not of the data and not of the index. It is the one performance lever that lives entirely in the text of the query. ## The shape rule A sargable predicate looks like: ```sql col = 42 col > 100 col BETWEEN 10 AND 20 col IN (1, 2, 3) col LIKE 'abc%' col >= CURRENT_DATE - INTERVAL '7' DAY ``` A non-sargable predicate looks like: ```sql YEAR(col) = 2024 CAST(col AS DATE) = DATE '2024-03-01' UPPER(col) = 'SMITH' col * 100 > 500 col + INTERVAL '30' DAY < CURRENT_DATE SUBSTRING(col FROM 1 FOR 3) = 'ABC' COALESCE(col, 0) > 10 ``` The test is mechanical: **if the column sits inside a function call or an arithmetic expression, the predicate is not a range over that column.** Note that the *other* side may be as complicated as you like — `CURRENT_DATE - INTERVAL '7' DAY` is fine, because it is computed once, before any row is touched, and does not mention the column. Two related shapes fail for the same underlying reason. `WHERE col_a > col_b` mentions two columns of the same row, so neither index gets a constant boundary. And a comparison that forces the *column* to be converted to another type — a numeric literal against a character column, say — is the engine wrapping your column in a cast for you; that is the type-mismatch case, and the cure is the same rule: keep the column bare and put the conversion on the literal. ## Why the engine cannot see through the function An index on `col` stores `col`'s values in sorted order. It does not store `UPPER(col)` or `YEAR(col)`. To use the index for `YEAR(col) = 2024` the engine would have to *invert* the function — work out which stored values map to 2024 — and there is no general way to invert an arbitrary expression. So the planner treats `f(col)` as an opaque value it can only compute row by row. Some engines special-case a handful of functions, and some let you index the expression itself, but nothing about that is portable: assume no engine sees through the wrapper unless you have proved otherwise on yours. ## What it actually costs Correctness is untouched — the non-sargable form returns exactly the same rows. What changes is the access path. On a fifty-million-row table it is the difference between reading a handful of index pages and reading every page of the table while calling a function on each row. There is a second, quieter cost: the optimizer normally keeps statistics on *columns*, not on expressions, so it falls back to a default guess for `f(col) = x`. A bad row estimate propagates into join order and memory sizing, so the damage can extend well past the one table you filtered. ## The rewrite catalogue Almost every non-sargable predicate falls into one of four repairs: 1. **Truncation → half-open range.** `CAST(ts AS DATE) = DATE '2024-03-01'` becomes `ts >= DATE '2024-03-01' AND ts < DATE '2024-03-02'`. 2. **Arithmetic → move it to the constant side.** `price * 1.2 > 120` becomes `price > 120 / 1.2`; `d + INTERVAL '30' DAY < CURRENT_DATE` becomes `d < CURRENT_DATE - INTERVAL '30' DAY`. 3. **Prefix extraction → prefix match.** `SUBSTRING(code FROM 1 FOR 3) = 'ABC'` becomes `code LIKE 'ABC%'`. 4. **Not invertible at all** (`UPPER`, `COALESCE`, a hash) → normalize the data on write and compare the bare column, or move the fix into the schema with an indexed expression or generated column. ## Sargable is necessary, not sufficient A sargable predicate is not a promise of an index seek. There must actually be an index whose leading column is the one you filtered, and the predicate must be selective enough that seeking beats scanning; a filter matching a third of a large table is often served faster by reading the table straight through, and that is a legitimate plan. The point of sargability is narrower and worth stating precisely: it keeps the *option* open. A non-sargable predicate takes the choice away from the engine before it has made it.
- Does making a predicate sargable guarantee the engine will use an index?No. Sargability only preserves the option. There must still be an index whose leading column is the one you filtered, and the predicate must be selective enough that seeking and fetching rows beats reading the table straight through. A sargable filter matching most of a table will, and should, still be served by a scan.
- Is a non-sargable predicate ever wrong, or only slow?Only slow. The engine evaluates the expression on every row and returns exactly the rows the predicate describes, so results are identical. That is precisely why the bug is easy to miss in review and in tests on small data — it surfaces as a latency regression once the table grows, never as a wrong answer.
- Why does a complex expression on the right-hand side not break sargability?Because it does not mention the filtered column, so the engine can evaluate it once before the scan starts and use the result as a range boundary. `created_at >= CURRENT_DATE - INTERVAL '7' DAY` computes one timestamp, then seeks. The rule is about the column's side, not about how complicated the comparison value is.
saying these in an interview costs you the question
- Thinks a non-sargable predicate returns different or wrong rows
- Believes the optimizer rewrites YEAR(col) = 2024 into a range automatically
- Says any WHERE clause on an indexed column is sargable
- Adds a CAST around the column to make the comparison safer
- Assumes a sargable predicate always produces an index seek