Letting the Workload Tell You What to Index

It’s 3:00 AM, the pager is screaming, and your dashboard is a sea of crimson spikes. You’ve checked the application logic, you’ve scaled the pods, and you’ve even considered a frantic, expensive hardware upgrade—but the database is still choking. Most tutorials will tell you to just run a generic “optimization wizard” and call it a day, but they never show you the chaos of a production environment where a single missing index turns a simple lookup into a full table scan that brings the entire payment gateway to its knees. Learning how to find missing indexes isn’t about following a polished checklist; it’s about knowing how to trace the wreckage when the system starts to fail.

I’m not here to give you a theoretical lecture or a “happy path” guide that assumes your data is clean and your traffic is predictable. Instead, I’m going to show you the actual workflow I use when I’m staring down a mounting queue of stalled transactions. We are going to look at execution plans, query store histories, and the specific red flags that signal a bottleneck before the whole system crashes. This is about finding the truth in the logs, not the fluff in a textbook.

Table of Contents

Reading the Database Query Execution Plan for Lies

Reading the Database Query Execution Plan for Lies.

When you run `EXPLAIN` on a slow query, the database doesn’t actually tell you it’s struggling; it just presents a roadmap of how it intends to find your data. The problem is that the database query execution plan can be incredibly deceptive. You’ll see a “Sequential Scan” or a “Full Table Scan” staring back at you, and in a tutorial, they’d tell you to just add an index and call it a day. But in a production environment with millions of rows, the engine might decide that a scan is “cheaper” than using the index you just built, especially if your statistics are stale.

I’ve spent many late nights staring at these plans, trying to decipher why the optimizer is ignoring my perfectly valid B-tree. You have to look past the high-level summary and dig into the cost estimates and row counts. If the engine expects ten rows but actually pulls ten million, your SQL performance tuning efforts will be nothing more than guesswork. You aren’t just looking for a missing index; you’re looking for the moment the engine lies to you about how much work it’s actually doing.

Identifying Bottleneck Queries That Scream for Help

Identifying Bottleneck Queries That Scream for Help

You don’t find bottlenecks by looking at a clean dashboard; you find them when the pager goes off at 3:00 AM. Most people start by looking at the most expensive queries, but that’s a trap. A single, heavy report might run once an hour and consume massive resources, but it isn’t what’s killing your throughput. You need to look for the high-frequency, low-latency queries that are slowly bleeding your CPU dry. When you start identifying bottleneck queries, look for the ones with high execution counts that are just slightly over their latency budget.

I’ve spent enough time in legacy payment rails to know that a thousand “fast” queries can be more destructive than one slow one. This is where you stop looking at raw execution time and start digging into index usage statistics. You’re looking for patterns where the database engine is forced into a full table scan because a specific predicate isn’t covered. If you see your IOPS spiking alongside a steady climb in CPU usage, you aren’t just dealing with a heavy load—you’re dealing with a system that has lost its way because it can no longer find the data efficiently.

Five ways to spot the rot before it hits production

  • Stop looking at the code and start looking at the wait events. If your database is spending half its life in ‘IO: Data File Read’, it’s not a hardware problem; it’s a sign that your engine is brute-forcing its way through a full table scan because it has no map to follow.
  • Watch the high-water mark of your disk I/O during peak traffic. A sudden, unexplained spike in read operations while your write volume stays steady is the classic signature of a query that used to be fast but just outgrew its lack of an index.
  • Audit your ‘Index Usage Stats’ instead of just guessing. It’s easy to add an index to fix a slow query, but if you’re adding them like confetti, you’re just punishing your `INSERT` and `UPDATE` operations. If an index has zero seeks and a million scans, it’s dead weight.
  • Look for the ‘Sargability’ killers in your WHERE clauses. You can have the perfect index, but if your developers are wrapping columns in functions—like `WHERE YEAR(created_at) = 2023`—the engine is forced to ignore the index entirely and scan the whole mess.
  • Check for index fragmentation in your long-running maintenance windows. Sometimes the index is there, but it’s so fragmented from constant page splits that the database engine treats it like a shredded book; it’s technically there, but reading it is a nightmare.

The Hard Lessons

Don’t trust the execution plan blindly; a query might look efficient in a staging environment with a handful of rows, but it will lie to you the moment your production table hits a million entries and the scan turns into a full-table crawl.

Stop hunting for “slow” queries in isolation; a query isn’t just slow because of its own logic, but because it’s fighting for locks or competing for I/O with a dozen other unindexed processes.

An index is not a magic wand—adding them blindly to every column will bloat your write operations and eventually make your database struggle to keep up with the very transactions you’re trying to save.

## The Cost of the Happy Path

“Tutorials show you a clean schema and a lightning-fast SELECT statement, but they never show you the 3:00 AM pager alert when a table hits a million rows and suddenly your primary key isn’t enough to stop the full table scans from choking the CPU.”

Pernille Askeland

The Aftermath of the Fix

The Aftermath of the Fix: Optimized indexes.

Finding the missing index is only half the battle; the real work is understanding why the system failed to signal you sooner. We’ve looked at how to parse execution plans to spot those deceptive sequential scans, how to hunt down the specific queries that are currently eating your CPU alive, and how to differentiate between a genuine bottleneck and a symptom of something else entirely. Remember, an index isn’t a magic wand—it’s a surgical tool. If you apply them blindly without checking your write latency or your storage overhead, you’re just trading one set of headaches for another. The goal isn’t to have a hundred indexes; it’s to have the right ones in the right places.

Maintaining a database is a lot like my bookbinding hobby. You can’t just slap glue on a broken spine and hope the pages stay put; you have to understand the tension, the structure, and where the stress points are actually located. Software is the same. Don’t let the “happy path” tutorials fool you into thinking your schema is set in stone. Systems degrade, data grows, and patterns shift. Instead of fearing the next latency spike, learn to anticipate the breakage. When you stop treating your database like a black box and start seeing it as a living, breathing structure, you stop being a firefighter and start being an engineer.

Frequently Asked Questions

If I add all the indexes the execution plan suggests, won't I just tank my write performance and bloat the storage?

Yes, you absolutely will. If you treat the execution plan like a grocery list and add everything it suggests, you’re essentially building a library where every new book requires ten librarians to re-index the entire building. Your `INSERT` and `UPDATE` operations will crawl, and your storage costs will bloat. You don’t index every column; you index the paths that actually matter. My rule is simple: if the write penalty outweighs the read gain, leave it alone.

How do I tell the difference between a query that is actually slow and one that just happens to be running during a massive batch job?

You need to look at the wait events, not just the execution time. If the query is slow because of a missing index, you’ll see high CPU usage or logical reads. But if it’s just caught in the crossfire of a batch job, you’ll see lock contention or I/O waits. Check your system metrics: if the disk latency or lock wait times spike exactly when the query slows down, you’re not looking at a bad query—you’re looking at a resource war.

At what point does an index stop being a performance booster and start becoming a maintenance nightmare for the database engine?

It’s a tipping point. You add an index to shave five milliseconds off a SELECT, but suddenly your INSERTs are dragging and your WAL logs are bloating. Every time you write a row, the engine has to pause and update every single index tree associated with that table. If your write-to-read ratio shifts, or if you have ten overlapping indexes on a single table, you aren’t optimizing anymore—you’re just paying a heavy tax on every transaction.

About Pernille Askeland

I write the part the tutorial skipped: what it looks like when it breaks.

Scroll to Top