Quick Answer
SOS_SCHEDULER_YIELD occurs when tasks voluntarily yield CPU time after consuming their quantum (4ms by default), allowing other tasks to run. This wait type indicates CPU pressure and typically points to inefficient queries performing large scans, missing indexes, or suboptimal execution plans requiring CPU-intensive operations.
Root Cause Analysis
SQL Server uses cooperative scheduling through the SQL Operating System (SQLOS) with one scheduler per logical CPU core. Each task receives a 4-millisecond quantum to execute on a worker thread. When a task consumes its full quantum without completing, it voluntarily yields the scheduler and enters the runnable queue with PENDING state, generating SOS_SCHEDULER_YIELD waits.
The scheduler maintains three queues: runnable (tasks waiting for CPU), suspended (tasks waiting for resources), and running (currently executing task). Tasks in SOS_SCHEDULER_YIELD waits sit in the runnable queue competing for CPU time. High values indicate CPU saturation where tasks cannot complete within their quantum due to expensive operations like index scans, hash joins, sorts, or aggregations.
CPU-bound queries trigger frequent yielding when they perform operations requiring multiple quantum cycles. Common culprits include missing indexes forcing table scans, outdated statistics causing poor cardinality estimates, parameter sniffing leading to suboptimal plans, and queries with high logical reads per execution.
SQL Server 2016 introduced automatic soft-NUMA which affects scheduler distribution. SQL Server 2019 added intelligent query processing features that can reduce CPU pressure through adaptive joins and memory grants. SQL Server 2022's parameter-sensitive plan optimization addresses some parameter sniffing scenarios that previously caused excessive CPU consumption.
The quantum length remains consistent across versions, but scheduler behavior improved in SQL Server 2016+ with better NUMA awareness and worker thread management.
AutoDBA checks CPU utilization patterns, scheduler queue depths, and query execution plan efficiency across your entire SQL Server instance in 60 seconds. Download the free diagnostic script and see what else needs attention.
Diagnostic Queries
-- Identify top CPU-consuming queries contributing to scheduler pressure
SELECT TOP 20
qs.sql_handle,
qs.plan_handle,
qs.total_worker_time / qs.execution_count AS avg_cpu_time_ms,
qs.total_logical_reads / qs.execution_count AS avg_logical_reads,
qs.execution_count,
qs.total_elapsed_time / qs.execution_count AS avg_duration_ms,
SUBSTRING(st.text, (qs.statement_start_offset/2)+1,
((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text)
ELSE qs.statement_end_offset END - qs.statement_start_offset)/2) + 1) AS query_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
WHERE qs.total_worker_time > 1000000 -- Filter queries using significant CPU
ORDER BY qs.total_worker_time DESC;
-- Check current scheduler runnable queue lengths and worker utilization
SELECT
scheduler_id,
cpu_id,
current_tasks_count,
runnable_tasks_count, -- Tasks waiting for CPU
current_workers_count,
active_workers_count,
work_queue_count,
pending_disk_io_count,
load_factor -- Scheduler load percentage
FROM sys.dm_os_schedulers
WHERE scheduler_id < 255 -- Exclude hidden schedulers
ORDER BY runnable_tasks_count DESC;
-- Analyze wait statistics focusing on CPU-related waits
SELECT
wait_type,
waiting_tasks_count,
wait_time_ms,
max_wait_time_ms,
signal_wait_time_ms, -- CPU wait time
wait_time_ms - signal_wait_time_ms AS resource_wait_time_ms,
CAST(100.0 * wait_time_ms / SUM(wait_time_ms) OVER() AS DECIMAL(5,2)) AS pct_total_waits
FROM sys.dm_os_wait_stats
WHERE wait_type IN ('SOS_SCHEDULER_YIELD', 'CXPACKET', 'PAGEIOLATCH_SH', 'PAGEIOLATCH_EX')
AND waiting_tasks_count > 0
ORDER BY wait_time_ms DESC;
-- Identify missing indexes that could reduce CPU pressure
SELECT
migs.avg_total_user_cost * (migs.avg_user_impact / 100.0) * (migs.user_seeks + migs.user_scans) AS improvement_measure,
migs.last_user_seek,
migs.last_user_scan,
mid.statement AS table_name,
mid.equality_columns,
mid.inequality_columns,
mid.included_columns
FROM sys.dm_db_missing_index_groups mig
INNER JOIN sys.dm_db_missing_index_group_stats migs ON migs.group_handle = mig.index_group_handle
INNER JOIN sys.dm_db_missing_index_details mid ON mig.index_handle = mid.index_handle
WHERE migs.avg_total_user_cost > 10 -- Focus on expensive queries
ORDER BY improvement_measure DESC;
-- Check for plan cache pollution and parameter sniffing issues
SELECT TOP 20
cp.objtype,
cp.cacheobjtype,
cp.size_in_bytes,
cp.usecounts,
st.text,
qp.query_plan
FROM sys.dm_exec_cached_plans cp
CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) st
CROSS APPLY sys.dm_exec_query_plan(cp.plan_handle) qp
WHERE cp.cacheobjtype = 'Compiled Plan'
AND cp.objtype = 'Adhoc'
AND cp.usecounts = 1 -- Single-use plans indicate parameterization issues
ORDER BY cp.size_in_bytes DESC;
Fix Scripts
-- Force plan recompilation for queries with poor parameter sniffing
-- WARNING: Test in development first, causes temporary CPU spike
DECLARE @sql_handle VARBINARY(64) = 0x... -- Replace with actual handle from diagnostic queries
-- Option 1: Recompile specific plan
DBCC FREEPROCCACHE (@sql_handle);
-- Option 2: Add query hint to problematic stored procedure
-- ALTER PROCEDURE [procedure_name] WITH RECOMPILE
-- or add OPTION (RECOMPILE) to specific statements
-- Create missing indexes identified in diagnostics
-- Replace table_name and columns based on missing index DMV results
-- TEST IN DEVELOPMENT ENVIRONMENT FIRST
-- Example based on missing index output
CREATE NONCLUSTERED INDEX IX_TableName_FilterCols_IncludeCols
ON [schema].[table_name] ([equality_columns])
INCLUDE ([included_columns])
WITH (ONLINE = ON, MAXDOP = 4); -- Adjust MAXDOP based on server capacity
-- Monitor index usage after creation
-- SELECT * FROM sys.dm_db_index_usage_stats WHERE object_id = OBJECT_ID('[schema].[table_name]')
-- Update statistics for tables with high CPU queries
-- Identify tables from expensive query analysis first
DECLARE @table_name SYSNAME = 'schema.table_name' -- Replace with actual table
-- Full statistics update with sample scan
UPDATE STATISTICS [@table_name] WITH FULLSCAN;
-- Alternative: Update with sampling for large tables
-- UPDATE STATISTICS [@table_name] WITH SAMPLE 20 PERCENT;
-- Verify statistics freshness
SELECT
OBJECT_NAME(s.object_id) AS table_name,
s.name AS stats_name,
s.auto_created,
STATS_DATE(s.object_id, s.stats_id) AS last_updated,
sp.modification_counter
FROM sys.stats s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp
WHERE s.object_id = OBJECT_ID(@table_name);
-- Implement query optimization hints for problematic queries
-- Add appropriate hints based on execution plan analysis
-- Example: Force specific join order for complex queries
SELECT /* OPTION (FORCE ORDER) */ columns
FROM table1 t1
INNER JOIN table2 t2 ON t1.id = t2.id
WHERE conditions;
-- Example: Limit parallelism for queries causing CXPACKET waits
SELECT columns
FROM large_table
WHERE complex_conditions
OPTION (MAXDOP 4); -- Adjust based on server cores and workload
-- Example: Force index usage when optimizer chooses wrong plan
SELECT columns
FROM table_name WITH (INDEX(IX_specific_index))
WHERE conditions;
AutoDBA generates fix scripts like these automatically, with impact estimates and rollback SQL included.
Prevention
Configure cost threshold for parallelism appropriately (typically 25-50) and set MAXDOP to number of physical cores per NUMA node, capped at 8. Enable automatic plan correction in SQL Server 2017+ to address plan regression. Implement regular statistics maintenance with AUTO_UPDATE_STATISTICS_ASYNC enabled for large databases.
Monitor query performance continuously using Query Store (enabled by default in SQL Server 2019+) to identify plan regression and forced parameterization opportunities. Set up intelligent query processing features in SQL Server 2019+ including adaptive memory grants and adaptive joins.
Create filtered statistics on large tables with skewed data distributions. Consider columnstore indexes for analytical workloads and in-memory OLTP for high-concurrency OLTP scenarios. Implement connection pooling and avoid ad-hoc queries that pollute plan cache.
Schedule statistics updates during maintenance windows using SQL Server Agent jobs. Monitor wait statistics trends using extended events or third-party tools. Configure Resource Governor for mixed workloads to prevent CPU-intensive queries from overwhelming the system.
Establish query performance baselines and alert thresholds. Regular execution plan analysis prevents performance degradation before it impacts production workloads.
Need hands-on help?
Dealing with persistent sos_scheduler_yield issues across your environment? Samix Technology provides hands-on SQL Server performance consulting with 15+ years of production DBA experience.