Hero image for SQL Performance Tuning: Execution Plans, Statistics & Optimization

SQL Performance Tuning: Execution Plans, Statistics & Optimization

sql-server database performance optimization

Prerequisites: Understand B-Tree indexes. See DB 03 B-Tree Index.

Your query works, but it’s slow. This article teaches you to find and fix performance bottlenecks.


Part A: Execution Plans — The SQL Server X-Ray

1. What is an Execution Plan?

When you run a query, SQL Server creates a plan showing HOW it will execute:

graph LR
    Q[Your Query] --> QO[Query Optimizer]
    QO --> EP[Execution Plan]
    EP --> E[Execute]
    E --> R[Results]
    
    style QO fill:#3498db,color:#fff
    style EP fill:#27ae60,color:#fff

2. How to View Execution Plans

MethodShortcutDescription
Estimated PlanCtrl+LShows plan WITHOUT running query
Actual PlanCtrl+M, then runShows plan WITH runtime stats
Live Query Stats-Real-time progress
-- Or use SET commands:
SET SHOWPLAN_TEXT ON;
SET STATISTICS IO ON;
SET STATISTICS TIME ON;

SELECT * FROM Customers WHERE Country = 'Taiwan';

Understanding STATISTICS IO Output

-- Example output:
-- Table 'Customers'. Scan count 1, logical reads 125, physical reads 0
MetricMeaningWhat to Watch
Logical ReadsPages read from memory (buffer pool)🔥 THIS is your key metric — reduce this!
Physical ReadsPages read from disk0 is great (data was cached)
Scan CountNumber of times table was accessedHigh = possible N+1 problem

3. Reading the Execution Plan

Read from RIGHT to LEFT, BOTTOM to TOP.

┌─────────────────────────────────────────────────────────────────┐
│                     Execution Plan                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   SELECT ← Nested Loop ← Index Seek (Customers)                 │
│     │         │              │                                  │
│     │         │              └── Cost: 10%                      │
│     │         └── Cost: 30%                                     │
│     └── Cost: 60%                                               │
│                                                                 │
│   Data flows: Right → Left (arrows show direction)              │
│   Thicker arrow = More rows                                     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

4. Key Operators to Know

OperatorIconMeaningPerformance
Index Seek🎯Jump directly to data⚡ Fast
Index Scan📜Read entire index🐌 Slow
Table Scan📋Read entire table🐌🐌 Very slow
Key Lookup🔑Extra trip to table⚠️ Watch out
Nested Loop♻️For each row, find matchOK for small sets
Hash Match#️⃣Build hash tableGood for large joins
Sort🔄Needs memoryCan spill to disk

5. Warning Signs in Execution Plans

WarningMeaningFix
Yellow triangle !Plan issue detectedCheck tooltip
Fat arrowsMany rows movingFilter earlier
Table ScanNo useful indexCreate index
Key Lookup (high %)Missing covering indexAdd INCLUDE columns
Sort (high %)Missing ORDER BY indexCreate sorted index

Part B: Statistics — The Crystal Ball

6. What are Statistics?

SQL Server guesses how many rows a query will return using statistics.

┌─────────────────────────────────────────────────────────────────┐
│                     Statistics                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   Column: Country                                               │
│   ┌────────────────┬──────────────┐                            │
│   │ Value          │ Row Count    │                            │
│   ├────────────────┼──────────────┤                            │
│   │ Taiwan         │ 50,000       │                            │
│   │ USA            │ 200,000      │                            │
│   │ Japan          │ 30,000       │                            │
│   └────────────────┴──────────────┘                            │
│                                                                 │
│   Query: WHERE Country = 'Taiwan'                               │
│   SQL Server estimates: ~50,000 rows                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

7. Why Statistics Matter

EstimateRealityConsequence
100 rows100 rows✅ Good plan
100 rows1,000,000 rows❌ BAD plan! Wrong join strategy

Stale statistics = Wrong estimates = Slow queries

The Tipping Point: When SQL Server Ignores Your Index

Even with a perfect index, SQL Server might choose a Table Scan. Why?

SelectivitySQL Server’s ChoiceReason
< 1% of rowsIndex Seek + Key LookupFast point lookups
1-30% of rowsIndex Seek + Key LookupStill worth the extra trip
> 30% of rowsTable ScanScan is actually faster!
Tipping Point Formula:
If (Rows Selected / Total Rows) > ~25-33%
   → Table Scan beats Index Seek + Key Lookup

Why? Each Key Lookup requires a random I/O.
       Sequential scan of entire table is faster than 
       thousands of random I/Os.

Common Confusion: “I created an index but SQL Server ignores it!” Check if your WHERE clause returns too many rows. That’s the Tipping Point in action.

8. Managing Statistics

-- View statistics:
DBCC SHOW_STATISTICS('Customers', 'IX_Country');

-- Update statistics for table:
UPDATE STATISTICS Customers;

-- Update all statistics in database:
EXEC sp_updatestats;

-- Auto-update setting:
ALTER DATABASE MyDB SET AUTO_UPDATE_STATISTICS ON;

Part C: Isolation Levels — Balancing Speed vs Accuracy

9. The Problem: Dirty Reads, Phantom Reads

ProblemDescriptionExample
Dirty ReadRead uncommitted dataSee $100 that gets rolled back
Non-Repeatable ReadSame query, different resultBalance was 100,now100, now 50
Phantom ReadNew rows appear mid-queryCOUNT was 10, now 11

10. Isolation Levels

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- or
SELECT * FROM Accounts WITH (NOLOCK);
LevelDirty ReadNon-RepeatablePhantomLock DurationSpeed
READ UNCOMMITTED✅ Yes✅ Yes✅ YesNone⚡⚡⚡ Fastest
READ COMMITTED❌ No✅ Yes✅ YesShort⚡⚡ Fast
REPEATABLE READ❌ No❌ No✅ YesLong⚡ Medium
SERIALIZABLE❌ No❌ No❌ NoLongest🐌 Slow
SNAPSHOT❌ No❌ No❌ NoUses versioning⚡⚡ Fast

11. When to Use What

ScenarioIsolation Level
Dashboard / reporting (stale OK)READ UNCOMMITTED
Normal OLTPREAD COMMITTED (default)
Financial calculationsSERIALIZABLE
High concurrency + accuracySNAPSHOT

Part D: Query Hints — Manual Override

12. What are Query Hints?

When the optimizer makes wrong choices, hints force a specific behavior.

-- Force index usage:
SELECT * FROM Customers WITH (INDEX(IX_Country))
WHERE Country = 'Taiwan';

-- Force join type:
SELECT * FROM Orders o
INNER HASH JOIN Customers c ON o.CustomerID = c.CustomerID;

-- Force parallel execution:
SELECT * FROM BigTable OPTION (MAXDOP 4);

13. Common Hints

HintEffectUse When
NOLOCKNo shared locksReporting (dirty reads OK)
INDEX(name)Force specific indexOptimizer picks wrong one
FORCESEEKForce index seekOptimizer uses scan
HASH/LOOP/MERGE JOINForce join typeKnow better than optimizer
MAXDOP nLimit parallelismControl CPU usage
RECOMPILEFresh plan each timeParameter sniffing issues

14. Warning: Hints are Dangerous!

⚠️ Use hints as LAST RESORT only!

- Data changes → hint may become wrong
- Optimizer improves with SQL Server updates
- Hints prevent adaptation

Part E: Common Performance Killers

15. Anti-Patterns Checklist

ProblemDetectionSolution
Missing IndexTable/Index ScanCreate appropriate index
Non-SARGable QueryFunction on column in WHERERewrite to be SARGable
Implicit ConversionYellow ⚠️ on SELECT operatorMatch data types
**SELECT ***Key Lookup high costSelect only needed columns
Parameter SniffingSame query, different timesOPTION (RECOMPILE)
Too Many IndexesSlow INSERT/UPDATERemove unused indexes

16. SARGable: The #1 Performance Rule

SARGable = Search ARGument ABLE — can SQL Server use an index?

-- ❌ Non-SARGable (Index CANNOT be used)
WHERE YEAR(OrderDate) = 2024
WHERE UPPER(CustomerName) = 'JOHN'
WHERE Price + 10 > 100
WHERE LEFT(Phone, 3) = '886'

-- ✅ SARGable (Index CAN be used)
WHERE OrderDate >= '2024-01-01' AND OrderDate < '2025-01-01'
WHERE CustomerName = 'John'  -- (use case-insensitive collation)
WHERE Price > 90
WHERE Phone LIKE '886%'

The Golden Rule: Put the column ALONE on one side of the operator. If you wrap a column in a function, SQL Server must evaluate EVERY row.

17. Implicit Conversion: The Silent Killer

This is invisible in your code but devastating in the execution plan:

-- Column is VARCHAR, but you pass INT:
WHERE CustomerID = 12345  -- CustomerID is VARCHAR(10)

-- SQL Server secretly does:
WHERE CONVERT(INT, CustomerID) = 12345  -- Forces table scan!

How to spot it:

  1. Look for yellow warning triangle ⚠️ on the SELECT operator
  2. Hover to see: “Type conversion in expression… may affect ‘CardinalityEstimate‘“
Your CodeColumn TypeProblemFix
WHERE ID = '123'INTConverts each rowWHERE ID = 123
WHERE Code = 100VARCHARConverts each rowWHERE Code = '100'
WHERE Date = '2024-01-01'DATETIME2Usually OK

18. Parameter Sniffing Explained

-- First call with CustomerID = 1 (1 order)
EXEC GetOrders @CustomerID = 1;  -- Plan cached for "few rows"

-- Second call with CustomerID = 999 (1,000,000 orders)
EXEC GetOrders @CustomerID = 999;  -- Uses same "few rows" plan = SLOW!

Solutions:

-- 1. Recompile each time (CPU cost):
CREATE PROCEDURE GetOrders @CustomerID INT
WITH RECOMPILE
AS ...

-- 2. Optimize for unknown:
OPTION (OPTIMIZE FOR UNKNOWN);

-- 3. Use local variables:
DECLARE @LocalID INT = @CustomerID;
SELECT * FROM Orders WHERE CustomerID = @LocalID;

Summary: Performance Tuning Checklist

Execution Plan Red Flags

FlagAction
Table ScanCreate index
Key Lookup > 10%Add INCLUDE columns
Sort > 20%Create sorted index
Hash Match + spillAdd memory or simplify

Quick Wins

  1. Check indexes — Missing? Fragmented? Unused?
  2. Update statistics — Stale = wrong plans
  3. **Avoid SELECT *** — Fetch only needed columns
  4. Watch data types — Implicit conversion kills indexes

Tools

ToolPurpose
Execution PlanSee how query runs
STATISTICS IOCount page reads
sys.dm_exec_query_statsFind expensive queries
Query StoreHistorical query performance

💡 Practice Questions

Conceptual

  1. What is an execution plan and why is it important for performance tuning?

  2. Explain what Table Scan vs Index Seek means. Which is better and why?

  3. What are statistics in SQL Server and why do stale statistics cause performance issues?

  4. Describe what parameter sniffing is and how it can cause performance problems.

Hands-on

-- Given this execution plan info: "Table Scan (Cost: 95%)"
-- What does this tell you? What would you do to fix it?

-- Write a query that intentionally prevents index usage due to a function on a column,
-- then rewrite it to allow index seek.
-- Bad: WHERE YEAR(OrderDate) = 2024
-- Good: ?
💡 View Answer

Table Scan Analysis:

  • 95% cost = query is scanning the entire table
  • No useful index exists for the WHERE clause
  • Fix: Create an index on the filtered columns

Index-friendly rewrite:

-- BAD: Function on column prevents index usage
SELECT * FROM Orders WHERE YEAR(OrderDate) = 2024;

-- GOOD: Use range comparison (index can be used)
SELECT * FROM Orders 
WHERE OrderDate >= '2024-01-01' AND OrderDate < '2025-01-01';

The key insight: Any function on a column (YEAR(), UPPER(), CONVERT()) prevents index seek. Rewrite to compare the column directly.

Scenario

  1. Investigation: A stored procedure ran fast (1 second) on Monday but now takes 30 seconds. The data hasn’t changed significantly. What are the top 3 things you would investigate?
💡 Scenario 1 Answer

Top 3 things to investigate:

  1. Parameter Sniffing — Check if the plan was compiled with an atypical parameter value. Use sp_recompile or OPTION(RECOMPILE) to test.

  2. Statistics — Were statistics updated over the weekend? Check STATS_DATE() function. Consider UPDATE STATISTICS on involved tables.

  3. Blocking/Locking — Is another process locking the table? Check sys.dm_exec_requests for blocking_session_id > 0. A weekend batch job might still be running.

Bonus checks: Memory pressure (Page Life Expectancy), Plan cache eviction, Index fragmentation after bulk load.

  1. Trade-off: A report query uses NOLOCK hint for speed. Business says some numbers don’t match. Explain what’s happening and propose alternatives.