Written by: ekwoster.dev on Wed Aug 20

Why Your SQL Queries Are Slower Than a Sloth: Deep Dive Into Indexes You’re Probably Misusing

Why Your SQL Queries Are Slower Than a Sloth: Deep Dive Into Indexes You’re Probably Misusing

Cover image for Why Your SQL Queries Are Slower Than a Sloth: Deep Dive Into Indexes You’re Probably Misusing

Why Your SQL Queries Are Slower Than a Sloth: Deep Dive Into Indexes You’re Probably Misusing

If you've ever stared at your screen wondering why your SQL queries feel like they’re being powered by a hamster on a wheel, this post is for you.

Let’s be honest. Indexes are the magic pixie dust that every developer has heard of—but few really understand. You’ve probably googled “how to speed up SQL query” and followed the first suggestion: Add an index. But here’s the kicker: most developers misuse indexes, and it ends up doing more harm than good.

So buckle up. 🔧 This is not just another blog post about SELECT and WHERE clauses — this is a deep, hands-on look at common index anti-patterns and how to surgically fix slow-performing queries.


🚀 The Setup: Our Sample Database

We'll use a PostgreSQL database, but the principles apply to MySQL, SQL Server, and most relational databases.

Suppose we have a fictional e-commerce database:

CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL,
    status TEXT NOT NULL,
    total NUMERIC(10,2) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created_at ON orders(created_at);

Looks good, right? Wait for it…

The Query

Let’s say we often run this query:

SELECT *
FROM orders
WHERE status = 'shipped'
AND created_at >= now() - interval '30 days'
ORDER BY created_at DESC;

🤔 Seems like the indexes we added (status, created_at) should be helping. But the query is still slow. Why?


🧠 Index 101: What You Think vs. What Actually Happens

Here's the mistake: the query needs to filter by status AND created_at and sort by created_at DESC.

But our individual indexes are like fractured mirrors — each only reflects part of the world.

🔥 The Fix: Composite Indexes

You need to create a composite index, like this:

DROP INDEX IF EXISTS idx_orders_status;
DROP INDEX IF EXISTS idx_orders_created_at;

-- Add this instead:
CREATE INDEX idx_orders_status_created
ON orders(status, created_at DESC);

Now PostgreSQL can filter by status, find the relevant created_at dates quickly, AND already have the data sorted. No additional sorting needed.

TL;DR on Index Order:

📌 Think of composite indexes as the magic combo moves in Mortal Kombat: the order matters!

If your queries always filter by status then sort/filter by created_at, your index should be:

(status, created_at)

If your queries sometimes only filter by created_at, but rarely by status, you’d do:

(created_at, status)

🔎 EXPLAIN Is Your Best Friend (No, Really)

Don’t guess. Use EXPLAIN ANALYZE.

EXPLAIN ANALYZE 
SELECT *
FROM orders
WHERE status = 'shipped'
AND created_at >= now() - interval '30 days'
ORDER BY created_at DESC;

Watch for these red flags:

  • Seq Scan: You’re scanning the whole table. Oof.
  • Sort: You added an index but forgot sorting.
  • Rows Removed by Filter: The database grabs too many rows, then filters most of them.

Magic Output:

Look for:

  • Index Scan using idx_orders_status_created
  • Rows Removed by Filter: 0
  • Sort Method: quicksort Memory: (meaning it's sorting super fast)

🔄 The Anti-Pattern: Too Many Indexes

Adding too many individual indexes may seem like hedging bets, but it’s like putting ketchup on sushi — it just doesn’t work.

Each extra index bloats your insert/update cost. And the query planner might end up ignoring the best one due to confusion.

👎 Example:

CREATE INDEX idx_orders_status;
CREATE INDEX idx_orders_user_id;
CREATE INDEX idx_orders_created_at;
CREATE INDEX idx_orders_total;

You’re paying storage, insert speed penalty, and disk IO. Compounded exponentially.


Real-World Case Study ⚠️

One startup saved 80% of their API response time just by replacing this query:

SELECT * FROM events
WHERE event_type = 'signup'
AND user_id = 345
ORDER BY created_at DESC LIMIT 50;

They had an index on user_id. Another on event_type. Still slow.

✅ The fix:

CREATE INDEX idx_events_userid_eventtype_created
ON events(user_id, event_type, created_at DESC);

Boom 💥 — drop from 2.8 seconds to 250ms.


🧙‍♂️ Bonus Tips: Little Known Index Tricks

  • Use INCLUDE for covering indexes (PostgreSQL only):
CREATE INDEX idx_orders_status_created_include
ON orders(status, created_at DESC) INCLUDE (total);

This allows postgres to serve the full query from the index — no need to reach the table.

  • Avoid SELECT * unless you really need every column.

  • Use partial indexes for sparse datasets:

CREATE INDEX idx_orders_shipped_only
ON orders(created_at DESC)
WHERE status = 'shipped';

Conclusion: The Real Secret Sauce 🥫

Indexing isn’t black magic — it’s pattern recognition. Look at how your queries behave, how they filter and sort, and design indexes for actual usage, not theoretical coverage.

Next time your query feels slower than a cassette tape, remember:

✅ Composite indexes

✅ EXPLAIN ANALYZE

✅ Avoid index sprawl

✅ Sort-Filter patterns

✅ Partial and covering indexes

Let your database breathe and fly!


Happy Querying! Got a painful query slowing down your app? Paste it below — let’s debug!


Is your team struggling with SQL performance? Want a second pair of eyes? I do database consulting and query tuning — drop me a message!


🔍 If you need hands-on help with backend performance, database troubleshooting, or fullstack application tuning — I offer such services.