A family of Microsoft relational database management and analysis systems for e-commerce, line-of-business, and data warehousing solutions.
I ran your script on SQL Server 2022 and SQL Server 2025 on the same hardware (Docker, Linux containers, Express edition), including your exact 2025 build, CU10 17.0.5035.5. I couldn't reproduce a 15–48× slowdown, but I found something that produces a slowdown of that size.
Same hardware, your script, 1,000,000 iterations
- SQL Server 2022 (16.0.4305.5) Express: about 1,020 ms
- SQL Server 2025 CU10 (17.0.5035.5) Express: about 1,130 ms, roughly 10% slower
- On both, CPU time was almost equal to elapsed time, and the session's waits were negligible (a few hundred, under 10 ms in total)
Your own numbers point the same way: SQL Server 2017 took 1,813 ms and 2019 took 218 ms on different VMs. That's an 8× spread between older versions, so differences between VMs are large enough to swamp any version difference.
What did reproduce a 15–48× slowdown: per-statement instrumentation
This loop runs about 2 million tiny statements, so anything that adds even a little work per statement is multiplied 2 million times. With 100,000 iterations:
- No instrumentation: about 90 ms on both versions
-
SET STATISTICS TIME ON: about 2,200 ms on 2022 and 2,500 ms on 2025, roughly 25× slower - An Extended Events session capturing
sql_statement_completed: about 4× slower -
SET STATISTICS XML ON(the actual execution plan): no measurable effect on this loop
So it's worth checking whether the 2025 instances run the test with extra per-statement work that the older ones don't.
Step 1. Run the script with sqlcmd on the server itself. This removes client tool settings. In SSMS, also check Query → Query Options → Advanced for SET STATISTICS TIME or SET STATISTICS IO.
Step 2. Check for Extended Events sessions that capture statement-level events on each instance:
SELECT s.name AS session_name, e.event_name
FROM sys.dm_xe_sessions AS s
JOIN sys.dm_xe_session_events AS e ON e.event_session_address = s.address
WHERE e.event_name IN (N'sql_statement_completed', N'sql_statement_starting',
N'sp_statement_completed', N'sp_statement_starting',
N'query_post_execution_showplan')
ORDER BY s.name;
Step 3. Measure CPU time and waits in the same batch on each instance:
SET NOCOUNT ON;
DECLARE @t0 datetime2 = SYSDATETIME();
DECLARE @cpu0 int = (SELECT cpu_time FROM sys.dm_exec_requests WHERE session_id = @@SPID);
DECLARE @i int = 0;
WHILE @i < 1000000
BEGIN
SET @i = @i + 1;
END;
SELECT DATEDIFF(MILLISECOND, @t0, SYSDATETIME()) AS ElapsedMs,
(SELECT cpu_time FROM sys.dm_exec_requests WHERE session_id = @@SPID) - @cpu0 AS CpuMs;
SELECT TOP (5) wait_type, waiting_tasks_count, wait_time_ms
FROM sys.dm_exec_session_wait_stats
WHERE session_id = @@SPID
ORDER BY wait_time_ms DESC;
If you see millions of waits, or one wait type with a large total, that's per-statement overhead. If CPU time is close to elapsed time and waits are tiny, the instance is simply running the loop on slower CPU, which points to the VM (host CPU contention, power or frequency settings) rather than SQL Server.
Step 4. Compare versions on one VM. The most reliable test is to install SQL Server 2022 Express as a named instance on the same VM as 2025 Express and run the script on both. That removes the hardware differences from the comparison.
My test ran on Linux, not Windows, so it doesn't rule out a Windows-specific issue. It does show that the SQL Server 2025 engine isn't inherently 15–48× slower at this loop.
Let me know what Steps 2 and 3 show on the slow instance. If this helps you find the cause, please consider marking it as the accepted answer so others who see the same slowdown can find it.