Why query optimization matters
Efficient SQL queries reduce I/O, lower CPU usage, and improve application responsiveness. In production systems, small improvements in query design often translate to large savings in latency and infrastructure costs.
1. Use indexes wisely
Indexes speed up lookups but add overhead on INSERT
, UPDATE
, and DELETE
. Add them to columns used in WHERE
, JOIN
, and ORDER BY
, but avoid over-indexing.
(customer_id, created_at)
for queries filtering by customer_id
then sorting by created_at
).2. Avoid SELECT *
Fetching every column increases I/O and memory usage. Select only the columns you need to reduce network transfer and speed up row reads.
-- Bad
SELECT * FROM orders WHERE status = 'paid';
-- Better
SELECT id, customer_id, total_amount, paid_at
FROM orders
WHERE status = 'paid';
3. Check your joins
Ensure join conditions use indexed columns with matching data types. Unindexed or type-mismatched joins often force full table scans and large temporary result sets.
-- Example: join on indexed keys
SELECT o.id, o.total_amount, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.created_at > '2025-01-01';
4. Use EXPLAIN
to understand execution plans
Run EXPLAIN <your_query>
to see how MySQL executes a query — whether it uses indexes, performs full scans, or creates temporary tables. Fix the hotspots revealed by EXPLAIN
.
EXPLAIN SELECT id FROM orders WHERE customer_id = 42;
5. Limit and paginate efficiently
For large result sets, use LIMIT
and indexed ordering. Prefer keyset pagination (also called cursor pagination) over OFFSET
for better performance on deep pages.
-- OFFSET based (can be slow on large offset)
SELECT id, total_amount FROM orders ORDER BY created_at DESC LIMIT 50 OFFSET 1000;
-- Keyset pagination (faster)
SELECT id, total_amount FROM orders
WHERE created_at < '2025-09-01 12:00:00'
ORDER BY created_at DESC
LIMIT 50;
6. Cache results for read-heavy workloads
Frequently-run, expensive read queries can be cached using an application-level cache (Redis, Memcached) or MySQL query cache alternatives. Cache invalidation when data changes is the key challenge — design sensible TTLs or event-driven invalidation.
Quick checklist before deploying
- Run
EXPLAIN
on slow queries - Add/remove indexes based on query patterns
- Avoid
SELECT *
and unnecessary joins - Use keyset pagination for deep paging
- Measure impact (latency, CPU, I/O) before/after changes
Small changes often bring large gains — a targeted index or a rewritten join can cut query time from seconds to milliseconds. Happy optimizing!