Hook: Interviewers love this because a tiny rewrite can turn a full scan of millions of rows into a fast index range scan.
Question: What is query rewrite in SQL, and how does it help performance?
Answer: Query rewrite means changing a SQL statement into an equivalent form that the database can execute more efficiently. The main goal is to keep indexed columns easy to search, push filters earlier, and avoid hiding a column inside a function or expression. In practice, that often changes a slow table scan into a much smaller index seek or index range scan.
Interview-Ready Answer: I use query rewrite to make the same result cheaper to compute. My first check is whether the predicate is sargable, meaning the database can use an index for it, so I try to keep the indexed column on its own and move calculations to the other side. For example, instead of wrapping created_at in a date function, I rewrite it as a half-open time range, which usually turns a full scan into a fast index range scan. I also watch for NULLs and outer joins so I do not accidentally change the result set.
Query rewrite is not magic; it is a deliberate way to express the same logic in a form the optimizer can execute better. A predicate is a filter condition, like a WHERE clause. A sargable predicate is one the database can use with an index instead of reading rows one by one. Think of it as giving the engine a short route instead of asking it to inspect every house on the street.
| Slow shape | Rewrite | Why it helps |
|---|---|---|
DATE(col)=... | Range on col | Keeps index usable |
COUNT(*)>0 | EXISTS | Stops at first match |
LOWER(col)=... | Normalized column | Avoids function on column |
Two key ideas are worth remembering: first, rewriting is about shape, not just syntax; second, the optimizer is smart, but it is not psychic. It cannot always infer your business meaning if you hide it inside functions, casts, or awkward joins.
Use query rewrite when a query is slow because it filters too late, hides an indexed column inside an expression, or forces the engine to read far more rows than needed. Typical examples include date filtering, string matching, existence checks, and join simplification. If the query already uses a good index and reads only a small part of the table, rewriting may not help much, so check the execution plan instead of guessing.
A full table scan is roughly O(N) because the engine checks many or all rows. A B-tree index lookup is closer to O(log N + K), where K is the number of matched rows. On a table with 10 million rows, reading 5,000 rows from one day is usually far cheaper than scanning the whole table. In real systems, a good rewrite can cut latency from seconds to milliseconds and reduce I/O, CPU, and connection-pool pressure. Also remember selectivity: if a predicate matches 70% of the table, a scan may actually be cheaper than bouncing through the index many times.
NULL is not equal to anything, even another NULL.WHERE into ON can change which rows survive.UNION ALL or IN, but only if duplicates and semantics are safe.Memory rule: if the filter is on the indexed column itself, the index has a chance; if the column is wrapped in a function, the index often cannot help.
Imagine an e-commerce checkout service with an endpoint that shows today’s paid orders. A developer writes WHERE DATE(created_at) = CURRENT_DATE because it looks clean. During a sale, the orders table grows to tens of millions of rows, the query starts scanning huge chunks of data, and the API p95 jumps from 40 ms to 3 seconds. The app’s connection pool of 20 gets exhausted, new requests queue up, and users see spinning loaders and timeout errors.
The logs show repeated slow-query warnings, high CPU on the database, and requests timing out while waiting for a connection. The fix is a rewrite to a half-open range on created_at, which keeps the index usable and makes the query touch only the rows for that day. The outage lesson is simple: a query can be logically correct and still be operationally expensive.
The scary part is that the bug is not obvious in code review. The query returns the right rows, but it asks the database to do much more work than necessary. That is exactly why interviewers like this topic: it tests whether you can connect SQL shape to production behavior.
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
created_at TIMESTAMP,
status VARCHAR(20) NOT NULL
);
CREATE INDEX idx_orders_created_at ON orders(created_at);
INSERT INTO orders (order_id, customer_id, created_at, status) VALUES
(1, 101, TIMESTAMP '2026-07-11 09:15:00', 'PAID'),
(2, 102, TIMESTAMP '2026-07-11 23:59:59', 'PAID'),
(3, 103, TIMESTAMP '2026-07-12 00:00:00', 'PAID'),
(4, 104, NULL, 'PENDING');
-- Bad shape: wrapping the indexed column in a function makes the predicate non-sargable.
-- Many engines must scan far more rows because the index stores raw timestamps, not DATE(created_at).
SELECT order_id, status
FROM orders
WHERE CAST(created_at AS DATE) = DATE '2026-07-11';
-- Better shape: keep the indexed column naked and move the work to constant boundaries.
-- This is logically equivalent for a single day and is friendly to a B-tree index on created_at.
SELECT order_id, status
FROM orders
WHERE created_at >= TIMESTAMP '2026-07-11 00:00:00'
AND created_at < TIMESTAMP '2026-07-12 00:00:00';
-- Edge case: NULL timestamps match neither predicate, which is usually what you want for real created-time rows.
SELECT order_id
FROM orders
WHERE created_at IS NULL;
Follow-up & Tricky Questions:
EXISTS better than IN? EXISTS is often better for correlated checks because the engine can stop at the first match. Modern optimizers sometimes transform both into similar plans, so the data shape matters more than the keyword alone.OR conditions? Sometimes you can rewrite to UNION ALL so each branch can use a different index. That only works if duplicate handling and semantics are safe.WHERE col + 1 = 5 a gotcha? Because the arithmetic is on the column side, so the index on col is usually harder to use. Rewrite it to col = 4 so the engine can search directly.BETWEEN always safe for timestamps? Not always. For date-time values, an inclusive end boundary can accidentally include midnight of the next day, so the safer pattern is a half-open range: start inclusive, end exclusive.LEFT JOIN into an INNER JOIN always work? No. It only preserves results when unmatched rows are impossible or irrelevant; otherwise you lose rows that should have been kept with NULLs on the right side.Common Mistakes:
NOT IN and outer joins.Memory Hook: Keep the indexed column naked. If you dress it up in a function, the index cannot recognize it quickly.
Cheat Sheet:
EXISTS for existence checks when possible.Practice Tasks:
DATE(created_at) into an index-friendly range filter.COUNT(*) > 0 check into EXISTS and compare the plan.