Cheat SheetsMySQLQuery Optimisation

Query Optimisation — Cheat Sheet

MySQL · 3 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Query Optimisation
MySQL3 topicsQuick revision reference
1

EXPLAIN & Query Analysis

EXPLAIN shows the execution plan: type (const, eq_ref, ref, range, ALL), key used, rows scanned, and Extra flags like Using filesort, Using temporary — key inputs for tuning.

  • `type: ALL` means a full table scan — always investigate; add an index or rewrite the query.
  • Access type ladder (best → worst): const → eq_ref → ref → range → index → ALL.
  • `Using filesort` and `Using temporary` in Extra are red flags for missing indexes on ORDER BY / GROUP BY columns.
  • A composite index (a, b) covers `WHERE a = ? ORDER BY b` — no filesort needed if column order matches.
  • EXPLAIN ANALYZE (MySQL 8.0+) reveals actual vs estimated row counts — use it when estimates look wrong.
  • Run `ANALYZE TABLE` after bulk inserts to refresh the InnoDB statistics used by the optimiser.
SQL — EXPLAIN basics
-- Sample schema
CREATE TABLE orders (
  id         INT PRIMARY KEY AUTO_INCREMENT,
  user_id    INT NOT NULL,
  status     VARCHAR(20),
  created_at DATETIME,
  INDEX idx_user (user_id),
  INDEX idx_status_created (status, created_at)
);

-- 1. Full table scan — no usable index on 'amount'
EXPLAIN SELECT * FROM orders WHERE amount > 100;
-- type: ALL, key: NULL, rows: ~50000, Extra: Using where

-- 2. Range scan — leftmost prefix of composite index
EXPLAIN SELECT * FROM orders WHERE status = 'PENDING' AND created_at > '2024-01-01';
-- type: range, key: idx_status_created, rows: ~200, Extra: Using index condition

-- 3. eq_ref in a JOIN — primary key lookup
EXPLAIN
  SELECT o.id, u.email
  FROM orders o
  JOIN users u ON u.id = o.user_id
  WHERE o.status = 'PENDING';
-- orders: type: ref, key: idx_status_created
-- users:  type: eq_ref, key: PRIMARY
2

Query Optimisation Techniques

Add indexes, rewrite correlated subqueries as JOINs, avoid SELECT *, use LIMIT, avoid functions on indexed columns in WHERE, and leverage the query cache (or ProxySQL) carefully.

  • Wrap an indexed column in a function (YEAR(), LOWER(), etc.) in a WHERE clause and the index cannot be used — rewrite as a range.
  • Correlated subqueries re-execute per outer row; rewrite as a JOIN with a pre-aggregated subquery for O(1) vs O(n) performance.
  • SELECT * prevents covering-index optimisation and fetches unnecessary data — always select only needed columns.
  • LIMIT/OFFSET pagination degrades at high offsets; use keyset (cursor) pagination with WHERE id > last_seen_id for consistent performance.
  • Implicit type conversions (string literal on INT column) silently disable indexes — always match parameter types to column types.
  • Use EXPLAIN ANALYZE after every optimisation to confirm the plan changed as expected and estimate improvements.
SQL — Core Optimisation Rules
-- ❌ BAD: function on indexed column — full scan
SELECT * FROM orders WHERE YEAR(created_at) = 2024;
-- EXPLAIN type: ALL (no index)

-- ✅ GOOD: rewrite as range — index used
SELECT id, total FROM orders
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
-- EXPLAIN type: range, key: idx_created_at

-- ❌ BAD: implicit type conversion on user_id INT column
SELECT * FROM users WHERE user_id = '456';  -- string vs INT → full scan

-- ✅ GOOD: matching type
SELECT id, email FROM users WHERE user_id = 456;

-- ❌ BAD: SELECT * over wide table
SELECT * FROM products WHERE category_id = 5;

-- ✅ GOOD: select only needed columns — can use covering index
SELECT id, name, price FROM products WHERE category_id = 5;
3

Slow Query Log

Enable slow_query_log with a long_query_time threshold to capture expensive queries; use mysqldumpslow or pt-query-digest to identify and rank the worst offenders.

  • Enable slow_query_log at runtime with SET GLOBAL — no MySQL restart needed.
  • Start with long_query_time=1 in production; lower to 0.1–0.5 once you've fixed the worst offenders.
  • log_queries_not_using_indexes captures harmful full-table scans even when they are fast on small datasets.
  • Use pt-query-digest (not mysqldumpslow) for production analysis — it gives full statistics, percentiles, and can output to a report table.
  • Sort by total time (not average) to identify the highest-impact queries — a 0.1s query called 100 000 times matters more than a 5s query called once.
  • The slow log is discovery; EXPLAIN is diagnosis; adding indexes/rewriting is the fix.
SQL / my.cnf — Slow Query Log Setup
-- Enable at runtime (no restart needed)
SET GLOBAL slow_query_log      = 'ON';
SET GLOBAL long_query_time     = 1;      -- log queries taking > 1 second
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
SET GLOBAL log_queries_not_using_indexes = 'ON';  -- also catch full-table scans

-- Verify settings
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';

-- Persist in my.cnf (survives restart)
[mysqld]
slow_query_log          = ON
slow_query_log_file     = /var/log/mysql/slow.log
long_query_time         = 0.5      # 500ms threshold for busy production apps
log_queries_not_using_indexes = ON
log_throttle_queries_not_using_indexes = 10  # limit to 10 per minute (avoids log flood)

-- Check how many slow queries have been captured
SHOW GLOBAL STATUS LIKE 'Slow_queries';
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/mysql