Execution plans look like a forest of arrows and percentages. They're actually a straightforward map: each box is an operation the database performs, and the arrows show data flowing between them. Once you know what to look for, you can diagnose 80% of slow queries without involving a DBA. Here's the field guide we use ourselves.
How to get the plan
In SSMS or Azure Data Studio, hit Ctrl+M before running the query to include the actual execution plan, or Ctrl+L for the estimated plan. Always prefer the actual plan when you can — estimated plans use statistics, actuals use what really happened.
-- Or with T-SQL, no GUI required:
SET STATISTICS XML ON;
SET STATISTICS IO ON;
SELECT TOP 10 * FROM Orders WHERE CustomerId = 42;
SET STATISTICS XML OFF;
SET STATISTICS IO OFF;
Read it right to left, top to bottom
Plans flow from right to left: the rightmost operators read data, intermediate operators transform it, and the leftmost operator returns the result to you. Arrow thickness shows row count — fat arrows mean lots of data is moving.
The operators that matter
Scan vs. Seek
- Index Seek — the engine jumps directly to the rows it needs. Cheap, predictable.
- Index Scan — the engine reads the whole index. Sometimes correct (you really do want all the rows), often a sign of a missing or unusable index.
- Table Scan — there's no clustered index, or the optimizer decided not to use one. On a large table, this is almost always your problem.
Joins
- Nested Loops — for each row on the left, look up matches on the right. Great when one side is small.
- Merge Join — both inputs must be sorted on the join key. Very fast when sorted indexes already exist.
- Hash Join — builds a hash table from one side, probes from the other. The optimizer's pick when sets are large and unsorted. Watch for hash spills to tempdb.
Sorts and spools
A Sort operator is a yellow flag — it means the engine had to
order rows in memory (or worse, in tempdb). Often eliminated by adding an index
on the ORDER BY column. Table Spools mean
intermediate results are being written to tempdb. Worth investigating.
The two warnings to never ignore
Implicit conversion
You'll see a yellow exclamation mark on an operator. Hover, and you'll find:
Type conversion in expression
(CONVERT_IMPLICIT(nvarchar(50), [Orders].[CustomerCode], 0))
may affect "CardinalityEstimate" in query plan choice.
This usually means a parameter type doesn't match the column type — for example, a
varchar column compared to an NVARCHAR parameter. The
conversion happens per-row and disables index usage. Fix the type mismatch and
throughput often jumps 10x.
Estimated vs. actual rows mismatch
If an operator estimated 10 rows but actually returned 1,000,000, the optimizer
made bad choices downstream — wrong join algorithm, wrong memory grant, possibly
a spill. Causes: stale statistics (run UPDATE STATISTICS), parameter
sniffing, or a non-SARGable predicate.
SARGability: the one term worth learning
A predicate is "SARGable" (Search ARGument-able) if SQL Server can use an index to evaluate it. Wrap a column in a function and you destroy SARGability:
-- Bad: function on column. Forces a scan.
WHERE YEAR(OrderDate) = 2025;
-- Good: function on the constant side. Index seek.
WHERE OrderDate >= '2025-01-01'
AND OrderDate < '2026-01-01';
Same result; very different plan.
When to add an index
If a query is slow and the plan shows a scan or a costly key lookup, an index is often the answer. SQL Server even suggests them — there's a green "Missing Index" hint at the top of the plan when one would help. Don't blindly create it. Indexes cost write performance and storage. Rules of thumb:
- Index columns used in
WHEREandJOINfirst. - Add
INCLUDEcolumns to cover theSELECTlist and avoid key lookups. - Match column order to the most-selective query you'll run.
- Don't add an index that duplicates a prefix of an existing one.
-- A typical "covering" index for a hot query.
CREATE NONCLUSTERED INDEX IX_Orders_Customer_Date
ON dbo.Orders (CustomerId, OrderDate)
INCLUDE (Total, Status);
Parameter sniffing in three sentences
The optimizer compiles a plan based on the first parameter values it sees, then
caches it. If those values are unrepresentative, every subsequent execution gets
a bad plan. Mitigate with OPTION (RECOMPILE) for occasional skewed
queries, or with the OPTIMIZE FOR hint for known-bad cases.
A simple workflow
- Reproduce the slow query in SSMS with actual plan on.
- Find the fattest arrow and the operator producing it.
- Check for warnings (yellow triangles).
- Look at the worst row-estimate mismatch.
- Decide: index, query rewrite, or statistics update.
- Re-run, compare.
Do that ten times and execution plans stop being scary. They become the most useful diagnostic tool the database gives you.
Our database performance track covers indexing strategy, query rewriting, and tempdb pressure in production workloads. If you've got a particular query that's stubborn, bring it to a coaching session.