Why can an index on last_name not supply the order for ORDER BY LOWER(last_name)?
answer
- an index stores one particular ordering
- the engine cannot assume functions keep sequence
- some wrappers can simply be deleted
- case folding really does reorder values
- watch for expressions hidden in a view or alias
basics
~10 sThe index stores the sort order of last_name, not of LOWER(last_name), and lowercasing can reorder values under a case-sensitive collation. Since the engine cannot assume a function preserves order, it sorts the result instead.
solid answer
~40 sAn index is an ordered list of *column values*. It tells the engine the sequence of `last_name`, and nothing about the sequence of any function applied to it. Because a function may reorder its inputs arbitrarily — under a case-sensitive collation `'Zeta'` sorts before `'alpha'`, but `LOWER` swaps them — the engine cannot reuse the index order and adds a sort. The practical distinction is whether your expression is order-preserving. `total_cents / 100.0`, `col + 1` and `CAST(ts AS DATE)` never move a value ahead of a smaller one, so `ORDER BY` the bare column produces an ordering that satisfies the original request and is index-friendly. `LOWER(name)`, `COALESCE(shipped_at, created_at)` and `CASE` expressions genuinely reorder rows; those need a case-insensitive collation, an index on the expression, or an accepted sort.
code
sql · 7 lines-- index on invoices(total_cents)
-- expression around the ordering key: forces a sort of every matching row
SELECT * FROM invoices ORDER BY total_cents / 100.0 DESC FETCH FIRST 20 ROWS ONLY;
-- dividing by a positive constant preserves order, so the wrapper can go
SELECT * FROM invoices ORDER BY total_cents DESC FETCH FIRST 20 ROWS ONLY;go deeper
Remember that an index knows the order of the raw column values only. Wrapping the ordering column in a function means the database has to sort.
Explain why the optimiser cannot assume a function preserves order, and separate order-preserving wrappers you can delete from ones like LOWER or COALESCE that you cannot.
Spot expressions you did not type — hidden in a view, a select-list alias, or an implicit conversion — and weigh the collation change, generated column, and accepted sort against each other for the real data volume.
Decide the policy: which orderings the product commits to, and whether case-insensitive presentation ordering should be solved once in the column's collation rather than repeatedly in query text across many teams.
## An index records one specific ordering A B-tree index on `last_name` is a sorted list of the stored `last_name` values (in the column's collation) plus pointers to rows. Reading it yields exactly one ordering: by `last_name`. When a query asks for rows ordered by `LOWER(last_name)`, that is a different ordering of the same rows, and the index holds no information about it. The optimiser will not — and cannot safely — assume that applying a function leaves the sequence intact, so it plans a sort. This is not a limitation of a particular engine; it follows from what the structure stores. The consequence for the query author is sharp: an expression around an ordering key silently converts a cheap ordered walk into a blocking sort, which matters most when a row limit sits on top, because that limit then bounds the result without bounding the work. ## Order-preserving transforms you can strip Many expressions people write are strictly increasing over the column's domain. If `f` never moves a value ahead of a smaller one, then ordering by the bare column already satisfies ordering by `f(col)`, and you can simply delete the wrapper: ```sql -- index on invoices(total_cents) SELECT * FROM invoices ORDER BY total_cents / 100.0 DESC FETCH FIRST 20 ROWS ONLY; -- sorts SELECT * FROM invoices ORDER BY total_cents DESC FETCH FIRST 20 ROWS ONLY; -- index walk ``` Dividing by a positive constant, adding a constant, and multiplying by a positive constant are all order-preserving. So is truncating a timestamp to a date: ordering by the full timestamp is a *refinement* of ordering by its date part, and since rows sharing a date may legally come back in any sequence, the refined ordering is still a correct answer to the original request. Ordering by an alias of a computed select-list column has the same trap and the same cure — check what the alias expands to. One related rewrite is worth naming: `ORDER BY -price` expresses descending order through arithmetic. Replace it with `ORDER BY price DESC`, which the index can serve. Be careful if the column is nullable, because engines that place NULLs at opposite ends for `ASC` and `DESC` will move the NULL rows; add an explicit `NULLS FIRST` or `NULLS LAST` if their position matters — and then confirm you have not reintroduced a sort by asking for a placement the index does not physically have. ## Transforms you cannot strip Other expressions genuinely reorder rows, and no rewriting of the `ORDER BY` will make the plain column index apply: - **`LOWER(name)` / `UPPER(name)`** under a case-sensitive collation. Case-sensitive orderings typically group by case, so `'Zeta' < 'alpha'` while `LOWER` makes `'alpha' < 'zeta'`. Different sequence, different ordering. - **`COALESCE(shipped_at, created_at)`** — the value being ordered comes from two columns, so no single-column index describes it. - **`CASE WHEN status = 'URGENT' THEN 0 ELSE 1 END`** — a business-priority ordering that exists nowhere in the stored data. - **String manipulation** such as `SUBSTRING(code FROM 4)`, which reorders on a suffix the index knows nothing about. For these the options are: change the column's collation so the plain ordering *is* the one you want (a schema decision with wide consequences); persist the computed value, either as a generated column or by having the schema owner create an index on the expression; or accept the sort because the result set is small enough for it not to matter. The middle option is index-design work — as the query author your contribution is to state precisely which expression must be ordered. ## Watch for expressions you did not write An expression can arrive without being typed by you. Ordering by a select-list alias that names a computed column is one route. A comparison across mismatched types is another: if the engine has to convert a value to compare or order it, it is ordering something other than the stored column. Sorting on a column of a derived table or view whose definition wraps the column in a function is a third, and the most easily missed, because the query text you are reading looks perfectly bare. ## The habit to build When a query has a row limit and feels slower than the number of returned rows justifies, read the `ORDER BY` list and ask of each key: *is this the literal name of a stored column?* If not, decide whether the expression is order-preserving — if it is, delete it; if it is not, you are choosing between a schema change and a sort.
- Why is ORDER BY CAST(created_at AS DATE), then, safe to replace with ORDER BY created_at?Because truncating a timestamp to its date never moves a later timestamp ahead of an earlier one. Ordering by the full timestamp is a stricter ordering that still satisfies ordering by the date part — rows sharing a date are simply returned in a specific sequence rather than an arbitrary one, which the original request permits.
- You must order by LOWER(last_name) and cannot change the query. What are the options?Three, all outside the query text: give the column a case-insensitive collation so the plain ordering already matches; store the folded value in a generated column and order by that; or have an index created on the expression itself so the engine has the folded order available. Otherwise accept the sort and keep the input small with a selective filter.
- Is ORDER BY -price a good way to sort descending?No. It is an expression, so an index on `price` cannot supply the order, and it can also relocate NULL rows because engines may default NULLs to opposite ends for ASC and DESC. Write `ORDER BY price DESC`, adding an explicit `NULLS FIRST`/`NULLS LAST` if the placement of NULLs is part of the requirement.
saying these in an interview costs you the question
- Assumes an index on the column also orders any function of it
- Says the optimiser will simplify the expression away
- Claims LOWER cannot change the ordering of strings
- Sorts with -col instead of DESC
- Misses that a view or alias is hiding the expression