"The app is slow" is one of the most common things a business tells me when they get in touch. Pages that loaded in a blink now take five or six seconds. Reports time out. Everything gets worse at the busiest time of day — exactly when you can least afford it. And the database server sits there pinned at 100%, so the assumption is: we've outgrown the hardware, we need a bigger machine.
Sometimes that's true. Usually it isn't. In most systems I've looked at, throwing more CPU and RAM at the problem just buys a few months and a bigger bill, because the real cause is still sitting in the code. Below is the order I actually work through when a SQL Server database has slowed to a crawl — roughly cheapest and highest-impact first.
1. Missing indexes — the number one culprit
An index is what lets the database jump straight to the rows it needs instead of reading the entire table. Without the right one, a query for a single customer might scan all two million rows every single time it runs. On a small table nobody notices. As the data grows, that same query goes from milliseconds to seconds, and the slowdown creeps up so gradually that no single day feels like the day it broke.
The good news is this is often the single biggest win available, and it's low-risk. Adding a well-chosen index to a column you filter or join on regularly can turn a multi-second query into an instant one, with no application changes at all. SQL Server will even tell you which indexes it wishes it had — the trick is knowing which of those suggestions to trust and which to ignore, because too many indexes slow down writes.
2. The N+1 query problem
This one hides inside the application code, and it's everywhere. It looks like this: you load a list of 50 orders with one query, then — often without realising — the code fires off one more query per order to fetch its customer. That's 51 round trips to the database to show a single page. With ORMs like Entity Framework it's especially easy to write by accident, because the extra queries are invisible in the C# — they only show up when you watch what actually hits the database.
Each individual query is fast, so nothing looks wrong in isolation. But the round trips add up, and under load they multiply. The fix is usually to load the related data up front in one query instead of hundreds. It's a small code change with an outsized effect, and it's one of the first things I look for when a specific page — a dashboard, a list, a report — is far slower than the rest of the app.
3. SELECT * and pulling back more than you need
When a query grabs every column and every row "just in case," the database has to read, transfer and materialise all of it — including large text and image columns the page never even displays. Multiply that by every user hitting the page and you're moving far more data than the feature actually needs.
Asking only for the columns and rows you'll use — and paging large lists instead of loading ten thousand records into a dropdown — cuts the work at the source. It's unglamorous, but on data-heavy screens it's often the difference between a page that feels snappy and one that hangs while the browser waits.
4. Queries that can't use their indexes
Sometimes the index exists and the query still ignores it. This usually comes down to how the query is written: wrapping a column in a function, doing type conversions in the wrong place, or leading a search with a wildcard all quietly force the database to scan the whole table anyway. The index is right there — the query just phrased its question in a way that can't use it.
These are subtle, and you only find them by reading the execution plan — SQL Server's own explanation of how it ran the query. The plan shows exactly where it resorted to scanning instead of seeking, and where time actually went. Reading plans is most of what practical performance tuning really is; the fixes are often a one-line change once you can see the problem.
5. Blocking and locking under load
If the app is fine when it's quiet and falls apart when everyone's using it, the problem may not be any single slow query — it may be queries getting in each other's way. When one operation holds a lock longer than it should, everything else queues up behind it, waiting. Users experience this as random freezes that are impossible to reproduce on a quiet test environment.
The usual causes are long-running transactions doing too much at once, or queries reading and writing the same rows in a way that forces them to take turns. The fixes range from keeping transactions short and tight, to adjusting how the app reads data, to the indexing work above — because a faster query holds its locks for less time and gets out of everyone else's way sooner.
6. Statistics and fragmentation the server forgot to maintain
SQL Server decides how to run a query based on internal statistics about your data. If those go stale — which happens naturally as data changes — it can start making bad decisions, choosing a slow plan because its picture of the data is out of date. Similarly, indexes fragment over time and quietly lose efficiency.
On a healthy system, routine maintenance keeps both in check automatically. On a lot of the systems I inherit, that maintenance was never set up, so performance degrades month after month for reasons nobody can see. Putting a simple, scheduled maintenance job in place is a one-time fix that stops the slow, invisible decay.
7. Only now: is it actually the hardware?
Sometimes, after all of the above, the honest answer is yes — the workload has genuinely outgrown the machine, and more memory or faster storage is the right call. But by then you're making that decision with evidence instead of a guess, and you're paying to scale an efficient system rather than paying to paper over a wasteful one.
The difference matters. I've seen a "we need a bigger server" emergency turn out to be two missing indexes and one N+1 query — fixed in an afternoon, no new hardware, and the server that was pinned at 100% dropped to idle. Scaling up would have hidden all three problems and cost money every month forever.
A bigger server makes a slow query slow more quickly. It doesn't make it fast. The fix is almost always in the queries, not the hardware.
How I approach a slow database
The method is the same every time, and it's deliberately boring: measure first, guess never.
Find the queries that actually hurt
SQL Server tracks which queries run most often and consume the most time. That short list — usually a handful of queries — is almost always where the pain is. Optimising anything else is wasted effort. So the first job is to find the real offenders instead of assuming.
Read the plan, fix the cause
For each offender, the execution plan shows what it's really doing and why it's slow. That points to the actual fix — an index, a query rewrite, a code change — rather than a shotgun of "optimisations" that change nothing. One clear cause, one targeted fix.
Measure again, then stop
After each change I re-measure, so I can prove it helped and by how much. Performance work has a point of diminishing returns, and knowing when to stop is part of doing it well — you fix the things that matter and leave the rest alone.
Is your app slow and you're not sure why?
I do SQL Server performance audits and query tuning for .NET applications — find the real bottleneck, fix it, and prove the difference with numbers. Often it's an afternoon's work, not a new server. Tell me what you're seeing and I'll take a look. If it's a wider issue, my write-up on rescuing a legacy .NET app covers the bigger picture.
Start a project →