SQL Optimization

SQL is the language that helps us talk to databases, and like any language, it's more effective when used wisely. Imagine you're looking for a book in a giant library. Would you walk down every aisle, or would you use a catalog to go straight to it? SQL optimization helps databases do just that—find answers faster.

1. Use Indexes (Like a Phonebook)

Indexes are shortcuts that make data retrieval faster, like a phonebook or book index. But they also add some overhead on writes (INSERT/UPDATE/DELETE).

This SQL query checks every row in the database.

SELECT * FROM students WHERE name = 'Alice';

Think of an index as a sorted phonebook for a column. With a nonclustered index on students(name), SQL Server can do an Index Seek (jump) to “Alice” instead of a Table/Index Scan (read everything).

CREATE INDEX idx_name ON students(name);

When this SQL statement can be wrong or less helpful,

  1. Low selectivity / Lots of matches
    If half the table is named “Alice” (extreme example), SQL may still choose a scan because seeking then reading many rows is not faster than a single scan.

  2. Leading wildcard searches

     -- Leading wildcard breaks seeks
     WHERE Name LIKE '%Alice%'
    

    This pattern can’t use the b-tree order efficiently and often scans. (But LIKE 'Alice%' can seek.)

  3. Expressions on the indexed column

     WHERE UPPER(Name) = 'ALICE'
    

    Wrapping the column in a function prevents an efficient seek on IX_Students_Name (unless you use a computed column with its own index).

  4. Very small tables
    For a tiny table, a scan is so cheap that an index offers little or no benefit.

  5. Write overhead & storage
    Every INSERT/UPDATE/DELETE must also maintain the index. Too many indexes slow writes and bloat storage.

2. Understanding Execution Plans (How the Database Thinks)

Different databases have different ways of showing you how they plan to run your query. Looking at an execution plan is like checking Google Maps directions before starting your trip.

  • It shows the route (steps SQL will take).

  • It highlights traffic or tolls (expensive operations like scans or sorts).

  • You can see if the database is taking the shortest path (index seek) or the long way around (table scan).

PostgreSQL/MySQL: Use EXPLAIN to view the plan.

  •   EXPLAIN SELECT * FROM students WHERE name = 'Alice';
    

SQL Server: Use either the Execution Plan in SSMS or SET STATISTICS options.

Using Set STATISTICS IO/TIME

SET STATISTICS IO ON;
SET STATISTICS TIME ON;
SELECT * FROM Employees WHERE Name = 'Alice';
SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;

Output Explanation:

  • Logical reads: Pages read from memory (like flipping notebook pages).

  • Physical reads: Pages read from disk (slower, like walking to your locker).

  • CPU time: How much work the processor did.

  • Elapsed time: How long the whole query took.

If logical reads and elapsed time are small, the query is optimized.

Graphical Execution Plans in SSMS

When you enable Actual Execution Plan (Ctrl + M in SSMS), SQL Server shows a map of how it runs your query.

  • Table Scan: SQL checked the whole table (slower).

  • Sort: SQL had to reorder rows (can be expensive).

  • Nested Loops: SQL combined rows step-by-step.

  • Index Seek (if present): SQL jumped directly to the right rows (fastest).

Think of this like watching your GPS route. If it shows you’re always taking the long way (table scan), you know it’s time to build a shortcut (index).

3. Avoid SELECT *

This pulls everything—even data you don't need.

SELECT * FROM students;

Only get what you need, like just picking book titles instead of reading every book.

SELECT name, grade FROM students;

4. Pick the Right JOIN for the job

This left join gets all students, even those not in a club.

SELECT * FROM students LEFT JOIN clubs ON students.id = clubs.student_id;

Better with the inner join if you only care about the matches.

SELECT * FROM students INNER JOIN clubs ON students.id = clubs.student_id;

5. EXISTS vs IN

    • EXISTS works with a correlated subquery (the subquery references the outer query).

      • It only cares about the existence of rows, not their actual values (SELECT 1 is just a placeholder).

      • Performance is often better than IN when the subquery returns many rows.

IN

SELECT * FROM students WHERE id IN (SELECT student_id FROM grades WHERE grade =
'A');
  • SQL first collects all student_id values from the grades table where the grade = 'A'. Then it compares each students.id against that list.

  • Imagine writing down a whole list of students who got an A, and then checking if each student is on that list.

EXISTS

SELECT * FROM students WHERE EXISTS (
SELECT 1 FROM grades
WHERE students.id = grades.student_id AND grade = 'A'
);
  • For each student, SQL checks if there is at least one grade 'A' in the grades table.

  • If yes, it returns that student.

  • It stops searching as soon as it finds one match.

Imagine instead of writing down the whole list, you ask: “Does this student have an A?” If the answer is yes, you move on to the next student without listing them all.

Key Difference (Analogy)

  • IN: Like making a big list of everyone with an A first, then checking names one by one.

  • EXISTS: Like asking each student directly: “Do you have an A?” and stopping as soon as they say yes.

Performance Tip

  • If the subquery returns a small list, IN is fine.

  • If the subquery returns a large list, EXISTS is usually faster, because it doesn’t need to process everything.

6. Partitioning (Like Folders by Year)

Partitioning a large table in SQL is like organizing your data into separate, well-labeled folders. Instead of keeping all your data in one massive pile, partitioning helps split it into smaller, more manageable segments based on a key column—like the year in an orders table.

CREATE TABLE orders (
id INT,
customer_id INT,
order_date DATE,
total NUMERIC
)
PARTITION BY RANGE(order_date);

Let’s break this down with a concrete example:

Problem Without Partitioning:

You have a table called orders with millions of rows, spanning multiple years (2020 to 2025).
When you run:

SELECT * FROM orders WHERE order_date BETWEEN '2025-01-01' AND '2025-12-31';

SQL must scan the entire table to find matching rows, even though you're only interested in 2025.

Solution With Partitioning:

You can partition the orders table by year, e.g., using a RANGE partition on order_date. Now, the table is split into physically separate segments like:

  • orders_2020

  • orders_2021

  • ...

  • orders_2025

When you run the same query:

SELECT * FROM orders WHERE order_date BETWEEN '2025-01-01' AND '2025-12-31';

SQL uses partition pruning: it only scans the 2025 partition (orders_2025), ignoring all other partitions. This dramatically reduces I/O and improves query performance.

Think of the entire orders table as a filing cabinet. Without partitioning, you have to flip through every document. With partitioning, each year has its own drawer. If you're only interested in 2025, you open just that drawer.

Partitioning is crucial for:

  • Performance on large datasets

  • Efficient data maintenance (e.g., dropping a partition instead of deleting rows)

  • Archival strategies

7. Materialized View (Pre-Saved Answers)

Instead of calculating big results each time, save the answer and refresh it daily.

CREATE MATERIALIZED VIEW sales_summary AS
SELECT customer_id, SUM(total) FROM orders GROUP BY customer_id;

8. Use CTEs and Temp Tables when you need to break complex problems into simpler parts.

WITH top_students AS (
SELECT * FROM students WHERE grade = 'A'
)
SELECT COUNT(*) FROM top_students;

OR

CREATE TEMP TABLE top_students AS
SELECT * FROM students WHERE grade = 'A';
SELECT COUNT(*) FROM top_students;

Think of SQL as giving directions to a friend. The clearer and smarter you are, the faster they get there. By using indexes, smart joins, pre-saved results, and breaking things into steps, you can make your SQL queries lightning-fast and more efficient. Happy querying!

0
Subscribe to my newsletter

Read articles from Indira Unnikrishnan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Indira Unnikrishnan
Indira Unnikrishnan