Reading EXPLAIN
EXPLAIN SELECT * FROM students WHERE department = 'CSE';
EXPLAIN ANALYZE SELECT … ; -- MySQL 8.0.18+: actually runs it and reports real timings
EXPLAIN FORMAT=JSON SELECT … ; -- full cost details
Key columns:
| Column | What to look for |
|---|---|
type | The access method — see the ranking below |
key | Which index was chosen; NULL means none |
rows | Estimated rows examined — lower is better |
filtered | Percentage of rows left after the WHERE |
Extra | Warnings such as Using filesort, Using temporary |
type, best to worst: system → const → eq_ref → ref → range → index → ALL.
ALL is a full table scan and index is a full index scan. Either one on a large table is where you start optimising.
Extra values worth reacting to:
Using index— good: a covering index, no row lookup needed.Using where— normal.Using filesort— the sort could not use an index; consider indexing theORDER BYcolumns.Using temporary— an internal temp table, common withGROUP BYon an unindexed column.
The slow query log
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- seconds
SET GLOBAL log_queries_not_using_indexes = 'ON';
SELECT @@slow_query_log_file;
Analyse the file with mysqldumpslow or pt-query-digest. Also useful:
SELECT * FROM sys.statements_with_full_table_scans LIMIT 10;
SELECT * FROM sys.statement_analysis ORDER BY total_latency DESC LIMIT 10;
SHOW PROCESSLIST; -- what is running right now
Optimisation checklist
- Index what you filter, join and sort on. This fixes most slow queries.
- Select only the columns you need — it enables covering indexes and cuts network traffic.
- Keep the
WHEREsargable — no functions or arithmetic wrapped around indexed columns. - Replace
ORon different columns withUNION ALLof two indexed queries when the optimizer refuses to use an index. - Avoid
SELECT COUNT(*)on huge tables for pagination; use an approximate count or keyset pagination. - Batch writes. One 1000-row
INSERTbeats 1000 single inserts. - Use
EXISTSinstead ofINfor large, correlated subqueries. - Delete in chunks —
DELETE … LIMIT 10000in a loop instead of one giant statement that holds locks for minutes.
Rewriting examples
-- before: cannot use the index on created_at
SELECT * FROM logs WHERE DATE(created_at) = '2026-09-17';
-- after
SELECT * FROM logs
WHERE created_at >= '2026-09-17' AND created_at < '2026-09-18';
-- before: deep offset reads and discards 100k rows
SELECT * FROM posts ORDER BY id LIMIT 20 OFFSET 100000;
-- after: keyset pagination
SELECT * FROM posts WHERE id > 100000 ORDER BY id LIMIT 20;
Server-level tuning
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
SHOW GLOBAL STATUS LIKE 'Threads_connected';
The single most important setting is innodb_buffer_pool_size — on a dedicated database server, set it to roughly 60–75% of RAM so the working set stays in memory. After that, look at max_connections, tmp_table_size and connection pooling in the application.
Optimise in this order: schema and data types, then indexes, then the query, then server configuration, then hardware. Measure before and after each change — assumptions about what is slow are wrong more often than not.