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.

Running SQL Server 2022 on Linux: Enhancements, Best Practices, and Business Use Cases

Microsoft’s decision to bring SQL Server to Linux marked a significant milestone, opening doors for more flexible and cost-effective database management solutions. SQL Server 2022 continues to enhance this cross-platform capability, offering a robust and feature-rich environment for enterprises leveraging Linux. In this blog, we will explore the enhancements in SQL Server 2022 for Linux, best practices for optimal performance, and compelling business use cases.


🎉 Why SQL Server on Linux?

Before diving into the technical details, let’s understand the benefits of running SQL Server on Linux:

  1. Cost Savings: Linux is an open-source platform, which can significantly reduce licensing costs compared to Windows environments.
  2. Flexibility: Enterprises can choose the platform that best suits their infrastructure and expertise, leveraging existing investments in Linux.
  3. Performance: SQL Server on Linux has been optimized for performance, taking advantage of the low overhead and efficient resource management of Linux systems.
  4. Security: Linux is known for its robust security features, which complement SQL Server’s advanced security capabilities.
  5. Compatibility: SQL Server on Linux supports many of the same features and functionalities as on Windows, ensuring a consistent experience across platforms.

🚀 SQL Server 2022 Enhancements on Linux

1. Enhanced Availability and Performance

SQL Server 2022 introduces several enhancements to improve availability and performance on Linux:

High Availability and Disaster Recovery (HADR)

SQL Server 2022 on Linux now supports improved Always On Availability Groups, providing robust high availability and disaster recovery (HADR) options. This includes:

  • Synchronous and Asynchronous Data Replication: Ensure data consistency and high availability across multiple Linux servers.
  • Automatic Failover: Minimize downtime by automatically switching to a standby server in case of a failure.

Implementation

Configure Always On Availability Groups using the following commands:

sudo /opt/mssql/bin/mssql-conf set hadr.hadrenabled 1
sudo systemctl restart mssql-server

Performance Improvements

SQL Server 2022 leverages Linux’s low-latency networking and I/O capabilities, enhancing performance for intensive workloads.

2. Advanced Security Features

Security is paramount, and SQL Server 2022 on Linux offers several advanced security features:

  • Transparent Data Encryption (TDE): Encrypts data at rest, protecting it from unauthorized access.
  • Always Encrypted: Protects sensitive data by encrypting it at the client side, ensuring that the database never sees the plaintext data.

Implementation

Enable TDE using the following SQL commands:

CREATE DATABASE ENCRYPTION KEY
WITH ALGORITHM = AES_256
ENCRYPTION BY SERVER CERTIFICATE MyServerCert;
ALTER DATABASE YourDatabase
SET ENCRYPTION ON;

3. Improved Cross-Platform Management

SQL Server 2022 enhances management capabilities, allowing seamless administration across Windows and Linux platforms:

  • SQL Server Management Studio (SSMS): Use SSMS to manage SQL Server instances on Linux.
  • SQL Server Data Tools (SSDT): Develop and deploy SQL Server solutions across platforms.

🛠️ Best Practices for Running SQL Server 2022 on Linux

  1. Choose the Right Distribution

Select a supported Linux distribution, such as Red Hat Enterprise Linux (RHEL), Ubuntu, or SUSE Linux Enterprise Server (SLES), based on your organization’s requirements and support considerations.

  1. Optimize System Configuration
  • Memory and CPU Configuration: Ensure adequate memory and CPU allocation based on workload requirements.
  • Disk I/O Optimization: Use SSDs for storage to take advantage of faster data access and improved I/O performance.
  1. Security Best Practices
  • Regularly Update and Patch: Keep your SQL Server and Linux OS updated with the latest security patches.
  • Implement Strong Authentication: Use integrated authentication methods and enforce strong passwords.
  1. Monitor and Tune Performance
  • Use Performance Monitoring Tools: Leverage SQL Server tools like sys.dm_os_performance_counters and Linux tools like iostat and vmstat to monitor performance.
  • Query Optimization: Regularly review and optimize queries to ensure efficient execution.

🏢 Business Use Cases

1. Cost-Effective Database Solutions

Organizations with existing Linux infrastructure can reduce licensing costs by deploying SQL Server on Linux. This is especially beneficial for startups and small to medium-sized enterprises (SMEs) looking to optimize their budget without compromising on database capabilities.

2. High-Performance Data Analytics

SQL Server 2022 on Linux provides the performance and scalability needed for data-intensive applications, such as real-time analytics and big data processing. Companies can leverage the robust performance capabilities of Linux to handle large volumes of data efficiently.

3. Cross-Platform Development and Deployment

For organizations with a mixed OS environment, SQL Server 2022 on Linux enables consistent database management across platforms. This allows for streamlined development and deployment processes, reducing complexity and enhancing productivity.

4. Enhanced Security and Compliance

With advanced security features like TDE and Always Encrypted, SQL Server 2022 on Linux helps organizations meet stringent data security and compliance requirements, such as GDPR and HIPAA.


🏁 Conclusion

SQL Server 2022 on Linux offers a powerful, flexible, and cost-effective solution for modern enterprises. With enhancements in performance, security, and management, along with the advantages of the Linux platform, it is an excellent choice for businesses looking to leverage the best of both worlds. Whether you’re aiming to reduce costs, improve performance, or ensure robust security, SQL Server 2022 on Linux provides the tools and features necessary to achieve your goals.

If you have any questions or need further guidance, feel free to leave a comment or reach out! Happy computing! 🚀

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.