27. Indexes & Performance Tuning -- Speeding Up Queries
Level: Advanced SQL Server for real projects
- What indexes do (speed up searches)
- Clustered vs nonclustered indexes
- CREATE INDEX syntax
- Composite indexes (multiple columns)
- Indexes on foreign keys
- Execution plans (estimate query cost)
- Identify missing indexes
- Index fragmentation
- Common indexing mistakes
- Best practices
Queries scan tables. Indexes speed up searches. This article teaches creating indexes and reading execution plans.
Why Indexes Matter for Backend Developers
Performance impacts user experience:
Without index:
WHERE StudentId = 5 -> Scans all 100K rows -> Slow
With index:
WHERE StudentId = 5 -> Index lookup -> Fast (microseconds)
v
Queries run 100x faster
APIs respond instantly
Indexes are table of contents. Dramatically speed searches.
Index Concepts
Clustered Index: Physical row order. One per table (usually PRIMARY KEY).
StudentId (clustered): Rows sorted by StudentId physically
Nonclustered Index: Fast lookup pointer. Many per table.
Name (nonclustered): Index points to rows, keeps them unsorted physically
Example 1: Create Nonclustered Index
Speed up student lookup by name:
CREATE NONCLUSTERED INDEX IX_Student_Name
ON Student (Name);
-- Query now fast:
SELECT * FROM Student WHERE Name = 'Ravi Kumar';
Before: Table scan (all rows). After: Index seek (direct to row).
Naming convention: IX_TableName_Columns
Example 2: Composite Index (multiple columns)
Index on class and marks:
CREATE NONCLUSTERED INDEX IX_ExamResult_StudentId_MarksObtained
ON ExamResult (StudentId, MarksObtained DESC);
-- Fast query:
SELECT * FROM ExamResult
WHERE StudentId = 5
ORDER BY MarksObtained DESC;
Composite index: (StudentId, MarksObtained). First column filters, second orders.
Example 3: Foreign Key Index
Index on ForeignKey (required for JOINs):
-- Foreign key should always be indexed
CREATE NONCLUSTERED INDEX IX_Subject_TeacherId
ON Subject (TeacherId);
-- Fast JOIN:
SELECT s.Name, t.Name
FROM Subject s
INNER JOIN Teacher t ON s.TeacherId = t.TeacherId;
Rule: Index all foreign keys. JOINs use them constantly.
Example 4: Execution Plan (estimate cost)
Check query performance:
-- Enable actual plan
SET STATISTICS IO ON;
SELECT s.Name, r.MarksObtained
FROM Student s
INNER JOIN ExamResult r ON s.StudentId = r.StudentId
WHERE s.ClassName = '10-A';
-- Check output: Logical Reads (lower = faster)
-- Logical Reads: 2 (good)
Cost indicators:
- Low: <1 (instant)
- Medium: 1-10 (fast)
- High: >10 (slow, needs index)
Look for: Table Scan (slow) vs Index Seek (fast).
Example 5: Missing Index Analysis
Find missing indexes in SQL Server:
-- Query slow without index:
SELECT * FROM ExamResult WHERE MarksObtained > 80;
-- SQL Server suggests missing index:
-- Missing Index: ON ExamResult (MarksObtained)
-- Create it:
CREATE NONCLUSTERED INDEX IX_ExamResult_MarksObtained
ON ExamResult (MarksObtained);
-- Re-run query: Now fast (index seek)
Check execution plan for "Missing Index" warning.
Example 6: Covering Index (INCLUDE columns)
Include columns to avoid lookup:
-- Without covering index:
SELECT Name, MarksObtained
FROM ExamResult er
WHERE er.StudentId = 5;
-- Index has StudentId, but needs to look up Name (extra I/O)
-- With covering index:
CREATE NONCLUSTERED INDEX IX_ExamResult_StudentId
ON ExamResult (StudentId)
INCLUDE (MarksObtained);
-- Index has both StudentId and MarksObtained (no lookup needed)
-- Query now faster: Index covers it
Covering index: INCLUDE other columns needed by query. Eliminates extra lookups.
Example 7: Index Fragmentation
Check and rebuild fragmented index:
-- Check fragmentation:
SELECT
OBJECT_NAME(ips.object_id) AS TableName,
i.name AS IndexName,
ips.avg_fragmentation_in_percent AS Fragmentation
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') ips
INNER JOIN sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_id
WHERE ips.avg_fragmentation_in_percent > 10
ORDER BY Fragmentation DESC;
-- Result (fragmented):
TableName | IndexName | Fragmentation
Student | IX_Student_Name | 25%
Fragmentation > 10%: Rebuild.
-- Rebuild fragmented index:
ALTER INDEX IX_Student_Name ON Student REBUILD;
-- Check again: Fragmentation now ~0%
Example 8: Drop Unused Index
Remove unnecessary indexes:
-- Find unused indexes:
SELECT
OBJECT_NAME(i.object_id) AS TableName,
i.name AS IndexName,
s.user_seeks + s.user_scans + s.user_lookups AS TimesUsed
FROM sys.indexes i
LEFT JOIN sys.dm_db_index_usage_stats s ON i.object_id = s.object_id AND i.index_id = s.index_id
WHERE OBJECTPROPERTY(i.object_id, 'IsUserTable') = 1
AND (s.user_seeks + s.user_scans + s.user_lookups = 0 OR s.user_seeks IS NULL)
ORDER BY TimesUsed;
-- Drop unused:
DROP INDEX IX_OldUnusedIndex ON TableName;
Remove indexes never used. Save space, improve INSERT/UPDATE.
Common Indexing Mistakes
Mistake 1: Index every column
Wrong:
CREATE INDEX ON Student (StudentId);
CREATE INDEX ON Student (Name);
CREATE INDEX ON Student (ClassName);
CREATE INDEX ON Student (RollNumber);
-- Too many indexes: slow INSERT/UPDATE, wastes space
Result: 4 indexes x 100K rows = bloat.
Fix: Index strategically:
-- Only frequently searched columns
CREATE INDEX ON Student (StudentId); -- JOIN, WHERE
CREATE INDEX ON Student (ClassName); -- Filter, GROUP BY
-- Name, RollNumber: less frequently searched
Mistake 2: Index low-cardinality columns
Wrong:
CREATE INDEX ON Student (Gender);
-- Only 2 values (M/F): Index scan as fast as table scan
Result: Wasted space.
Fix: Index high-cardinality (many unique values):
CREATE INDEX ON Student (RollNumber); -- Unique per student
CREATE INDEX ON ExamResult (MarksObtained); -- Many values
Mistake 3: Not indexing foreign keys
Wrong:
CREATE TABLE Subject (
SubjectId INT PRIMARY KEY,
TeacherId INT, -- No index!
Name VARCHAR(50)
);
-- JOIN slow:
SELECT s.Name, t.Name FROM Subject s
JOIN Teacher t ON s.TeacherId = t.TeacherId;
Result: Table scan on every JOIN.
Fix: Index foreign keys:
CREATE INDEX ON Subject (TeacherId);
Mistake 4: Composite index with columns in wrong order
Wrong:
-- Query: WHERE MarksObtained > 80 AND StudentId = 5
CREATE INDEX ON ExamResult (MarksObtained, StudentId);
-- Index ordered wrong: First scans all Marks > 80, then filters StudentId (slow)
Fix: Filter column first:
CREATE INDEX ON ExamResult (StudentId, MarksObtained);
-- First narrows to StudentId = 5, then scans Marks (fast)
Most selective column first.
Mistake 5: Ignoring fragmentation
Wrong:
-- Index fragmented 80%, but not maintained
SELECT * FROM Student WHERE ClassName = '10-A';
-- Reads 80% extra pages (fragmented)
Result: Slow queries over time.
Fix: Rebuild periodically:
ALTER INDEX IX_Student_ClassName ON Student REBUILD;
Or defragment if light fragmentation:
ALTER INDEX IX_Student_ClassName ON Student REORGANIZE;
Best Practices for Indexes
- Index columns in WHERE clauses -- Speed up filters
- Index columns in JOINs (FK) -- Required for performance
- Index columns in ORDER BY -- Speed up sorts
- High cardinality first -- Unique columns
- Filter columns before sort -- Composite order matters
- Avoid too many indexes -- Slows INSERT/UPDATE
- Monitor fragmentation -- Rebuild when > 10%
- Drop unused indexes -- Free space, improve writes
Clustered: Physical row order (one per table).
Primary Key is usually clustered
Rows sorted by StudentId physically
Nonclustered: Pointer to rows (many per table).
Speed up lookups without changing physical order
Multiple indexes on one table
Clustered: Changes physical order. Nonclustered: Pointer only.
When query filters on multiple columns:
SELECT * FROM ExamResult
WHERE StudentId = 5 AND MarksObtained > 80;
-- Composite index:
CREATE INDEX ON ExamResult (StudentId, MarksObtained);
Order matters: Most selective first:
-- If StudentId = 5 returns 10 rows
-- MarksObtained > 80 returns 50K rows
-- Index (StudentId, MarksObtained): Filter StudentId first (10 rows), then Marks
Filter first, sort second.
Yes, almost always:
-- Foreign key
CREATE TABLE Subject (SubjectId INT, TeacherId INT FOREIGN KEY REFERENCES Teacher(TeacherId));
-- Index:
CREATE INDEX ON Subject (TeacherId);
JOINs use foreign keys constantly. Index them.
Exception: Rarely-used relationships can skip.
Look for:
- Index Seek: Fast (good)
- Table Scan: Slow (needs index)
- Estimated vs Actual: If differ greatly, stats stale (UPDATE STATISTICS)
Cost: Higher = slower.
If slow: Look for scans, add indexes.
Compare execution plan before/after:
-- Before (no index):
SELECT * FROM ExamResult WHERE MarksObtained > 80;
-- Table Scan (cost 100)
-- After (index created):
CREATE INDEX ON ExamResult (MarksObtained);
SELECT * FROM ExamResult WHERE MarksObtained > 80;
-- Index Seek (cost 5)
If cost drops significantly: Index helps.
Fragmentation:
- 0-10%: No action
- 10-30%: REORGANIZE (online, faster)
- 30-100%: REBUILD (offline, thorough)
-- Light fragmentation:
ALTER INDEX IX_Name ON TableName REORGANIZE;
-- Heavy fragmentation:
ALTER INDEX IX_Name ON TableName REBUILD;
Check fragmentation regularly.
Use ChatGPT, Claude, or Copilot to go deeper on Indexes and performance tuning in SQL Server. Try these prompts:
"Show how to create index: speed up WHERE StudentId = 5""What's a composite index? When should I use one?""How do I read execution plan to identify slow queries?""When should I create a covering index (INCLUDE)?""Quiz me: ask 5 questions about indexes, fragmentation, execution plans"
💡 Tip: After reading this article, paste your own code into AI and ask "What could go wrong here and why?" — fastest way to find edge cases and deepen understanding.