Lesson 121 of 12140 min read

SQL Query Optimization Practice with Indexes and EXPLAIN

A hands-on capstone applying every indexing and EXPLAIN concept from this module to diagnose and fix a set of deliberately slow queries.

Author: CodersNexus

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

ScenarioEXPLAIN DiagnosisApplied Fix
Slow JOIN on appointments/doctorstype=ALL, key=NULL on the JOIN columnIndex the foreign key: appointments(doctor_id)
Slow filter on low-cardinality statuskey=NULL despite an existing indexReconsider approach; low cardinality limits index usefulness
ORDER BY causing filesortExtra: Using filesortIndex the sorted column: appointments(appointment_date)
Frequent narrow dashboard querySecondary index requires extra lookupBuild 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?

  1. Immediately add an index to every column
  2. Run EXPLAIN to understand the current execution plan
  3. Rewrite the entire query from scratch
  4. 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?

  1. Deleting the original query
  2. Re-running EXPLAIN to verify the fix actually improved the execution plan
  3. Immediately adding more indexes just in case
  4. 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.

Frequently Asked Questions

Run EXPLAIN to diagnose the current execution plan, identify the specific bottleneck (like a missing index, a filesort, or an uncovered query), apply a targeted fix such as a new index, and then re-run EXPLAIN to verify the fix genuinely improved the plan.

Check with EXPLAIN whether the JOIN condition's columns are indexed, particularly the foreign key side, since foreign keys are not automatically indexed in MySQL and this is one of the most common causes of slow JOINs.

Add an index on the column or columns used in the ORDER BY clause, since a B-Tree index's sorted structure can often satisfy the sort order directly, eliminating the need for MySQL's separate, explicit sorting step.

When a specific query runs extremely frequently and only needs a small, well-defined set of columns, building a composite covering index that includes all of them lets MySQL satisfy the entire query from the index alone.

Verification confirms that an applied fix genuinely produced the intended improvement in the execution plan, rather than assuming it worked, ensuring the optimization effort actually achieved its goal.