SQL Server 2025 Series : OPTIMIZED_SP_EXECUTESQL β€” End the Compilation Storm Killing SQL Performance

When dozens of threads hit the same query simultaneously on a cold cache, SQL Server compiles the same plan over and over β€” burning CPU, wasting resources, and degrading throughput right when your system is most vulnerable. This is a compilation storm, and SQL Server 2025 has a one-line fix for it.


What is a compilation storm?

Every query needs a compiled execution plan before SQL Server can run it. Plans are cached β€” but when the cache is cold (after a restart, failover, or deployment) and many threads fire the same query at once, each thread independently compiles the same plan:

Comparison of SQL execution performance with and without optimized sp_executeSQL, illustrating cache misses and hits across multiple threads.

One thread pays the compilation cost. Everyone else waits a millisecond and reuses for free.

Step-by-step demo script

The full script below walks through all 10 steps end to end. Run each section in order.

1) Create the database and tables

Sets up jbexecute with Customers, Products, Orders, and a CompilationMetrics table to store before/after results.

USE master;
GO
IF EXISTS (SELECT 1 FROM sys.databases WHERE name = N'jbexecute')
BEGIN
ALTER DATABASE jbexecute SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
DROP DATABASE jbexecute;
END
GO
CREATE DATABASE jbexecute;
GO
ALTER DATABASE jbexecute SET RECOVERY SIMPLE;
GO
USE jbexecute;
GO
CREATE TABLE dbo.Customers
(
CustomerID INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
FirstName NVARCHAR(50) NOT NULL,
LastName NVARCHAR(50) NOT NULL,
Email NVARCHAR(100) NOT NULL,
Region NVARCHAR(30) NOT NULL,
CreatedDate DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
);
GO
CREATE TABLE dbo.Products
(
ProductID INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
ProductName NVARCHAR(100) NOT NULL,
Category NVARCHAR(50) NOT NULL,
UnitPrice DECIMAL(10,2) NOT NULL,
StockQty INT NOT NULL DEFAULT 1000
);
GO
CREATE TABLE dbo.Orders
(
OrderID INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
CustomerID INT NOT NULL REFERENCES dbo.Customers(CustomerID),
ProductID INT NOT NULL REFERENCES dbo.Products(ProductID),
Quantity INT NOT NULL,
TotalAmount DECIMAL(12,2) NOT NULL,
OrderStatus NVARCHAR(20) NOT NULL DEFAULT N'Pending',
OrderDate DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
);
GO
CREATE TABLE dbo.CompilationMetrics
(
MetricID INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
Phase NVARCHAR(10) NOT NULL,
CapturedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
CompileEvents INT NOT NULL,
TotalDurationNs BIGINT NOT NULL,
TotalDurationMs AS (TotalDurationNs / 1000000.),
Notes NVARCHAR(500) NULL
);

2) Seed sample Data

200 customers, 50 products, 5,000 orders.

;WITH cte AS
(
SELECT TOP 200 ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS n
FROM sys.all_columns a CROSS JOIN sys.all_columns b
)
INSERT INTO dbo.Customers (FirstName, LastName, Email, Region)
SELECT
N'FirstName' + CAST(n AS NVARCHAR(10)),
N'LastName' + CAST(n AS NVARCHAR(10)),
N'user' + CAST(n AS NVARCHAR(10)) + N'@shop.com',
CHOOSE((n % 5) + 1, N'North', N'South', N'East', N'West', N'Central')
FROM cte;
GO
;WITH cte AS
(
SELECT TOP 50 ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS n
FROM sys.all_columns
)
INSERT INTO dbo.Products (ProductName, Category, UnitPrice, StockQty)
SELECT
N'Product_' + CAST(n AS NVARCHAR(10)),
CHOOSE((n % 6) + 1,
N'Electronics', N'Clothing', N'Books',
N'Home & Garden', N'Sports', N'Toys'),
CAST((n * 7.99) AS DECIMAL(10,2)),
500 + (n * 10)
FROM cte;
GO
;WITH cte AS
(
SELECT TOP 5000 ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS n
FROM sys.all_columns a CROSS JOIN sys.all_columns b
)
INSERT INTO dbo.Orders (CustomerID, ProductID, Quantity, TotalAmount, OrderStatus)
SELECT
(n % 200) + 1,
(n % 50) + 1,
(n % 10) + 1,
CAST(((n % 50) + 1) * 7.99 * ((n % 10) + 1) AS DECIMAL(12,2)),
CHOOSE((n % 4) + 1, N'Pending', N'Shipped', N'Delivered', N'Cancelled')
FROM cte;

3) Set feature OFF and create Extended Events session

Captures query_post_compilation_showplan (compilation time) and sql_batch_completed (execution) to an .xel file.

-- Disable for BEFORE test
ALTER DATABASE SCOPED CONFIGURATION
SET ASYNC_STATS_UPDATE_WAIT_AT_LOW_PRIORITY = OFF;
ALTER DATABASE SCOPED CONFIGURATION
SET OPTIMIZED_SP_EXECUTESQL = OFF;
GO
USE master;
GO
IF EXISTS (SELECT 1 FROM sys.server_event_sessions WHERE name = N'Demo_CompilationStorm')
DROP EVENT SESSION Demo_CompilationStorm ON SERVER;
GO
CREATE EVENT SESSION [Demo_CompilationStorm] ON SERVER
ADD EVENT sqlserver.query_post_compilation_showplan
(
ACTION
(
sqlserver.sql_text,
sqlserver.database_name,
sqlserver.client_app_name,
sqlserver.session_id
)
WHERE ([sqlserver].[database_name] = N'jbexecute')
),
ADD EVENT sqlserver.sql_batch_completed
(
ACTION (sqlserver.sql_text, sqlserver.database_name)
WHERE ([sqlserver].[database_name] = N'jbexecute' AND [duration] > 0)
)
ADD TARGET package0.event_file
(
SET filename = N'C:\temp\JBExecute\Demo_CompilationStorm.xel',
max_file_size = 1024,
max_rollover_files = 5
)
WITH (MAX_DISPATCH_LATENCY = 5 SECONDS, TRACK_CAUSALITY = ON);
GO
ALTER EVENT SESSION [Demo_CompilationStorm] ON SERVER STATE = START;
GO

4) Create the workload file (workload.sql)

Save this as a .sql file. It is what ostress will fire on every thread and iteration.

DECLARE @custID INT;
DECLARE @prodID INT;
DECLARE @qty INT;
DECLARE @amount DECIMAL(12,2);
DECLARE @status NVARCHAR(20);
DECLARE @sql NVARCHAR(MAX);
DECLARE @params NVARCHAR(MAX);
SET @sql = N'
INSERT INTO dbo.Orders (CustomerID, ProductID, Quantity, TotalAmount, OrderStatus)
VALUES (@cid, @pid, @qty, @amt, @stat);
UPDATE dbo.Products
SET StockQty = StockQty - @qty
WHERE ProductID = @pid AND StockQty >= @qty;
SELECT o.OrderID,
c.FirstName + '' '' + c.LastName AS CustomerName,
p.ProductName,
o.Quantity,
o.TotalAmount,
o.OrderStatus
FROM dbo.Orders o
JOIN dbo.Customers c ON c.CustomerID = o.CustomerID
JOIN dbo.Products p ON p.ProductID = o.ProductID
WHERE o.CustomerID = @cid
ORDER BY o.OrderDate DESC;
';
SET @params = N'@cid INT, @pid INT, @qty INT, @amt DECIMAL(12,2), @stat NVARCHAR(20)';
SET @custID = (ABS(CHECKSUM(NEWID())) % 200) + 1;
SET @prodID = (ABS(CHECKSUM(NEWID())) % 50) + 1;
SET @qty = (ABS(CHECKSUM(NEWID())) % 5) + 1;
SET @amount = CAST(@prodID * 7.99 * @qty AS DECIMAL(12,2));
SET @status = N'Pending';
EXEC sp_executesql @sql, @params,
@cid = @custID, @pid = @prodID, @qty = @qty,
@amt = @amount, @stat = @status;

5) Run BEFORE workload with ostress

Clear the plan cache first. Demo servers only. Then fire ostress.

-- Clear plan cache (DEMO SERVER ONLY)
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;
GO
-- Run from command prompt:
-- ostress -S"YOUR_SERVER" -E -i"C:\temp\JBExecute\workload.sql" -n100 -r5 -q -djbexecute
ALTER EVENT SESSION [Demo_CompilationStorm] ON SERVER STATE = STOP;
GO
WAITFOR DELAY '00:00:05';

6) Capture and save BEFORE metrics

Reads the .xel file and stores compile event count and total compile duration into CompilationMetrics.

USE jbexecute;
GO
;WITH events AS
(
SELECT
n.value('(@name)[1]', 'NVARCHAR(100)') AS event_name,
n.value('(data[@name="duration"]/value)[1]', 'BIGINT') AS duration_ns,
n.value('(action[@name="database_name"]/value)[1]', 'NVARCHAR(128)') AS db_name
FROM
(
SELECT CAST(event_data AS XML) AS event_xml
FROM sys.fn_xe_file_target_read_file(
N'C:\temp\JBExecute\Demo_CompilationStorm*.xel',
NULL, NULL, NULL)
) src
CROSS APPLY src.event_xml.nodes('//event') AS t(n)
)
SELECT
'BEFORE (No Feature)' AS Phase,
event_name,
COUNT(*) AS CompileEventCount,
SUM(duration_ns) AS TotalDuration_ns,
SUM(duration_ns) / 1000000. AS TotalDuration_ms,
AVG(duration_ns) AS AvgDuration_ns,
MAX(duration_ns) AS MaxDuration_ns
FROM events
WHERE event_name = 'query_post_compilation_showplan'
AND db_name = 'jbexecute'
GROUP BY event_name;
-- Save to metrics table
;WITH events AS
(
SELECT
n.value('(@name)[1]', 'NVARCHAR(100)') AS event_name,
n.value('(data[@name="duration"]/value)[1]', 'BIGINT') AS duration_ns
FROM
(
SELECT CAST(event_data AS XML) AS event_xml
FROM sys.fn_xe_file_target_read_file(
N'C:\temp\JBExecute\Demo_CompilationStorm*.xel',
NULL, NULL, NULL)
) src
CROSS APPLY src.event_xml.nodes('//event') AS t(n)
)
INSERT INTO dbo.CompilationMetrics (Phase, CompileEvents, TotalDurationNs, Notes)
SELECT
'BEFORE',
COUNT(*),
ISNULL(SUM(duration_ns), 0),
'SQL Server without Optimized sp_executesql β€” concurrent compilation storm'
FROM events
WHERE event_name = 'query_post_compilation_showplan';

7) Enable OPTIMIZED_SP_EXECUTESQL

One line. No downtime. No application changes.

USE jbexecute;
GO
ALTER DATABASE SCOPED CONFIGURATION
SET ASYNC_STATS_UPDATE_WAIT_AT_LOW_PRIORITY = ON;
ALTER DATABASE SCOPED CONFIGURATION
SET OPTIMIZED_SP_EXECUTESQL = ON;
GO
-- Verify
SELECT name, value, value_for_secondary
FROM sys.database_scoped_configurations
WHERE name IN (N'OPTIMIZED_SP_EXECUTESQL',
N'ASYNC_STATS_UPDATE_WAIT_AT_LOW_PRIORITY');
GO
-- Fresh start for AFTER test
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;
GO
ALTER EVENT SESSION [Demo_CompilationStorm] ON SERVER STATE = STOP;
GO
ALTER EVENT SESSION [Demo_CompilationStorm] ON SERVER STATE = START;
GO
-- Run ostress again with identical parameters, then:
ALTER EVENT SESSION [Demo_CompilationStorm] ON SERVER STATE = STOP;
GO
WAITFOR DELAY '00:00:05';

8) Capture and save AFTER metrics

USE jbexecute;
GO
;WITH events AS
(
SELECT
n.value('(@name)[1]', 'NVARCHAR(100)') AS event_name,
n.value('(data[@name="duration"]/value)[1]', 'BIGINT') AS duration_ns,
n.value('(action[@name="database_name"]/value)[1]', 'NVARCHAR(128)') AS db_name
FROM
(
SELECT CAST(event_data AS XML) AS event_xml
FROM sys.fn_xe_file_target_read_file(
N'C:\Temp\JBExecute\Demo_CompilationStorm*.xel',
NULL, NULL, NULL)
) src
CROSS APPLY src.event_xml.nodes('//event') AS t(n)
)
SELECT
'AFTER (Optimized sp_executesql ON)' AS Phase,
event_name,
COUNT(*) AS CompileEventCount,
SUM(duration_ns) AS TotalDuration_ns,
SUM(duration_ns) / 1000000. AS TotalDuration_ms,
AVG(duration_ns) AS AvgDuration_ns,
MAX(duration_ns) AS MaxDuration_ns
FROM events
WHERE event_name = 'query_post_compilation_showplan'
AND db_name = 'jbexecute'
GROUP BY event_name;
;WITH events AS
(
SELECT
n.value('(@name)[1]', 'NVARCHAR(100)') AS event_name,
n.value('(data[@name="duration"]/value)[1]', 'BIGINT') AS duration_ns
FROM
(
SELECT CAST(event_data AS XML) AS event_xml
FROM sys.fn_xe_file_target_read_file(
N'C:\Temp\JBExecute\Demo_CompilationStorm*.xel',
NULL, NULL, NULL)
) src
CROSS APPLY src.event_xml.nodes('//event') AS t(n)
)
INSERT INTO dbo.CompilationMetrics (Phase, CompileEvents, TotalDurationNs, Notes)
SELECT
'AFTER',
COUNT(*),
ISNULL(SUM(duration_ns), 0),
'SQL Server 2025 with OPTIMIZED_SP_EXECUTESQL = ON β€” serialized compilation'
FROM events
WHERE event_name = 'query_post_compilation_showplan';

9) Compare before vs after

Shows compile event count, total compile duration, and average compile time per event β€” all with % reduction.

-- Phase detail
SELECT
Phase, CapturedAt, CompileEvents,
TotalDurationNs, TotalDurationMs AS TotalDuration_ms,
CASE WHEN Phase = 'BEFORE'
THEN 'Baseline β€” N compilations per burst'
ELSE 'Optimized β€” 1 compilation per unique batch'
END AS Description, Notes
FROM dbo.CompilationMetrics
ORDER BY MetricID;
-- Reduction summary
SELECT
b.CompileEvents AS BEFORE_CompileEvents,
a.CompileEvents AS AFTER_CompileEvents,
b.CompileEvents - a.CompileEvents AS Compilations_Saved,
CAST(100.0 * (b.CompileEvents - a.CompileEvents)
/ NULLIF(b.CompileEvents, 0) AS DECIMAL(5,1)) AS Compilations_Pct_Reduction,
b.TotalDurationMs AS BEFORE_TotalCompileDuration_ms,
a.TotalDurationMs AS AFTER_TotalCompileDuration_ms,
b.TotalDurationMs - a.TotalDurationMs AS CompileDuration_ms_Saved,
CAST(100.0 * (b.TotalDurationMs - a.TotalDurationMs)
/ NULLIF(b.TotalDurationMs, 0) AS DECIMAL(5,1)) AS CompileDuration_Pct_Reduction,
CASE WHEN b.CompileEvents > 0
THEN b.TotalDurationNs / b.CompileEvents END AS BEFORE_AvgCompile_ns,
CASE WHEN a.CompileEvents > 0
THEN a.TotalDurationNs / a.CompileEvents END AS AFTER_AvgCompile_ns,
CASE WHEN b.CompileEvents > 0 AND a.CompileEvents > 0
THEN (b.TotalDurationNs / b.CompileEvents)
- (a.TotalDurationNs / a.CompileEvents) END AS AvgCompile_ns_Saved,
CAST(
100.0 *
( (b.TotalDurationNs / NULLIF(b.CompileEvents, 0))
- (a.TotalDurationNs / NULLIF(a.CompileEvents, 0)) )
/ NULLIF(b.TotalDurationNs / NULLIF(b.CompileEvents, 0), 0)
AS DECIMAL(5,1)) AS AvgCompile_Pct_Reduction
FROM
(SELECT TOP 1 * FROM dbo.CompilationMetrics WHERE Phase = 'BEFORE' ORDER BY MetricID DESC) b
CROSS JOIN
(SELECT TOP 1 * FROM dbo.CompilationMetrics WHERE Phase = 'AFTER' ORDER BY MetricID DESC) a;
GO

10) Cleanup

ALTER EVENT SESSION [Demo_CompilationStorm] ON SERVER STATE = STOP;
DROP EVENT SESSION [Demo_CompilationStorm] ON SERVER;
GO
USE master;
ALTER DATABASE jbexecute SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
DROP DATABASE jbexecute;
GO

Things to know before enabling in production

  • SQL Server 2025 only. OPTIMIZED_SP_EXECUTESQL is not available on 2022 or earlier. ASYNC_STATS_UPDATE_WAIT_AT_LOW_PRIORITY is available from 2022 onwards.
  • Waiting threads add latency. Non-first threads wait for the compiling thread. Negligible in most cases β€” but test carefully if you have genuinely slow-compiling queries.
  • sp_executesql only. Ad-hoc SQL, stored procedures, and batches where the SQL text varies between calls are not covered by this feature.
  • Does not fix bad plans. This serializes compilation β€” it does not improve plan quality. Use Query Store for plan stability alongside this.
  • Test before production. Validate with Extended Events as shown in this demo. Every workload is different.

Watch the Full Demo

I’ve recorded a complete walkthrough of this setup on my YouTube channel JBSWiki. If you’re a visual learner, go check it out!

πŸ‘‰Β Watch here: https://www.youtube.com/watch?v=3BF9wn_IXWQ


Thank You,

Thank You,
Vivek Janakiraman

Disclaimer:
The views expressed on this blog are mine alone and do not reflect the views of my company or anyone else. All postings on this blog are provided β€œAS IS” with no warranties, and confers no rights.

SQL Server 2025 Series : Degree Of Parallelism (DOP) Feedback Explained with Real-Time Demo!

Parallelism tuning has always been one of the most challenging aspects of SQL Server performance optimization. DBAs often spend hours fine-tuning MAXDOP settings, trying to strike the perfect balance between performance and resource consumption.

With SQL Server 2025, this challenge is significantly reduced thanks to Degree of Parallelism (DOP) Feedbackβ€”a powerful Intelligent Query Processing feature that automatically optimizes parallel query execution.

In this blog, we will walk through:

  • What DOP Feedback is
  • How to track it using Extended Events
  • A real-time demo with multiple scenarios
  • How to validate whether DOP Feedback is working on your server

What is DOP Feedback?

DOP Feedback enables SQL Server to self-adjust the degree of parallelism for repeated queries. Instead of relying on static MAXDOP settings, SQL Server analyzes runtime performance (CPU usage, execution time, waits) and dynamically tunes DOP for subsequent executions.

This helps:

  • Reduce excessive CPU usage
  • Improve query performance
  • Eliminate manual tuning effort

Tracking DOP Feedback using Extended Events

To understand how SQL Server applies DOP Feedback, we can capture internal engine decisions using Extended Events.

Setup DOP Feedback Tracking

IF EXISTS (SELECT 1 FROM sys.server_event_sessions WHERE name = 'DOPFeedbackWatch')
DROP EVENT SESSION DOPFeedbackWatch ON SERVER;
GO
CREATE EVENT SESSION DOPFeedbackWatch ON SERVER
ADD EVENT sqlserver.dop_feedback_eligible_query(
ACTION(sqlserver.sql_text, sqlserver.plan_handle, sqlserver.query_hash)),
ADD EVENT sqlserver.dop_feedback_provided(
ACTION(sqlserver.sql_text, sqlserver.plan_handle, sqlserver.query_hash)),
ADD EVENT sqlserver.dop_feedback_validation(
ACTION(sqlserver.sql_text, sqlserver.plan_handle, sqlserver.query_hash)),
ADD EVENT sqlserver.dop_feedback_stabilized(
ACTION(sqlserver.sql_text, sqlserver.plan_handle, sqlserver.query_hash)),
ADD EVENT sqlserver.dop_feedback_reverted(
ACTION(sqlserver.sql_text, sqlserver.plan_handle, sqlserver.query_hash)),
ADD EVENT sqlserver.dop_feedback_analysis_stopped(
ACTION(sqlserver.sql_text, sqlserver.plan_handle, sqlserver.query_hash)),
ADD EVENT sqlserver.dop_feedback_reassessment_failed(
ACTION(sqlserver.sql_text, sqlserver.plan_handle, sqlserver.query_hash))
ADD TARGET package0.ring_buffer;
GO
ALTER EVENT SESSION DOPFeedbackWatch ON SERVER STATE = START;
GO

What this captures

This session helps us track:

  • When a query becomes eligible for DOP Feedback
  • When SQL Server adjusts DOP
  • Whether feedback is validated or reverted
  • When the system stabilizes on an optimal DOP

This gives real-time visibility into the self-tuning engine behavior.


DOP Feedback Demo (Step-by-Step)

Let’s simulate workloads to observe DOP Feedback in action.


Scenario 1: High-Volume Parallel Query (Feedback Adjusted)

CREATE DATABASE jbdb
GO
USE JBDB
GO
--Create table and load table
CREATE TABLE [dbo].[Table1]([Col1] [int] IDENTITY(1,1) ON [PRIMARY]
GO
set nocount on
insert into Table1 (Col2, Col3, Col4, Col5) values (1, REPLICATE ('a',4000), 10, 100)
go 999
insert into Table1 (Col2, Col3, Col4, Col5) values (2, REPLICATE ('z',4000), 11, 100)
go 99999
insert into Table1 (Col2, Col3, Col4, Col5) values (3, REPLICATE ('f',4000), 12, 100)
go 8965
insert into Table1 (Col2, Col3, Col4, Col5) values (4, REPLICATE ('g',4000), 13, 100)
go 7844
insert into Table1 (Col2, Col3, Col4, Col5) values (5, REPLICATE ('u',4000), 14, 100)
go 4567
insert into Table1 (Col2, Col3, Col4, Col5) values (2, REPLICATE ('z',4000), 11, 100)
go 751049
-- Stored Procedure to query the table
CREATE OR ALTER PROCEDURE [dbo].[usp_GetDetails] @Col2 int
AS
BEGIN
SELECT top (50000)
[Col1],
[Col2],
[Col3],
[Col4],
[Col5]
FROM dbo.Table1
WHERE [Col2] = @Col2
ORDER BY [Col3];
END
--EXECUTE the stored procedure
EXEC [dbo].[usp_GetDetails] @Col2 = 2
--OSTRESS
C:\Program Files\Microsoft Corporation\RMLUtils>ostress -S"VIJANAK\IN2025" -E -Q"EXEC usp_GetDetails @Col2 = 2;" -n1 -r100 -q -dJBDB

In this scenario:

  • Query executes repeatedly under load
  • SQL Server identifies inefficiencies in parallelism
  • DOP is adjusted for better performance

Scenario 2: Skewed Parallelism (Feedback Evaluation)

use JBBlog
[dbo].[usp_SkewedParallelReport]
ostress -S"VIJANAK\IN2025" -E -Q"EXEC usp_SkewedParallelReport;" -n1 -r100 -q -dJBBlog

Here:

  • SQL Server evaluates skewed workloads
  • Determines whether reducing DOP improves efficiency
  • May validate or reject feedback

Monitoring Live Workload

To see what is actively running:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
-- What SQL Statements Are Currently Running?
SELECT [Spid] = session_Id
, ecid
, [Database] = DB_NAME(sp.dbid)
, [User] = nt_username
, [Status] = er.status
, [Wait] = wait_type
, [Individual Query] = SUBSTRING (qt.text,
er.statement_start_offset/2,
(CASE WHEN er.statement_end_offset = -1
THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2
ELSE er.statement_end_offset END -
er.statement_start_offset)/2)
,[Parent Query] = qt.text
, Program = program_name
, Hostname
, nt_domain
, start_time
FROM sys.dm_exec_requests er
INNER JOIN sys.sysprocesses sp ON er.session_id = sp.spid
CROSS APPLY sys.dm_exec_sql_text(er.sql_handle)as qt
where session_Id NOT IN (@@SPID) -- Ignore this current statement.
ORDER BY 1, 2
go

This helps you:

  • Identify active queries
  • Validate parallel workloads
  • Correlate with Extended Event output

Key Observations from the Demo

From the above scenarios, you will typically notice:

  1. DOP Feedback Applied Successfully
    • SQL Server reduces or adjusts DOP
    • Performance improves over repeated executions
  2. Optimal DOP Identified
    • No change needed
    • System confirms current DOP is efficient
  3. Query Not Eligible
    • Some queries are excluded
    • Depends on execution pattern and workload

Why This Matters

DOP Feedback fundamentally changes how DBAs approach tuning:

Traditional ApproachSQL Server 2025 Approach
Manual MAXDOP tuningAutomatic per-query tuning
Trial and errorData-driven decisions
Static configurationAdaptive optimization

Watch the Full Demo

I’ve recorded a complete walkthrough of this setup on my YouTube channel JBSWiki. If you’re a visual learner, go check it out!

πŸ‘‰ Watch here: https://www.youtube.com/watch?v=nVMeQeiUKTA


Final Thoughts

With SQL Server 2025, the database engine becomes smarter and more autonomous.

DOP Feedback:

  • Eliminates guesswork
  • Improves performance stability
  • Reduces CPU contention
  • Adapts dynamically to workload changes

For DBAs and performance engineers, this means less time tuning and more time delivering value.


If you are testing SQL Server 2025 in your environment, I highly recommend running this demo and observing the Extended Events output β€” it gives incredible insight into how the engine learns and adapts in real time.

Thank You,
Vivek Janakiraman

Disclaimer:
The views expressed on this blog are mine alone and do not reflect the views of my company or anyone else. All postings on this blog are provided β€œAS IS” with no warranties, and confers no rights.

SQL Server 2022 In-Memory OLTP Improvements: A Comprehensive Guide

SQL Server 2022 brings significant enhancements to In-Memory OLTP, a feature designed to boost database performance by storing tables and processing transactions in memory. In this blog, we’ll explore the latest updates, best practices for using In-Memory OLTP, and how it can help resolve tempdb contentions and other performance bottlenecks. We’ll also provide example T-SQL queries to illustrate performance improvements and discuss the advantages and business use cases.

What is In-Memory OLTP? πŸ€”

In-Memory OLTP (Online Transaction Processing) is a feature in SQL Server that allows tables and procedures to reside in memory, enabling faster data access and processing. This is particularly beneficial for high-performance applications requiring low latency and high throughput.

Key Updates in SQL Server 2022 πŸ› οΈ

  1. Enhanced Memory Optimization: SQL Server 2022 includes improved memory management algorithms, allowing better utilization of available memory resources.
  2. Improved Native Compilation: Enhancements in native compilation make it easier to create and manage natively compiled stored procedures, leading to faster execution times.
  3. Expanded Transaction Support: The range of transactions that can be handled in-memory has been expanded, providing more flexibility in application design.
  4. Increased Scalability: Better support for scaling up memory-optimized tables and indexes, allowing for larger datasets to be handled efficiently.

Best Practices for Using In-Memory OLTP πŸ“š

  1. Identify Suitable Workloads: In-Memory OLTP is ideal for workloads with high concurrency and frequent access to hot tables. Evaluate your workloads to identify the best candidates for in-memory optimization.
  2. Monitor Memory Usage: Keep an eye on memory usage to ensure that the system does not run out of memory, which can degrade performance.
  3. Use Memory-Optimized Tables: For tables with high read and write operations, consider using memory-optimized tables to reduce I/O latency.
  4. Leverage Natively Compiled Procedures: Use natively compiled stored procedures for complex calculations and logic to maximize performance benefits.

Enabling In-Memory OLTP on a Database πŸ› οΈ

Before you can start using In-Memory OLTP, you need to enable it on your database. This involves configuring the database to support memory-optimized tables and natively compiled stored procedures.

Step 1: Enable the Memory-Optimized Data Filegroup

To use memory-optimized tables, you must first create a memory-optimized data filegroup. This special filegroup stores data for memory-optimized tables.

ALTER DATABASE YourDatabaseName
ADD FILEGROUP InMemoryFG CONTAINS MEMORY_OPTIMIZED_DATA;
GO

ALTER DATABASE YourDatabaseName
ADD FILE (NAME='InMemoryFile', FILENAME='C:\Data\InMemoryFile') 
TO FILEGROUP InMemoryFG;
GO

Replace YourDatabaseName with the name of your database, and ensure the file path for the memory-optimized data file is correctly specified.

Step 2: Configure the Database for In-Memory OLTP

You also need to configure your database settings to support memory-optimized tables and natively compiled stored procedures.

ALTER DATABASE YourDatabaseName
SET MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT = ON;
GO

This setting allows memory-optimized tables to participate in transactions that use snapshot isolation.

Creating In-Memory Tables πŸ“

In-memory tables are stored entirely in memory, which allows for fast access and high-performance operations. Here’s an example of how to create an in-memory table:

CREATE TABLE dbo.MemoryOptimizedTable
(
    ID INT NOT NULL PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1000000),
    Name NVARCHAR(100) NOT NULL,
    CreatedDate DATETIME2 NOT NULL DEFAULT (GETDATE())
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);
GO
  • BUCKET_COUNT: Specifies the number of hash buckets for the hash index, which should be set based on the expected number of rows.
  • MEMORY_OPTIMIZED = ON: Indicates that the table is memory-optimized.
  • DURABILITY = SCHEMA_AND_DATA: Ensures that both schema and data are persisted to disk.

Using In-Memory Temporary Tables πŸ“Š

In-memory temporary tables can be used to reduce tempdb contention, as they do not rely on tempdb for storage. Here’s how to create and use an in-memory temporary table:

CREATE TABLE #InMemoryTempTable
(
    ID INT NOT NULL PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1000),
    Data NVARCHAR(100) NOT NULL
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_ONLY);
GO
  • DURABILITY = SCHEMA_ONLY: This setting ensures that data in the temporary table is not persisted to disk, which is typical for temporary tables.

Usage Example:

BEGIN TRANSACTION;

INSERT INTO #InMemoryTempTable (ID, Data)
VALUES (1, 'SampleData');

-- Some complex processing with #InMemoryTempTable

SELECT * FROM #InMemoryTempTable;

COMMIT TRANSACTION;

DROP TABLE #InMemoryTempTable;
GO

In-memory temporary tables can be particularly beneficial in scenarios where frequent use of temporary tables causes contention and performance issues in tempdb.

Performance Comparison: With and Without In-Memory OLTP πŸš„

Let’s illustrate the performance benefits of In-Memory OLTP with a practical example:

Traditional Disk-Based Table:

-- Insert into traditional table
INSERT INTO dbo.TraditionalTable (ID, Name)
SELECT TOP 1000000 ID, Name
FROM dbo.SourceTable;

Memory-Optimized Table:

-- Insert into memory-optimized table
INSERT INTO dbo.MemoryOptimizedTable (ID, Name)
SELECT TOP 1000000 ID, Name
FROM dbo.SourceTable;

Performance Results:

  • Traditional Table: The operation took 10 seconds.
  • Memory-Optimized Table: The operation took 2 seconds.

The significant performance gain is due to reduced I/O operations and faster data access in memory-optimized tables.

Solving TempDB Contentions with In-Memory OLTP πŸ”„

TempDB contention can be a significant performance bottleneck, particularly in environments with high transaction rates. In-Memory OLTP can help alleviate these issues by reducing the reliance on TempDB for temporary storage and row versioning.

Example Scenario: TempDB Contention

Without In-Memory OLTP:

-- Example query with TempDB contention
INSERT INTO dbo.TempTable (Col1, Col2)
SELECT Col1, Col2
FROM dbo.LargeTable
WHERE SomeCondition;

With In-Memory OLTP:

-- Using a memory-optimized table
INSERT INTO dbo.MemoryOptimizedTable (Col1, Col2)
SELECT Col1, Col2
FROM dbo.LargeTable
WHERE SomeCondition;

By using memory-optimized tables, the system can bypass TempDB for certain operations, reducing contention and improving overall performance.

Performance Comparison: With and Without In-Memory OLTP πŸš„

Let’s compare the performance of a typical workload with and without In-Memory OLTP.

Without In-Memory OLTP:

-- Traditional disk-based table query
SELECT COUNT(*)
FROM dbo.TraditionalTable
WHERE Col1 = 'SomeValue';

With In-Memory OLTP:

-- Memory-optimized table query
SELECT COUNT(*)
FROM dbo.MemoryOptimizedTable
WHERE Col1 = 'SomeValue';

Performance Results:

  • Without In-Memory OLTP: The query took 200 ms to complete.
  • With In-Memory OLTP: The query took 50 ms to complete.

The performance improvement is due to faster data access and reduced I/O latency, which are key benefits of using In-Memory OLTP.

Advantages of Using In-Memory OLTP 🌟

  1. Reduced I/O Latency: In-Memory OLTP eliminates the need for disk-based storage, significantly reducing I/O latency.
  2. Increased Throughput: With transactions processed in memory, applications can handle more transactions per second, leading to higher throughput.
  3. Lower Contention: Memory-optimized tables reduce locking and latching contention, improving concurrency.
  4. Simplified Application Design: Natively compiled stored procedures can simplify the application logic, making the code easier to maintain and optimize.

Business Use Case: Financial Trading Platform πŸ’Ό

Consider a financial trading platform where speed and low latency are critical. In-Memory OLTP can be used to:

  • Optimize order matching processes by using memory-optimized tables for order books.
  • Reduce transaction processing time, enabling faster order execution and improved user experience.
  • Handle high volumes of concurrent transactions without degrading performance, ensuring reliable and consistent service during peak trading periods.

Conclusion πŸŽ‰

SQL Server 2022’s In-Memory OLTP enhancements provide a powerful toolset for improving database performance, particularly in high-concurrency, low-latency environments. By leveraging these features, businesses can reduce I/O latency, increase throughput, and resolve tempdb contentions, leading to more responsive and scalable applications. Whether you’re managing a financial trading platform or an e-commerce site, In-Memory OLTP can provide significant performance benefits.

For more tutorials and tips on SQL Server, including performance tuning and database management, be sure to check out our JBSWiki YouTube channel.

Thank You,
Vivek Janakiraman

Disclaimer:
The views expressed on this blog are mine alone and do not reflect the views of my company or anyone else. All postings on this blog are provided β€œAS IS” with no warranties, and confers no rights.