Dynamic SQL is widely used in enterprise systems. It powers advanced filtering, flexible reporting, role-based visibility, and multi-tenant logic. But when developers concatenate strings and build SQL directly with values, the result is unsafe, slow, and unpredictable.
Poorly written dynamic SQL creates SQL injection risks, high CPU usage, excessive recompiles, and plan-cache pollution. The solution is to refactor dynamic SQL using proper parameterization.
This article explains the full process end-to-end with practical patterns, code samples, performance insights, and diagrams that demonstrate how SQL Server internally behaves.
Why Dynamic SQL Becomes A Problem
Dynamic SQL is not the issue. The problem is how developers construct it.
A Common Unsafe Pattern
DECLARE @sql NVARCHAR(MAX);
SET @sql = 'SELECT * FROM Sales WHERE Region = ''' + @region + '''';
EXEC(@sql);
Problems created:
SQL injection exposure
Every query string is unique, so SQL Server creates a new execution plan
High CPU usage due to plan-cache pollution
Bad cardinality estimates
Unpredictable performance
ASCII Diagram: What Happens Inside SQL Server
+--------------+ +------------------------+
| Incoming SQL | ----> | SQL Text Normalization |
+--------------+ +------------------------+
|
v
+-----------------------------+
| Plan Cache Lookup Fails |
| (because literals differ) |
+-----------------------------+
|
v
+--------------------+
| Compile New Plan |
+--------------------+
|
v
+----------------------------+
| Execute With Wrong Estimates|
+----------------------------+
Every incoming literal generates a different plan, causing unnecessary compilation and degraded performance.
Identifying Hotspots Before Refactoring
Before refactoring, identify areas where dynamic SQL is hurting production performance.
Check Plan Cache For Literal-Based Queries
SELECT TOP 20
qs.execution_count,
qs.total_worker_time,
st.text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
ORDER BY qs.total_worker_time DESC;
Look for:
Same query shape with different literal values
Excessive compilations
Find Unsafe Concatenation
Search in your codebase for:
EXEC(
Repeated string concatenation
Literal values injected into SQL
Detect Plan Cache Bloat
SELECT
objtype,
COUNT(*) AS totalPlans
FROM sys.dm_exec_cached_plans
GROUP BY objtype;
Too many adhoc plans indicate dynamic SQL without parameters.
Migrate To sp_executesql Properly
The most important step is replacing EXEC() with sp_executesql.
Unsafe Dynamic SQL
DECLARE @sql NVARCHAR(MAX);
SET @sql = 'SELECT * FROM Orders WHERE CustomerId = ' + CAST(@customerId AS NVARCHAR(10));
EXEC(@sql);
Parameterized Dynamic SQL (Safe And Fast)
DECLARE @sql NVARCHAR(MAX);
SET @sql = '
SELECT *
FROM Orders
WHERE CustomerId = @cid
';
EXEC sp_executesql
@sql,
N'@cid INT',
@cid = @customerId;
Advantages
Plan reuse improves
CPU cost reduces
SQL injection removed
Better cardinality estimates
Building Dynamic WHERE Clause Cleanly
Enterprise applications often require optional filters.
Dynamic WHERE Clause Pattern
DECLARE
@region NVARCHAR(50) = NULL,
@status NVARCHAR(20) = 'Active';
DECLARE @sql NVARCHAR(MAX) = N'SELECT * FROM Customers WHERE 1=1';
DECLARE @params NVARCHAR(MAX) = N'';
IF @region IS NOT NULL
BEGIN
SET @sql += N' AND Region = @pRegion';
SET @params += N'@pRegion NVARCHAR(50),';
END
IF @status IS NOT NULL
BEGIN
SET @sql += N' AND Status = @pStatus';
SET @params += N'@pStatus NVARCHAR(20),';
END
SET @params = LEFT(@params, LEN(@params)-1);
EXEC sp_executesql @sql, @params,
@pRegion = @region,
@pStatus = @status;
Benefits
Clean code
Safe execution
Maximum plan reuse
Using Table-Driven Parameter Sets
For large filtering scenarios, manage parameters using a table.
DECLARE @Filter TABLE(
FilterName NVARCHAR(100),
SqlCondition NVARCHAR(200),
ParamName NVARCHAR(50),
ParamType NVARCHAR(50),
ParamValue SQL_VARIANT
);
Populate the table and build SQL dynamically. Useful in reporting and BI systems.

Join the conversation! Your thoughts help the community grow.