Dynamic SQL Done Safely: sp_executesql, Parameters and QUOTENAME

Dynamic SQL, building a query as a string and executing it, is sometimes the right tool: flexible search screens, choosing a sort column at run time, or maintenance scripts that loop over tables. It is also the classic route to SQL injection. The rules for doing it safely are short and strict.

Safe dynamic SQL in SQL Server with sp_executesql

The dangerous way

-- DO NOT DO THIS
DECLARE @sql NVARCHAR(MAX) =
    N'SELECT * FROM dbo.Employee WHERE FullName = ''' + @Name + N'''';
EXEC (@sql);

If @Name contains x'; DROP TABLE dbo.Employee; --, it becomes part of the SQL and runs. Concatenating values also produces a different query text for every value, so SQL Server compiles a new plan each time and fills the plan cache.

Rule 1: pass values as parameters with sp_executesql

DECLARE @sql NVARCHAR(MAX) =
    N'SELECT EmployeeId, FullName, DepartmentId
      FROM dbo.Employee
      WHERE FullName = @Name AND DepartmentId = @DeptId;';

EXEC sys.sp_executesql
    @sql,
    N'@Name NVARCHAR(100), @DeptId INT',
    @Name = @Name, @DeptId = @DeptId;

The values never become part of the SQL text, so they cannot change its meaning, and the query text stays the same, so its plan is reused.

Rule 2: object names go through QUOTENAME and a whitelist

Table and column names cannot be parameters. When they must vary, validate them against a list you control, then wrap them with QUOTENAME:

DECLARE @SortColumn SYSNAME = N'HireDate',
        @SortDir    NVARCHAR(4) = N'DESC';

IF @SortColumn NOT IN (N'FullName', N'HireDate', N'DepartmentId')
    THROW 50010, 'Invalid sort column.', 1;
IF @SortDir NOT IN (N'ASC', N'DESC')
    THROW 50011, 'Invalid sort direction.', 1;

DECLARE @sql NVARCHAR(MAX) =
    N'SELECT EmployeeId, FullName, HireDate, DepartmentId
      FROM dbo.Employee
      ORDER BY ' + QUOTENAME(@SortColumn) + N' ' + @SortDir + N';';

EXEC sys.sp_executesql @sql;

QUOTENAME adds square brackets and escapes any closing bracket inside the name, so even an odd name cannot break out of the identifier.

Building optional search filters

A search screen with many optional filters is the best use of dynamic SQL. Add only the conditions the user supplied, always with parameters:

DECLARE @sql NVARCHAR(MAX) = N'SELECT EmployeeId, FullName, DepartmentId, HireDate
FROM dbo.Employee
WHERE 1 = 1';

IF @Name   IS NOT NULL SET @sql += N' AND FullName LIKE @Name + N''%''';
IF @DeptId IS NOT NULL SET @sql += N' AND DepartmentId = @DeptId';
IF @From   IS NOT NULL SET @sql += N' AND HireDate >= @From';

EXEC sys.sp_executesql @sql,
    N'@Name NVARCHAR(100), @DeptId INT, @From DATE',
    @Name = @Name, @DeptId = @DeptId, @From = @From;

Each combination of filters gets its own cached plan tailored to the columns actually used, which often beats a single "catch-all" query full of (@x IS NULL OR Column = @x) conditions.

Permissions

Code inside dynamic SQL runs with the permissions of the caller, not the owner of the stored procedure. A user allowed to execute a procedure may still get "permission denied" on the tables it queries dynamically. Grant the needed permissions explicitly, or sign the procedure with a certificate, rather than giving broad rights.

Debugging tips

  • Use NVARCHAR(MAX) and N'' literals to avoid silent truncation and Unicode issues.
  • PRINT @sql; (or SELECT @sql for long strings) before executing, to see exactly what will run.
  • Test with values containing quotes, brackets and semicolons.

Quick checklist

  • Values: always parameters via sp_executesql, never concatenated.
  • Names: whitelist plus QUOTENAME.
  • Use dynamic SQL for optional filters and sorting, not by default.
  • Check permissions and test with hostile input.

Related reading:

Comments

Popular posts from this blog

How to Read a SQL Server Execution Plan (Beginner's Guide)

SARGable Queries in SQL Server: Why Functions in WHERE Slow You Down

Azure SQL Database vs SQL Server on Azure VM: Cost Comparison (2026)