skip to content

Comparing INTEGER id to '42' stays index-friendly but VARCHAR code to 42 does not — why?

level: middleimportance: should knowfreq 52%

answer

  1. One predicate converts a constant, the other a column
  2. Count how many times the conversion runs
  3. Type precedence picks the loser, not the cheaper side
  4. An index stores values, not derived expressions
  5. Move the CAST to the literal side

basics

~20 s

Whichever side is converted decides. Converting a constant happens once and leaves the column bare, so its index still works. Converting the column happens per row and produces a derived value no index on that column stores.

solid answer

~50 s

Both predicates mix a character and a numeric type, but engines that resolve the mismatch pick a direction, and the direction is what matters. Where data-type precedence applies, numeric outranks character, so the *character* operand loses. With `user_id = '42'` on an INTEGER column that operand is the literal: it is converted once, the column stays bare, and an index on `user_id` is still searchable. With `part_code = 42` on a VARCHAR column the loser is the column itself, so the engine evaluates a conversion for every row and filters on a value no index over `part_code` contains. The practical rule for a query author is therefore: **keep the column bare and put any explicit CAST on the literal or parameter side**. If the values themselves are not convertible — a code column holding `'X-99'` — casting the column is not just slow, it can fail.

code

sql · 8 lines
sql
-- users.user_id INTEGER: the LITERAL is converted, once. Index on user_id still usable.
SELECT * FROM users WHERE user_id = '42';

-- parts.part_code VARCHAR(20): the COLUMN is converted, once per row. Index unusable.
SELECT * FROM parts WHERE part_code = 42;

-- Fix: move the conversion to the constant side
SELECT * FROM parts WHERE part_code = '42';

go deeper

for a junior

Learn the rule and apply it mechanically: leave the column alone, adjust the literal to match its type. If you find yourself typing CAST around a column name in a WHERE clause, stop and reconsider.

for a middle

Explain the mechanics: precedence decides who converts, a constant converts once while a column converts per row, and the conversion generally does not preserve ordering. That last point is why not even a range scan survives.

for a senior

Show the failure mode beyond speed — a cast on a column can raise conversion errors on rows other predicates appear to exclude, because evaluation order is not guaranteed and can change with the plan.

for a principal

Own the upstream call: recurring mismatches mean a column is declared in the wrong type. Weigh a type-changing migration against permanently defensive query conventions, and decide where the correctness is enforced.

## Two mismatches that look identical and behave oppositely ```sql -- users.user_id INTEGER SELECT * FROM users WHERE user_id = '42'; -- parts.part_code VARCHAR(20) SELECT * FROM parts WHERE part_code = 42; ``` Both statements put a character value next to a numeric one. Standard SQL calls those types not mutually comparable, and engines that accept the statement must convert one side. The first query is typically harmless; the second is a classic production slowdown. The difference is entirely about *which operand gets converted*. ## How the direction is decided Oracle and SQL Server publish a data-type precedence list. When two operands of different types meet, the one with the lower precedence is converted to the higher. Numeric types sit above character types, so the character operand is always the one that changes. PostgreSQL takes a different route: an untyped quoted literal is resolved to whatever type the context demands, so `'42'` simply becomes an integer next to an integer column — while an already-integer literal next to a `varchar` column has no resolution at all and raises an error. MySQL converts both operands to floating point when a string column meets a number. Different mechanisms, same practical outcome: the constant is cheap to move, the column is not. ## Why converting a constant is free and converting a column is fatal A literal or bind value is one value. The engine converts it once, before the scan begins, and then compares the *stored* column values against it. The index on that column is an ordered structure over exactly those stored values, so it can be searched directly. A column conversion is an expression evaluated once per row. Two things break. First, the filter now tests a derived value that the index does not contain, so there is nothing to search. Second, the conversion usually does not preserve order — as text `'9'` sorts after `'12345'`, as numbers it does not — so the engine cannot even walk a contiguous slice of the index. It must read everything and convert everything. ## The rule: cast the literal, never the column ```sql -- BAD: explicit cast on the column, same damage as the implicit one SELECT * FROM parts WHERE CAST(part_code AS INTEGER) = 42; -- GOOD: make the constant match the column's declared type SELECT * FROM parts WHERE part_code = '42'; -- GOOD: explicit cast when you want the intent visible SELECT * FROM parts WHERE part_code = CAST(42 AS VARCHAR(20)); ``` The same rule governs parameters: bind `'42'` as a character parameter for a character column and as an integer for an integer column, rather than binding whatever type the application variable happens to have. ## Casting the column can also be wrong, not merely slow Suppose `part_code` holds `'X-99'` for some rows and a query reads `WHERE category = 'NUMERIC' AND CAST(part_code AS INTEGER) = 42`. It is tempting to argue that the first predicate excludes the unconvertible rows. SQL makes no such promise: the engine may evaluate the predicates in either order, or evaluate the cast while scanning before the category filter is applied. The statement can therefore fail with a conversion error on rows the query was never meant to return, and — worse — it can start failing later, when a plan change reorders the evaluation. Converting the constant has no such hazard, because there is exactly one constant and it either converts at parse time or it does not. ## When the real answer is the schema If the mismatch keeps recurring, the column type is wrong. A `VARCHAR` column that only ever holds integers and is only ever compared to integers should be an integer column: the comparison becomes natural, the index shrinks, and the database starts rejecting garbage instead of storing it. If the column genuinely holds mixed codes, then the *callers* are wrong to compare it to numbers, and the fix is to correct the literals and the bindings. ## What to check Read the predicate text the plan prints, not just the operator name. A conversion function wrapped around a column — `CONVERT_IMPLICIT(...)`, `TO_NUMBER(...)`, `(part_code)::integer` — is the tell, and it applies equally whether you wrote the cast or the engine did.

  • If the column must be cast for the comparison to make sense at all, what are your options?
    Three. Change the column's declared type so the comparison is natural — usually the right answer. Or convert the constant to the column's type instead, if the values are genuinely comparable that way. Failing both, create an index on the same expression you are filtering by, so the derived value is what the index stores; that is index design work and it costs writes, so treat it as a last resort.
  • Does an explicit CAST on the column behave any differently from the engine's implicit one?
    No — the cost and the index consequences are identical, because both produce a per-row derived value. The only difference is honesty: an explicit cast is visible in code review, whereas an implicit one hides until someone reads the execution plan. Making the conversion explicit is a good habit, but write it on the constant side.

saying these in an interview costs you the question

  • Thinks a CAST is cheap because it is one keyword
  • Says the engine converts the smaller value automatically
  • Assumes WHERE predicates run left to right
  • Believes an explicit cast is safer than the implicit one
  • Fixes a mismatch by casting both sides

context