SQL Query Optimization Practice with Indexes and EXPLAIN
This capstone lesson brings together every concept from this module — views, indexes, cardinality, covering indexes, clustered vs. secondary indexes, and reading EXPLAIN — through a hands-on diagnose-and-fix workflow. Each practice scenario presents a deliberately slow or poorly-indexed query, walks through diagnosing the problem using EXPLAIN, and applies the correct fix.
Key Definitions
- Diagnose-fix-verify workflow: The standard, disciplined performance tuning process of first using EXPLAIN to understand a query's actual execution plan, then applying a targeted fix (usually an index), and finally re-running EXPLAIN to confirm the fix genuinely improved the plan.
What You'll Learn
- Diagnose a slow query's root cause using EXPLAIN's output columns.
- Apply the correct indexing fix based on that diagnosis.
- Verify the fix's effectiveness by re-running EXPLAIN.
- Practice this full diagnose-fix-verify workflow across several realistic scenarios.
Detailed Explanation
Scenario 1 — Missing JOIN index: `SELECT a.appointment_id, d.doctor_name FROM appointments a JOIN doctors d ON a.doctor_id = d.doctor_id` runs slowly on a large appointments table. Diagnosis: EXPLAIN shows type = ALL for the appointments table, key = NULL, since appointments.doctor_id has no index despite being used in the JOIN condition. Fix: CREATE INDEX idx_appointments_doctor ON appointments(doctor_id), directly applying the Lesson 9.14 principle. Verify: re-running EXPLAIN now shows type = ref and key = idx_appointments_doctor for that table.
Scenario 2 — Low-cardinality index ignored: A query filtering `WHERE status = 'Completed'` runs slowly despite an index existing on status. Diagnosis: EXPLAIN's key column shows NULL even with the index present, because status has very low cardinality (only a few distinct values), and MySQL's optimizer correctly judged a full scan more efficient than following that particular index, exactly the Lesson 9.11 scenario. Fix: this isn't actually a bug to fix through indexing — instead, consider whether a composite index combining status with a higher-cardinality column (like appointment_date) would better serve the actual query pattern, or accept that this specific low-cardinality filter genuinely doesn't benefit from indexing alone.
Scenario 3 — Unnecessary filesort: `SELECT * FROM appointments ORDER BY appointment_date` shows 'Using filesort' in EXPLAIN despite appointment_date seeming like an obvious indexing candidate. Diagnosis: no index currently exists on appointment_date at all. Fix: CREATE INDEX idx_appointments_date ON appointments(appointment_date). Verify: re-running EXPLAIN should show the filesort eliminated, since the B-Tree index's inherently sorted structure can now directly satisfy the ORDER BY.
Scenario 4 — Covering index opportunity: A dashboard query `SELECT doctor_id, doctor_name FROM doctors WHERE doctor_name LIKE 'Dr. S%'` runs extremely frequently. Diagnosis: an index on doctor_name alone would still require a secondary lookup against the clustered index to retrieve doctor_id, as covered in Lesson 9.12. Fix: create a composite covering index on (doctor_name, doctor_id) instead, directly applying the Lesson 9.13 technique, letting this frequent query be satisfied by the index alone.
Visual Summary
A four-step circular workflow diagram: [1. Run EXPLAIN on the slow query] --> [2. Read type, key, rows, and Extra columns to diagnose the root cause] --> [3. Apply a targeted fix: add/adjust an index] --> [4. Re-run EXPLAIN to verify the fix worked] --> back to step 1 for the next query.
Quick Reference
| Scenario | EXPLAIN Diagnosis | Applied Fix |
|---|---|---|
| Slow JOIN on appointments/doctors | type=ALL, key=NULL on the JOIN column | Index the foreign key: appointments(doctor_id) |
| Slow filter on low-cardinality status | key=NULL despite an existing index | Reconsider approach; low cardinality limits index usefulness |
| ORDER BY causing filesort | Extra: Using filesort | Index the sorted column: appointments(appointment_date) |
| Frequent narrow dashboard query | Secondary index requires extra lookup | Build a covering composite index for that specific query |
SQL Example
CREATE TABLE departments (
department_id INT PRIMARY KEY AUTO_INCREMENT,
department_name VARCHAR(100) NOT NULL
);
CREATE TABLE doctors (
doctor_id INT PRIMARY KEY AUTO_INCREMENT,
doctor_name VARCHAR(100) NOT NULL,
department_id INT,
salary INT,
FOREIGN KEY (department_id) REFERENCES departments(department_id)
);
CREATE TABLE patients (
patient_id INT PRIMARY KEY AUTO_INCREMENT,
patient_name VARCHAR(100) NOT NULL,
city VARCHAR(80)
);
CREATE TABLE appointments (
appointment_id INT PRIMARY KEY AUTO_INCREMENT,
patient_id INT,
doctor_id INT,
appointment_date DATE,
status VARCHAR(20),
FOREIGN KEY (patient_id) REFERENCES patients(patient_id),
FOREIGN KEY (doctor_id) REFERENCES doctors(doctor_id)
);
CREATE TABLE bills (
bill_id INT PRIMARY KEY AUTO_INCREMENT,
appointment_id INT,
amount DECIMAL(10,2),
paid BOOLEAN DEFAULT FALSE,
FOREIGN KEY (appointment_id) REFERENCES appointments(appointment_id)
);
INSERT INTO departments VALUES
(1, 'Cardiology'), (2, 'Orthopedics'), (3, 'Neurology'), (4, 'Dermatology');
INSERT INTO doctors (doctor_id, doctor_name, department_id, salary) VALUES
(101, 'Dr. Verma', 1, 95000), (102, 'Dr. Iyer', 2, 78000),
(103, 'Dr. Sen', 3, 120000), (104, 'Dr. Khan', 1, 88000);
INSERT INTO patients VALUES
(201, 'Amit Rao', 'Pune'), (202, 'Neha Joshi', 'Mumbai'),
(203, 'Karan Mehta', 'Delhi'), (204, 'Divya Nair', 'Pune');
INSERT INTO appointments VALUES
(301, 201, 101, '2026-05-01', 'Completed'),
(302, 202, 102, '2026-05-02', 'Completed'),
(303, 203, 101, '2026-05-03', 'Cancelled'),
(304, 204, 103, '2026-05-04', 'Completed');
INSERT INTO bills (appointment_id, amount, paid) VALUES
(301, 1500.00, TRUE), (302, 2200.00, FALSE),
(303, 800.00, TRUE), (304, 3000.00, TRUE);
-- SCENARIO 1: Diagnose and fix a missing JOIN index
EXPLAIN SELECT a.appointment_id, d.doctor_name
FROM appointments a JOIN doctors d ON a.doctor_id = d.doctor_id;
-- Diagnosis: type=ALL, key=NULL on appointments
CREATE INDEX idx_appointments_doctor ON appointments(doctor_id);
EXPLAIN SELECT a.appointment_id, d.doctor_name
FROM appointments a JOIN doctors d ON a.doctor_id = d.doctor_id;
-- Verify: type=ref, key=idx_appointments_doctor
-- SCENARIO 3: Diagnose and fix an unnecessary filesort
EXPLAIN SELECT * FROM appointments ORDER BY appointment_date;
-- Diagnosis: Extra = Using filesort
CREATE INDEX idx_appointments_date ON appointments(appointment_date);
EXPLAIN SELECT * FROM appointments ORDER BY appointment_date;
-- Verify: Using filesort should no longer appear
-- SCENARIO 4: Build a covering index for a frequent narrow dashboard query
CREATE INDEX idx_doctors_name_id ON doctors(doctor_name, doctor_id);
EXPLAIN SELECT doctor_id, doctor_name FROM doctors WHERE doctor_name LIKE 'Dr. S%';
-- Verify: Extra = Using index (fully covered, no table lookup needed)
Each scenario follows the exact diagnose-fix-verify workflow this capstone teaches: an initial EXPLAIN reveals a specific, identifiable problem (a missing JOIN index, a required filesort, or a secondary-index lookup that could be eliminated), a targeted fix is applied using the precise indexing technique that problem calls for, and a follow-up EXPLAIN confirms the fix produced the expected improvement in the execution plan — turning abstract module concepts into a concrete, repeatable, real-world performance tuning process.
Real-World Examples
- This exact diagnose-fix-verify workflow is the standard operating procedure for database performance engineers responding to slow query reports in production systems.
- Technical interviews for backend, database, and DevOps roles frequently present candidates with a slow query and ask them to walk through exactly this kind of EXPLAIN-driven diagnosis and fix.
- Database monitoring and observability tools are built specifically to surface queries exhibiting the EXPLAIN warning signs practiced in this capstone — full scans, missing filesort-avoiding indexes, and uncovered high-frequency queries.
Common Mistakes to Avoid
- Adding indexes speculatively without first diagnosing the actual bottleneck through EXPLAIN.
- Skipping the verification step, assuming a fix worked without confirming it through a follow-up EXPLAIN.
- Applying a generic indexing fix without considering cardinality, join columns, or covering index opportunities specific to the actual query pattern.
Interview Questions
Q1. Walk through your general process for diagnosing and fixing a slow SQL query.
Start by running EXPLAIN to understand the current execution plan, paying close attention to the type, key, rows, and Extra columns. Identify the specific bottleneck — a missing index on a JOIN or filter column, an unnecessary filesort, or a secondary index requiring an extra lookup — apply a targeted fix such as a new or composite index, and then re-run EXPLAIN to confirm the fix actually improved the plan before considering the optimization complete.
Q2. If EXPLAIN shows 'Using filesort' for a query with an ORDER BY, what would you check first?
I'd check whether an index already exists on the ORDER BY column or columns. If not, adding an appropriate index is often the direct fix, since a B-Tree index's inherently sorted structure can satisfy the ORDER BY directly, eliminating the need for MySQL to perform a separate, explicit sorting step.
Practice MCQs
1. What is the correct first step in the diagnose-fix-verify workflow for a slow query?
- Immediately add an index to every column
- Run EXPLAIN to understand the current execution plan
- Rewrite the entire query from scratch
- Restart the database server
Answer: B. Run EXPLAIN to understand the current execution plan
Explanation: The workflow always begins with EXPLAIN to accurately diagnose the specific bottleneck before applying any targeted fix, rather than guessing or making broad, unverified changes.
2. After applying a fix like adding an index, what is the final step in this workflow?
- Deleting the original query
- Re-running EXPLAIN to verify the fix actually improved the execution plan
- Immediately adding more indexes just in case
- Restarting the diagnosis from scratch every time
Answer: B. Re-running EXPLAIN to verify the fix actually improved the execution plan
Explanation: Verification through a follow-up EXPLAIN confirms the applied fix produced the intended improvement, completing the disciplined diagnose-fix-verify cycle.
Quick Revision Points
- The diagnose-fix-verify workflow (EXPLAIN, apply fix, EXPLAIN again) is the standard, disciplined approach to query optimization.
- This capstone integrates JOIN indexing, cardinality awareness, filesort elimination, and covering indexes into one practical process.
- Practical EXPLAIN-driven diagnosis is a frequently tested skill in backend and database technical interviews.
Conclusion
- Real query optimization follows a disciplined diagnose-fix-verify cycle centered on EXPLAIN.
- Different symptoms (missing JOIN index, filesort, uncovered frequent queries) call for different, specific indexing fixes.
- This capstone integrates every module concept into one practical, repeatable performance tuning workflow.
This capstone lesson practices the complete, disciplined query optimization workflow — diagnose with EXPLAIN, apply a targeted fix, then verify with EXPLAIN again — across four realistic scenarios drawn directly from this module's concepts: a missing JOIN index causing a full scan, a low-cardinality column where indexing provides limited benefit, an ORDER BY triggering an avoidable filesort, and a frequent narrow query that benefits from a purpose-built covering index. Working through this diagnose-fix-verify cycle transforms the module's individual concepts — views, index types, cardinality, clustered versus secondary indexes, and covering indexes — into one coherent, practical, real-world performance tuning skill.