In short
Your workload decides whether many cores or fast cores are better, not the data sheet. A single query that runs serially only gets faster with a faster core. More cores help parallel queries and many concurrent sessions, but not without limit and not linearly.
Before you decide on cores, ask what the CPU is working on today. In our lab, 5 percent of the calls consumed 84 percent of the CPU. Doubling the core count brought 43 percent more throughput; the improved query on the old hardware brought 3.6 times as much.
A real CPU bottleneck does not show up in a single number. Long response times alone do not prove it, and neither does the share of signal wait time alone. Only several metrics together give a reliable picture.
The picture: the supermarket checkout
Picture the CPU as the checkout area of a supermarket. Each core is a checkout, and each query is a customer with a cart.
Faster cashierEvery customer is done sooner.≈ faster core
More checkoutsShorter line, same time per customer.≈ more cores
One cart, three checkoutsDone sooner, but splitting it costs work.≈ parallel query
Overloaded cartBlocks a checkout for a long time.≈ inefficient query
- A faster cashier speeds up every single customer. That is a faster core.
- More checkouts shorten the line, but not the time each customer spends at the checkout. That is more cores for many concurrent queries.
- One big cart, split across several checkouts, is done sooner. Splitting and merging it costs extra work, though. That is a parallel query.
- An overloaded cart blocks a checkout for a long time, no matter how many there are. That is the inefficient query that consumes most of the CPU.
With this picture, the three experiments are easy to read.
In the picture: faster cashier, split cart
Experiment 1: one query, more cores
Expectation A query that runs on a single core cannot benefit from additional cores. The same query running in parallel should get faster, but with growing overhead.
Change A compute-intensive aggregation over 5 million rows, once serially (MAXDOP 1) and once in parallel with as many threads as cores are assigned. Measured with 2, 4, 8 and 12 cores.
Observation
| Cores | serial | parallel | faster than serial | parallel CPU time |
|---|---|---|---|---|
| 2 | 2,382 ms | 1,933 ms | 1.23× | 1.38× |
| 4 | 2,376 ms | 1,201 ms | 1.98× | 1.52× |
| 8 | 2,384 ms | 612 ms | 3.89× | 1.65× |
| 12 | 2,368 ms | 555 ms | 4.27× | 2.28× |
Run serially, the query took equally long with 2 and with 12 cores. In parallel it got faster with every step, but the gain per additional core shrank. From 8 to 12 cores, the run time dropped by only 9 percent, while the CPU time used rose by 38 percent.
Conclusion For a serial query, only the speed of a single core counts. In the lab we could not change the clock speed; that a faster core helps here follows from the fact that the number of cores makes no difference. Parallelism shortens the run time but costs additional CPU time for distributing and gathering the work. How much of it makes sense is described on the page about parallelism in PSG QX: the goal is a shorter run time at a reasonable CPU cost, not smaller wait statistics.
In the picture: more checkouts
Experiment 2: many sessions, more cores
Expectation Many concurrent short queries should benefit from more cores, because the line is spread across more of them.
Change 15 sessions running short, compute-intensive queries without pause, plus one session that regularly starts a large reporting query. Measured with 2, 4, 8 and 12 cores.
Observation
| Cores | short queries per second | p95 short query | reporting query (median) | CPU utilization |
|---|---|---|---|---|
| 2 | 450 | 60.8 ms | 7,187 ms | 99.9% |
| 4 | 770 | 37.0 ms | 2,056 ms | 98.9% |
| 8 | 1,317 | 21.7 ms | 660 ms | 94.3% |
| 12 | 1,374 | 21.5 ms | 691 ms | 89.0% |
Up to 8 cores, throughput rose by about 70 percent per doubling, and response times dropped noticeably. From 8 to 12 cores, only 4 percent more came on top. At the same time, each short query needed about 40 percent more CPU time.
Conclusion The common rule of thumb “fast cores for OLTP, many cores for data warehouse” falls short. In our lab, it was the workload of many short queries that benefited from more cores, while the single long serial query did not benefit at all. What matters is how much work is pending at the same time and whether it can be spread out.
That 12 cores brought hardly more than 8 is specific to this lab host. Our cores are virtual CPUs under Hyper-V. More virtual CPUs do not automatically mean more physical computing power, for example when they share hyperthreads, clock speed or cache of the host. We have not isolated the exact cause.
In the picture: more checkouts or overloaded cart
Experiment 3: more cores or a better query?
Expectation If a single inefficient query consumes most of the CPU, improving it should bring more than additional cores.
Change 16 sessions with short think time. Every twentieth call is a single-day query that applies a function to a date column and therefore cannot use the index in a targeted way. Baseline on 4 cores, then the same load on 8 cores, then the single-day query rewritten so that it uses the index, again on 4 cores.
Observation
| Variant | queries per second | p95 short query | p95 single-day query | CPU utilization |
|---|---|---|---|---|
| 4 cores, baseline | 163 | 30.5 ms | 1,874 ms | 97% |
| 8 cores, otherwise unchanged | 234 | 21.3 ms | 1,241 ms | 78% |
| 4 cores, query improved | 584 | 14.3 ms | 7 ms | 61% |
The single-day query accounted for 5 percent of the calls and consumed 84 percent of the CPU. Doubling the core count brought 43 percent more throughput; the single-day query stayed slow at over a second. The improved query on the old 4 cores brought 3.6 times the throughput at a much lower utilization.
The cross-check. If the workload is already efficient and the CPU is full anyway, the picture is different. Such a workload on 4 cores at 98 percent utilization reached 62 percent more throughput with 8 cores, and p95 was cut in half, from 31.8 to 15.5 milliseconds.
Conclusion More cores help when the CPU is needed for necessary work. If an overloaded cart takes up most of the checkout time, the better query is the stronger lever. It is usually also the cheaper one, because SQL Server generally uses per-core licensing.
How to recognize a real CPU bottleneck
In the lab we raised the load step by step from 1 to 32 concurrent sessions, on 4 cores. From 16 sessions on, the CPU was at 99 percent, throughput stalled at 922 queries per second, and response times grew with every additional session. For comparison, a completely different case: queries that waited for a lock took 2 seconds while CPU utilization was 0.6 percent. More cores would not have changed anything there.
This yields five metrics that you should look at together:
- 01
CPU utilization
close to 100 percent over a longer period, viewed in the operating system and per core, not just as an average.
- 02
Runnable tasks per scheduler
If several tasks per core permanently wait in the queue (
runnable_tasks_count), CPU time is in short supply. - 03
Signal wait time per second
in relation to the number of cores. What counts is the absolute amount, not the share.
- 04
Throughput plateau
More concurrent work does not get more work done, only longer response times.
- 05
CPU per execution of the most expensive queries
A few queries with high consumption argue for improving the query before buying new hardware.
Two traps. The share of signal wait time in total wait time is not a good measure on its own. With pure compute work there are hardly any other waits; in our lab the share was close to 100 percent at only 26 percent CPU utilization. And an average utilization below 100 percent does not rule out a queue: with a few permanently active connections, two queries at times ran on the same scheduler in our lab while three others stayed empty. That is why it pays to look per scheduler.
The following queries only read the values; we tested them on SQL Server 2017 to 2025. Run them while the problem is occurring. The second one filters out harmless background waits; otherwise it shows high values even on an idle server. Because many tasks wait at the same time, signal wait time per second can be greater than 1,000 milliseconds. For orientation, here are the values per second and core from our lab: about 0.3 ms with a single session, around 440 ms at 75 percent utilization, around 1,080 ms at full saturation. The third one reads the plan cache; on very large systems it can take a few seconds. The values in the plan cache start at the last restart or at the time a plan entered the cache.
Read-only – changes nothing in your database
-- Read-only: queue per scheduler (snapshot)
SELECT scheduler_id, cpu_id,
current_tasks_count, runnable_tasks_count, active_workers_count
FROM sys.dm_os_schedulers
WHERE status = N'VISIBLE ONLINE'
ORDER BY scheduler_id
OPTION (MAXDOP 1);
-- Read-only: signal wait time per second and per core over 10 seconds,
-- excluding harmless background waits (same list as in the guide, not exhaustive)
IF OBJECT_ID('tempdb..#before') IS NOT NULL DROP TABLE #before;
SELECT wait_type, signal_wait_time_ms
INTO #before
FROM sys.dm_os_wait_stats
OPTION (MAXDOP 1);
WAITFOR DELAY '00:00:10';
DECLARE @cores int = (SELECT COUNT(*) FROM sys.dm_os_schedulers
WHERE status = N'VISIBLE ONLINE');
SELECT cores = @cores,
signal_ms_per_second = SUM(n.signal_wait_time_ms - v.signal_wait_time_ms) / 10.0,
signal_ms_per_second_per_core = SUM(n.signal_wait_time_ms - v.signal_wait_time_ms) / 10.0 / @cores
FROM sys.dm_os_wait_stats AS n
JOIN #before AS v ON v.wait_type = n.wait_type
WHERE n.wait_type NOT LIKE N'SLEEP[_]%'
AND n.wait_type NOT LIKE N'BROKER[_]%'
AND n.wait_type NOT LIKE N'XE[_]%'
AND n.wait_type NOT LIKE N'QDS[_]%'
AND n.wait_type NOT LIKE N'HADR[_]%'
AND n.wait_type NOT LIKE N'PARALLEL[_]REDO[_]%'
AND n.wait_type NOT IN (
N'AZURE_IMDS_VERSIONS', N'CHECKPOINT_QUEUE', N'CHKPT', N'CLR_AUTO_EVENT', N'CLR_MANUAL_EVENT',
N'CLR_SEMAPHORE', N'DBMIRROR_DBM_EVENT', N'DBMIRROR_EVENTS_QUEUE',
N'DBMIRROR_WORKER_QUEUE', N'DBMIRRORING_CMD', N'DIRTY_PAGE_POLL',
N'DISPATCHER_QUEUE_SEMAPHORE', N'EXECSYNC', N'FSAGENT',
N'FT_IFTS_SCHEDULER_IDLE_WAIT', N'FT_IFTSHC_MUTEX', N'KSOURCE_WAKEUP',
N'LAZYWRITER_SLEEP', N'LOGMGR_QUEUE', N'MEMORY_ALLOCATION_EXT',
N'ONDEMAND_TASK_QUEUE', N'PREEMPTIVE_OS_FLUSHFILEBUFFERS',
N'PREEMPTIVE_XE_GETTARGETSTATE', N'PVS_PREALLOCATE',
N'PWAIT_ALL_COMPONENTS_INITIALIZED', N'PWAIT_DIRECTLOGCONSUMER_GETNEXT',
N'PWAIT_EXTENSIBILITY_CLEANUP_TASK', N'REDO_THREAD_PENDING_WORK',
N'REQUEST_FOR_DEADLOCK_SEARCH', N'RESERVED_MEMORY_ALLOCATION_EXT', N'RESOURCE_QUEUE', N'SERVER_IDLE_CHECK',
N'SNI_HTTP_ACCEPT', N'SOS_WORK_DISPATCHER', N'SP_SERVER_DIAGNOSTICS_SLEEP',
N'SQLTRACE_BUFFER_FLUSH', N'SQLTRACE_INCREMENTAL_FLUSH_SLEEP',
N'SQLTRACE_WAIT_ENTRIES', N'VDI_CLIENT_OTHER', N'WAIT_FOR_RESULTS',
N'WAITFOR', N'WAITFOR_TASKSHUTDOWN', N'WAIT_XTP_CKPT_CLOSE',
N'WAIT_XTP_HOST_WAIT', N'WAIT_XTP_OFFLINE_CKPT_NEW_LOG', N'WAIT_XTP_RECOVERY')
OPTION (MAXDOP 1);
-- Read-only: the ten queries with the most CPU time since plan creation
SELECT TOP (10)
cpu_ms_total = qs.total_worker_time / 1000,
executions = qs.execution_count,
cpu_ms_per_execution = qs.total_worker_time / 1000.0 / qs.execution_count,
cpu_share_percent = 100.0 * qs.total_worker_time
/ SUM(qs.total_worker_time) OVER (),
query_text = SUBSTRING(st.text, qs.statement_start_offset / 2 + 1, 200)
FROM sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
ORDER BY qs.total_worker_time DESC
OPTION (MAXDOP 1);
For high CPU load, the guide SQL Server Slow Under Load walks you from symptom to cause.
Many cores or fast cores?
The experiments suggest a mapping by workload profile. It does not replace measuring your own workload, but it shows what to look for.
| Workload profile | How to recognize it | What tends to help |
|---|---|---|
| Individual long, serial queries, such as nightly runs | one session, one core fully busy, overall utilization low | faster cores, or make the query more efficient or parallelizable |
| Large reporting queries that run in parallel | parallelism waits, high CPU time per execution | more cores up to a point, plus the right dose through MAXDOP and cost threshold for parallelism |
| Many concurrent short queries | CPU near 100%, growing queue per scheduler, throughput plateau | more cores, if the work itself is efficient |
| A few expensive queries dominate the CPU | high CPU per execution for few queries | improve the query first |
| Long response time at low CPU | lock, I/O or other waits | more cores will not help; look for the cause in the guide |
Three things that influence the decision
- Licenses. SQL Server generally uses per-core licensing. Every additional core therefore costs more than hardware. Standard Edition also uses only a limited number of cores; more on that in the section on edition in the upgrade decision guide. Your licensing partner can confirm licensing questions for you. We don't sell licenses and therefore advise without a revenue interest.
- Virtualization. A virtual CPU is not a physical core. Whether additional virtual CPUs bring computing power depends on what the host underneath provides. Our lab shows that the gain can become small beyond a certain point.
- Tomorrow's workload. A query that runs serially today can run in parallel after a change, and vice versa. Cores are a decision for years; queries change with every release.
Frequently asked questions
Doesn't more hardware always help a little?
Often a little, but rarely the most. In our lab, twice as many cores brought 43 percent more throughput; the improved query brought 3.6 times as much. If the hardware only helps because a query does too much work, the problem keeps growing with the data.
Our CPU is at 100 percent. Do we need more cores?
Maybe. First check which queries consume the CPU. If the load is spread across many efficient queries and the queue per scheduler stays high, more cores help. If a few expensive queries dominate, improving them is the stronger lever.
The CPU is low, yet everything is slow. What now?
Then the work is usually waiting on something else: locks, memory, disk, network or the application itself. More cores change nothing about that. The guide shows which waits are behind it.
Should we turn off hyperthreading?
There is no blanket answer. A hyperthread is not a second full core; how much it adds depends on the processor and the workload. In our lab it made no measurable difference whether four virtual CPUs sat as pairs or apart inside the virtual machine; how the host maps them to its cores, we cannot see from there. A decision needs a measurement on your platform.
How many cores does our SQL Server need?
As many as your efficient workload actually uses at peak times, with headroom for growth. That can be derived from the execution history over several weeks, not from a snapshot.
Method and limits
All measurements come from our lab series of Oct 23, 2026: SQL Server 2022 CU27 Developer in a Linux container, virtual CPUs under Hyper-V on an Intel Xeon at 2.4 GHz, all assigned CPUs in one NUMA node, 6 GiB of maximum SQL Server memory. Synthetic data with 10 million rows in memory, identical statistics. Each variant ran in five measurement windows after a warm-up phase; the values given are medians. Variants whose windows differed from each other by more than ten percent were repeated with longer windows. The load generator ran on separate CPUs and was never the bottleneck.
This is a lab, not a customer environment. We did not change the clock speed; statements about faster cores are conclusions from the serial measurement. The curve beyond 8 cores applies to this host. Two variants with a few permanently active sessions varied widely because queries were distributed unevenly across the schedulers; we used them only as an indication. For the 8-core variant in Experiment 3, the medians of two measurement series were close together, but the spread within one series was above ten percent.
How we do it Our engineers use PSG QX to analyze, from your existing Query Store, which queries actually consume the CPU and which run in parallel without finishing sooner, over weeks rather than from a snapshot. More about PSG QX
We help with the decision
In the SQL Server Health Check we check, under “Edition & cores,” whether your workload uses the existing cores sensibly and where the bottleneck actually lies. For planning new hardware, we assess core count and edition from your point of view, see Infrastructure & high availability.