Query Optimization Techniques
Writing a query that returns the right answer is the first step; writing one that returns it quickly, even as the underlying tables grow, is a separate skill. Most real-world query optimization comes down to a small set of recurring techniques — giving the optimizer indexes it can actually use, avoiding patterns that accidentally defeat those indexes, and being deliberate about how much data is fetched and processed.
Technique | Why it helps |
|---|---|
Index columns that are frequently filtered, joined, or sorted on | Lets the database avoid scanning the whole table for common access patterns |
Avoid SELECT * | Fetches only the columns actually needed, reducing I/O and enabling index-only scans |
Avoid wrapping indexed columns in functions inside WHERE | Lets the optimizer use an index instead of forcing a full scan to evaluate the function on every row |
Prefer EXISTS over IN for large subqueries | Lets the database stop as soon as one match is found, instead of materializing a full subquery result |
Batch large INSERT/UPDATE/DELETE operations | Avoids long-held locks and huge transaction logs from a single massive statement |
Filter as early as possible | Reduces the number of rows carried through subsequent joins and aggregations |
Avoid SELECT *
Requesting every column, even ones the application never uses, forces the database to read and transfer more data than necessary, and can prevent an index-only scan that would otherwise satisfy the query entirely from the index.
Naming only the columns actually needed
-- Fetches every column, even unused ones SELECT * FROM orders WHERE customer_id = 42; -- Fetches only what the application actually uses SELECT order_id, order_date, order_total FROM orders WHERE customer_id = 42;
Avoid wrapping indexed columns in functions
Applying a function to an indexed column inside WHERE usually forces the database to evaluate that function on every row before it can compare the result, which defeats a plain index on the column — the index is sorted by the raw column value, not by the function's output.
Sargable vs non-sargable filtering on an indexed date column
-- Non-sargable: wraps order_date in a function, index on order_date can't be used directly SELECT * FROM orders WHERE YEAR(order_date) = 2026; -- Sargable: compares order_date directly against a range, index can be used SELECT * FROM orders WHERE order_date >= '2026-01-01' AND order_date < '2027-01-01';
Prefer EXISTS over IN for large subqueries
For large subqueries, EXISTS can stop as soon as it finds the first matching row for a given outer row, whereas some databases handle a large IN list by fully materializing the subquery's result first. On small, well-indexed subqueries the two are often optimized identically, but EXISTS tends to remain efficient as the subquery grows.
EXISTS vs IN for a large subquery
-- IN: may require the full subquery result to be built first SELECT customer_name FROM customers c WHERE c.customer_id IN ( SELECT o.customer_id FROM orders o WHERE o.order_total > 1000 ); -- EXISTS: can stop at the first match per outer row SELECT customer_name FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id AND o.order_total > 1000 );
Batch large operations
A single UPDATE or DELETE touching millions of rows can hold locks for a long time and generate a huge amount of transaction log activity in one go, which can block other queries and make rollback expensive if something goes wrong. Splitting the operation into smaller batches, each in its own transaction, keeps individual locks short-lived.
Batching a large DELETE instead of running it all at once
-- Risky as a single statement against a huge table DELETE FROM logs WHERE created_at < '2020-01-01'; -- Batched: repeat this in a loop until zero rows are affected DELETE FROM logs WHERE created_at < '2020-01-01' LIMIT 10000;
Index the columns that are actually filtered, joined, or sorted on in real queries.
Select only the columns needed instead of SELECT *.
Keep indexed columns free of wrapping functions in WHERE clauses so the optimizer can use the index.
Prefer EXISTS over IN for large subqueries.
Batch large write operations to avoid long-held locks and huge transactions.
Confirm every optimization actually changes the query plan with EXPLAIN.