When to Use SQL Query Stress
Ideal Use Cases
SQL Query Stress is purpose-built for testing individual queries and stored procedures under load, not full application testing.
- Query optimization validation: Test if your index/query changes actually improve performance
- Concurrency testing: Understand how queries behave with multiple simultaneous executions
- Resource impact analysis: Measure CPU, IO, and memory effects of queries under load
- Regression testing: Verify that database changes don’t degrade performance
- Capacity planning: Determine maximum throughput for specific operations
- Stored procedure stress testing: Test procedures with realistic parameter distributions
- Full application load testing (use JMeter, LoadRunner, etc.)
- Testing web services or APIs directly
- UI/UX performance testing
- Multi-query transaction workflows (each test runs one query repeatedly)
Complementary Tools
SQL Query Stress works best alongside:- SQL Server Profiler/Extended Events: Capture broader database activity
- Query Store: Track query performance over time
- Database Engine Tuning Advisor: Get index recommendations
- sp_BlitzCache: Analyze execution plan patterns
Setting Realistic Thread and Iteration Counts
Understanding Thread Behavior
Each thread in SQL Query Stress (LoadEngine.cs:120-158):- Opens a dedicated SQL connection from the pool
- Executes the query for N iterations sequentially
- Optionally delays between iterations
- Runs at
BelowNormalthread priority (LoadEngine.cs:152)
Thread Count Guidelines
Start with your expected concurrent users:- OLTP applications: 20-100 threads per application server
- Reporting systems: 5-20 threads (reports typically run sequentially)
- Web applications: 10-50 threads (depends on connection pooling)
- Batch processes: 1-10 threads (often single-threaded)
- Baseline: 1 thread, 100 iterations (establishes single-user performance)
- Light load: 10 threads, 100 iterations each (1,000 total executions)
- Normal load: Match expected production concurrency
- Stress test: 2-3x normal load (find breaking points)
- Maximum throughput: Increase threads until performance plateaus
Iteration Count Guidelines
Total executions = threads × iterations Choose iterations based on test goals: Quick validation (10-50 iterations):- Verify query executes without errors
- Get rough performance estimates
- Fast feedback during development
- Establish reliable average execution times
- Smooth out variance and outliers
- Recommended minimum for comparative testing
- Detailed percentile analysis (p50, p95, p99)
- Identify rare edge cases
- Production-like performance characterization
- Detect memory leaks
- Identify performance degradation over time
- Test plan cache stability
More iterations = more statistical confidence but longer test runtime. Balance thoroughness with iteration time.
Test Environment Setup
Environment Considerations
Ideal test environment:- Matches production hardware specifications
- Isolated from other workloads
- Contains production-like data volumes
- Same SQL Server version and edition
- Identical indexes and statistics
Testing on underpowered hardware
Testing on underpowered hardware
Problem: Your test server has 4 CPUs and 16GB RAM, production has 32 CPUs and 256GB RAM.Impact:
- CPU-bound queries will appear much slower
- Memory pressure may force excessive disk IO
- Parallelism behavior differs dramatically
- Scale thread counts proportionally (reduce by CPU ratio)
- Focus on relative improvements, not absolute numbers
- Test on production-equivalent hardware for capacity planning
- Use Resource Governor to limit CPU/memory if testing on production
Empty or unrealistic test data
Empty or unrealistic test data
Problem: Testing with 1,000 rows when production has 10 million rows.Impact:
- Entire table fits in memory (won’t in production)
- Query plans differ dramatically (index scans vs seeks)
- Locking behavior is unrealistic
- Results are completely misleading
- Load representative data volumes
- Include realistic data distributions (not just sequential IDs)
- Update statistics:
UPDATE STATISTICS TableName WITH FULLSCAN - Consider using production backup restored to test environment
Stale or missing statistics
Stale or missing statistics
Problem: You restored a production backup but statistics are outdated.Impact:
- Query optimizer makes poor decisions
- Execution plans don’t match production
- Performance results are invalid
Network Considerations
Network impact on timing:- LoadEngine measures client-perceived time (LoadEngine.cs:413-435)
- Includes query execution + network transfer
ForceDataRetrieval = trueamplifies network effects for large result sets
Baseline Before Optimization
Why Baselines Matter
Without a baseline, you can’t prove optimization worked. Baseline workflow:-
Document current state:
- Current query or procedure code
- Existing indexes
- Current statistics collection state
-
Run baseline test:
- Enable
CollectIoStats = true(QueryStressSettings.cs:122) - Enable
CollectTimeStats = true(QueryStressSettings.cs:123) - Record: avg/min/max execution time, logical reads, CPU time
- Save the test configuration
- Enable
-
Make ONE change:
- Add an index, OR
- Rewrite the query, OR
- Adjust a parameter
- Never multiple changes simultaneously
-
Re-run identical test:
- Same thread count
- Same iteration count
- Same connection settings
- Same test data
-
Compare results:
- Did execution time improve?
- Did logical reads decrease?
- Did CPU time decrease?
Key Metrics to Baseline
Always track these QueryOutput metrics (LoadEngine.cs:535-545):- Time (TimeSpan): Client-perceived execution time
- Watch: Average, P95, P99, maximum
- LogicalReads (int): Pages read from buffer cache
- Goal: Minimize (fewer IOs = faster queries)
- Extracted via regex from InfoMessage events (LoadEngine.cs:280)
- CpuTime (int): SQL Server CPU milliseconds
- Distinguishes CPU-bound from IO-bound queries
- Extracted via regex from InfoMessage events (LoadEngine.cs:284-286)
- ElapsedTime (int): Server-side elapsed time
- Compare to client Time to identify network overhead
- Exception count: Should be zero in successful tests
- Any exceptions indicate problems
Interpreting Results Correctly
Understanding the Output
SQL Query Stress aggregates results from all threads via aBlockingCollection<QueryOutput> (LoadEngine.cs:18) that:
- Collects output from each iteration of each thread
- Reports progress to the UI (LoadEngine.cs:203)
- Tracks active threads (LoadEngine.cs:202)
Common Interpretation Mistakes
Focusing only on average time
Focusing only on average time
Why this is wrong:
- Averages hide outliers that affect user experience
- A few very slow executions get hidden by many fast ones
- P95 and P99 metrics matter more for user satisfaction
- Look at maximum execution time (worst case)
- Calculate percentiles if your UI supports it
- Investigate why some iterations are much slower
Ignoring logical reads
Ignoring logical reads
Why this matters:
- Logical reads = pages read from buffer cache
- Direct indicator of IO efficiency
- Reducing logical reads almost always improves performance
- Query A: 50ms average, 10,000 logical reads
- Query B: 55ms average, 1,000 logical reads
- Query B is better: Lower IO means better scalability under load
- Data not in cache (cold cache scenario)
- High concurrency (cache contention)
- Large databases (cache can’t hold everything)
Not accounting for first execution
Not accounting for first execution
Issue:
The first iteration may be slower due to:
- Plan compilation (LoadEngine caches SqlCommand objects, but plans may recompile)
- Cold buffer cache
- Statistics loading
- Run a warm-up test first
- Use high iteration counts (100+) so first execution is insignificant
- Or manually exclude first iteration from analysis
Testing only ideal conditions
Testing only ideal conditions
Real production has:
- Stale statistics
- Fragmented indexes
- Other concurrent workload
- Plan cache pressure
- TempDB contention
- Test with realistic concurrent load (use multiple test scenarios)
- Occasionally clear plan cache between tests
- Test with statistics out of date
- Run alongside other database activity
CPU-Bound vs IO-Bound Analysis
CompareCpuTime vs ElapsedTime from statistics:
CPU-bound query:
- CPU time << elapsed time
- Solution: Optimize query logic, reduce computations, improve indexes to reduce rows processed
- CPU time << elapsed time
- Solution: Reduce logical reads, add covering indexes, increase memory
- Both CPU and elapsed are low, but client Time is high
- Solution: Network issues, locking/blocking, check for waits in Extended Events
Comparing Test Runs
Test Configuration Consistency
For valid comparisons, keep identical:Save your test configurations! SQL Query Stress can save/load settings (QueryStressSettings.cs is serializable) to ensure consistency.
- Connection string (same server, database)
- Thread count (affects concurrency and connection pool)
- Iteration count (affects statistical confidence)
- CommandTimeout (QueryStressSettings.cs:30)
- ForceDataRetrieval (QueryStressSettings.cs:48)
- CollectIoStats / CollectTimeStats (QueryStressSettings.cs:18, 24)
- DelayBetweenQueries (QueryStressSettings.cs:84)
- Parameter query and mappings (if using parameters)
Making Valid Comparisons
Good comparison:Statistical Significance
When results are close:- Run multiple test cycles (3-5 complete runs of each configuration)
- Calculate standard deviation
- Look for consistent patterns across runs
- Small differences (<5%) may be noise
Advanced Testing Strategies
Progressive Load Testing
Parameter Distribution Testing
When using the parameter query feature (LoadEngine.cs:210-272): Design parameter queries to reflect production:- Include same distribution of values (not just SELECT TOP 100)
- Include edge cases (NULLs, empty strings, large values)
- Randomize order to avoid cache effects
Interlocked.Increment (LoadEngine.cs:223), cycling when reaching the end.
Endurance Testing
Test for degradation over time: Configuration:- Low thread count (5-10)
- Very high iteration count (10,000-100,000)
- Monitor for:
- Increasing execution times
- Growing memory usage
- Plan cache bloat
- TempDB growth
Cancellation Testing
The LoadEngine gives threads 5 seconds to exit gracefully (LoadEngine.cs:190) before forcing interruption. Queries should respond to cancellation within this window.Common Pitfalls to Avoid
- Testing SELECT * instead of actual column list: Column list affects IO and network transfer
- Forgetting to update statistics after data changes: Results will be misleading
- Testing with empty TempDB: Production TempDB is usually busy with other operations
- Not clearing plan cache between major changes: May test cached plan, not new query
- Using COUNT(*) to validate results: Adds overhead that skews timing
- Testing stored procedures without parameters: Real usage always includes parameters
- Assuming faster = better: Sometimes slower query with fewer reads is more scalable
- Not monitoring SQL Server during tests: Use Perfmon/DMVs to see resource utilization
Further Reading
- SQL Server Query Store
- Index Tuning Guidance
- Extended Events for Performance Monitoring
- LoadEngine.cs source code - Complete implementation details