Dynamic SQL Result Into a Variable with sp_executesql

To get a dynamic SQL result into a variable, declare an OUTPUT parameter and run the statement with sp_executesql. The dynamic statement assigns the value to that parameter, and the caller reads it afterward. A plain EXEC can’t do this.

Gouache painting of a slate blue jug on a small wooden pouring stand pouring glaze into a vermilion jar

Why Plain EXEC Falls Short

A dynamic statement runs in its own scope. It can’t see the variables of the batch that built it, and a variable it sets disappears when it ends. A SELECT inside it returns rows to the client, not to a variable. The system procedure sp_executesql solves both problems, because it accepts parameters in both directions.

Build the Demo Data

The demo database has three small tables. Departments holds five departments. Orders holds three orders, two of them for Marketing, and its key is a plain number. Tickets starts empty and appears near the end.

IF DB_ID(N'DynamicResultDemo') IS NULL CREATE DATABASE DynamicResultDemo;
GO
USE DynamicResultDemo;
GO
DROP TABLE IF EXISTS dbo.Orders, dbo.Departments, dbo.Tickets;
CREATE TABLE dbo.Departments (DepartmentID int NOT NULL PRIMARY KEY, DepartmentName nvarchar(50) NOT NULL);
INSERT INTO dbo.Departments (DepartmentID, DepartmentName)
VALUES (1, N'Sales'), (2, N'Support'), (3, N'Billing'), (4, N'Marketing'), (5, N'Shipping');
CREATE TABLE dbo.Orders (
    OrderID      int NOT NULL PRIMARY KEY,
    DepartmentID int NOT NULL,
    Total        decimal(10,2) NOT NULL
);
INSERT INTO dbo.Orders (OrderID, DepartmentID, Total) VALUES (1, 4, 25.00), (2, 4, 40.50), (3, 1, 99.99);
CREATE TABLE dbo.Tickets (TicketID int IDENTITY(1,1) NOT NULL PRIMARY KEY, Title nvarchar(60) NOT NULL);

Return One Value

First, the plain case. The statement is a string, and the value 4 travels in as a parameter. The result comes back as a row to the client.

DECLARE @sql nvarchar(400) = N'SELECT DepartmentName FROM dbo.Departments WHERE DepartmentID = @ID;';
EXEC sys.sp_executesql @sql, N'@ID int', @ID = 4;
DepartmentName
Marketing

Now capture the dynamic SQL result in a variable. Add a second parameter to the list, mark it OUTPUT, and assign to it inside the statement. The caller passes a variable for it, and the keyword OUTPUT appears again in the call.

DECLARE @sql nvarchar(400) = N'SELECT @Name = DepartmentName FROM dbo.Departments WHERE DepartmentID = @ID;';
DECLARE @Name nvarchar(50);
EXEC sys.sp_executesql @sql, N'@ID int, @Name nvarchar(50) OUTPUT', @ID = 4, @Name = @Name OUTPUT;
SELECT @Name AS ReturnedName;
ReturnedName
Marketing

The three pieces must agree. The statement assigns @Name. The parameter list declares @Name nvarchar(50) OUTPUT. The call passes @Name = @Name OUTPUT. Leave OUTPUT off the call, and the caller’s variable stays NULL.

The Statement Must Be nvarchar

The most common error comes from the type of the statement variable. The procedure wants nvarchar. A varchar variable fails right away, and the message points at the first parameter.

DECLARE @sql varchar(400) = 'SELECT 1;';
EXEC sys.sp_executesql @sql;

SSMS window with the two line script that passes a varchar variable to sys.sp_executesql and a Messages tab showing Msg 214, Level 16, State 2, Line 1 [Batch Start Line 0], Procedure expects parameter '@statement' of type 'ntext/nchar/nvarchar'

Msg 214, Level 16, State 2, Procedure sys.sp_executesql, Line 1
Procedure expects parameter '@statement' of type 'ntext/nchar/nvarchar'.

SSMS 22 adds [Batch Start Line 0] after Line 1, as the picture shows. Declare the variable as nvarchar, and prefix literals with N. The same rule applies to the parameter definition string. The type of the OUTPUT variable has no such rule. An int, a decimal or a date works.

Counts and Other Types

Any type works as an output. A count is typical. Marketing has two orders.

DECLARE @sql nvarchar(400) = N'SELECT @n = COUNT(*) FROM dbo.Orders WHERE DepartmentID = @ID;';
DECLARE @n int;
EXEC sys.sp_executesql @sql, N'@ID int, @n int OUTPUT', @ID = 4, @n = @n OUTPUT;
SELECT @n AS OrderCount;
OrderCount
2

Only One Row Fits

A variable holds one value. When the SELECT reads several rows, each assignment overwrites the last one. The next statement has no WHERE clause.

DECLARE @sql nvarchar(400) = N'SELECT @Name = DepartmentName FROM dbo.Departments;';
DECLARE @Name nvarchar(50);
EXEC sys.sp_executesql @sql, N'@Name nvarchar(50) OUTPUT', @Name = @Name OUTPUT;
SELECT @Name AS LastRowName;
LastRowName
Shipping

The variable ends with the value of the last row read, and without ORDER BY the row order isn’t guaranteed. Filter to one row, use TOP (1) with ORDER BY, or aggregate. An assignment from many rows keeps only the last value, and the order is not defined.

Table and Column Names

A parameter carries a value, never a name. You can’t pass a table name as a parameter, so a generic lookup builds the name into the string. QUOTENAME wraps the name in brackets and doubles any closing bracket inside it. That makes the name safe to concatenate. Values still travel as parameters, never inside the string. A value pasted into the string, such as a typed name, lets the user change the statement itself. That is how SQL injection works, and parameters close the door.

DECLARE @table sysname = N'Orders';
DECLARE @sql nvarchar(400) = N'SELECT @n = COUNT(*) FROM dbo.' + QUOTENAME(@table) + N';';
DECLARE @n int;
EXEC sys.sp_executesql @sql, N'@n int OUTPUT', @n = @n OUTPUT;
SELECT @n AS RowsInTable;
RowsInTable
3

When the name comes from user input, check it against the catalog first. OBJECT_ID returns NULL for a name that doesn’t exist, and the procedure can stop there.

JSON and XML Results

A statement can return JSON or XML as one value. Declare the output as nvarchar(max) for JSON, or xml for XML. The inner SELECT uses FOR JSON PATH here.

DECLARE @sql nvarchar(400) = N'SELECT @j = (SELECT DepartmentID, DepartmentName FROM dbo.Departments WHERE DepartmentID <= 2 FOR JSON PATH);';
DECLARE @j nvarchar(max);
EXEC sys.sp_executesql @sql, N'@j nvarchar(max) OUTPUT', @j = @j OUTPUT;
SELECT @j AS DepartmentsJson;
DepartmentsJson
[{“DepartmentID”:1,”DepartmentName”:”Sales”},{“DepartmentID”:2,”DepartmentName”:”Support”}]

New Identity Values

A dynamic INSERT runs in its own scope. SCOPE_IDENTITY() outside it can’t see the new row. In this demo it returns NULL, because no other insert with an identity column has run in the session. In a longer session, it returns the value of an older insert, which is worse. The @@IDENTITY function sees the row. It also counts rows that a trigger inserts, so it can return the wrong value. Read SCOPE_IDENTITY() inside the dynamic statement and return it through an OUTPUT parameter.

DECLARE @sql nvarchar(400) = N'INSERT INTO dbo.Tickets (Title) VALUES (N''Printer is down''); SELECT @id = SCOPE_IDENTITY();';
DECLARE @id int;
EXEC sys.sp_executesql @sql, N'@id int OUTPUT', @id = @id OUTPUT;
SELECT @id AS NewTicketViaOutput, SCOPE_IDENTITY() AS ScopeIdentityOutside, @@IDENTITY AS AtAtIdentity;
NewTicketViaOutputScopeIdentityOutsideAtAtIdentity
1NULL1

When You Need the Rows

Sometimes the result is a set of rows, not one value. Create a temporary table that matches the columns, and let INSERT take the output of sp_executesql.

DECLARE @sql nvarchar(400) = N'SELECT OrderID, Total FROM dbo.Orders WHERE DepartmentID = @ID ORDER BY OrderID;';
DROP TABLE IF EXISTS #Found;
CREATE TABLE #Found (OrderID int NOT NULL, Total decimal(10,2) NOT NULL);
INSERT INTO #Found (OrderID, Total) EXEC sys.sp_executesql @sql, N'@ID int', @ID = 4;
SELECT OrderID, Total FROM #Found;
OrderIDTotal
125.00
240.50

You could argue that dynamic SQL is a last resort. When only a value changes, a static query with a parameter is simpler and safer. The optimizer reuses its plan. Reach for dynamic SQL when a name or the shape of the statement changes.

What to Remember

To return a dynamic SQL result, declare the statement as nvarchar and mark the parameter OUTPUT in both places. Assign the value inside the statement. Return one row, or aggregate. Wrap names in QUOTENAME and pass values as parameters.

When you finish with the demo, run the cleanup script.

USE master;
GO
IF DB_ID(N'DynamicResultDemo') IS NOT NULL
BEGIN
    ALTER DATABASE DynamicResultDemo SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
    DROP DATABASE DynamicResultDemo;
END;

A dynamic SQL result is not returned, it is handed back through a parameter.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.


Discover more from SQL Authority with Pinal Dave

Subscribe to get the latest posts sent to your email.

Dynamic SQL, SQL Scripts, SQL Stored Procedure, SQL Variable
Previous Post
SQL SERVER – Get List of the Logical and Physical Name of the Files in the Entire Database
Next Post
Schema Changes History Report: See Who Dropped a Table

Related Posts

10 Comments. Leave new

  • Kalman Kernerman
    December 21, 2018 11:16 pm

    Very cool tip !!!

    Reply
  • I am glad you liked it.

    Reply
  • I use dynamic SQL a lot, but I do it slightly different. The parameter @ID isn’t passed to the SP, it is built into the @sqlCommand as text and then executed.

    DECLARE @sqlCommand NVARCHAR(4000)
    DECLARE @ID INT
    DECLARE @RESULT nvarchar (255)

    SET @ID = 4
    SET @sqlCommand = ‘SELECT [Name] as ReturnedName
    FROM [AdventureWorks2014].[HumanResources].[Department]
    WHERE ID = ‘ + cast (@ID as nvarchar (10))

    exec sp_executesql @sqlCommand, N’@RESULT nvarchar (255) out’, @RESULT out

    or (I prefer the table variable because of simpler and more readable syntax)

    DECLARE @sqlCommand NVARCHAR(4000)
    DECLARE @ID INT
    DECLARE @RESULT Table (RESULT nvarchar (255))

    SET @ID = 4
    SET @sqlCommand = ‘SELECT [Name] as ReturnedName
    FROM ViewAdressen
    WHERE ID = ‘ + cast (@ID as nvarchar (10))

    insert @RESULT exec sp_executesql @sqlCommand

    select Result as ReturnedName from @RESULT

    Reply
  • Thank you!!!! It helped me

    Reply
  • Thank you! This one was very helpfull to me.

    Reply
  • Alireza Nikoughadamazad
    November 17, 2020 9:37 am

    Perfect. Thanks for such a valuable tutorial.

    Reply
  • Thank you so much for this.
    In my case I needed a dynamic DLOOKUP type feature as follows: (I am an amateur so please feel free to correct)

    — This procedure will return a single value form any specified field in any specified table using criteria in any specified field or a debug message.
    Input parameters are TB – table name, FN – lookup field, SF – search field and IV – search criteria.
    — =============================================
    CREATE PROCEDURE [dbo].[stpGetOneValue]
    — Add the parameters for the stored procedure here
    @TB nvarchar(50),
    @FN nvarchar(50),
    @SF nvarchar(50),
    @IV nvarchar(50),
    @OutStr varchar(50) output
    AS
    BEGIN
    — SET NOCOUNT ON added to prevent extra result sets from
    — interfering with SELECT statements.
    SET NOCOUNT ON;

    — Insert statements for procedure here
    DECLARE @MyStatement nvarchar(MAX)
    DECLARE @Out nvarchar(50)
    DECLARE @COut nvarchar(50)
    DECLARE @InputValue nvarchar(50)
    DECLARE @FieldName nvarchar(50)
    DECLARE @Table nvarchar(50)
    DECLARE @SearchField nvarchar(50)
    DECLARE @MyCount int = 0

    SET @Table = @TB
    SET @FieldName = @FN
    SET @InputValue = @IV
    SET @SearchField = @SF

    SET @MyStatement = ‘SELECT @FieldVal = ‘ + @FieldName + ‘ FROM ‘ + @Table + ‘ WHERE ‘ + @SearchField + ‘ = ”’ + @InputValue + ””
    EXEC sp_executesql @MyStatement, N’@FieldName nvarchar(50), @InputValue nvarchar(50), @Table nvarchar(50), @SearchField nvarchar(50), @FieldVal nvarchar(50) OUTPUT’,
    @InputValue = @InputValue, @FieldName = @FieldName, @Table = @Table, @SearchField= @SearchField, @FieldVal = @Out OUTPUT
    SELECT @OutStr = @Out
    — Trash reslut if more or less than a single value is returned (this is NOT a recordset procedure)
    SET @MyStatement = ‘SELECT @MyCount = COUNT (@FieldName) FROM ‘ + @Table + ‘ GROUP BY ‘ + @SearchField + ‘ HAVING ‘ + @SearchField + ‘ = ”’ + @InputValue + ””
    EXEC sp_executesql @MyStatement, N’@FieldName nvarchar(50), @InputValue nvarchar(50), @Table nvarchar(50), @SearchField nvarchar(50), @MyCount int OUTPUT’,
    @InputValue = @InputValue, @FieldName = @FieldName, @Table = @Table, @SearchField= @SearchField, @MyCount = @COut OUTPUT
    if @COut 1
    SELECT @OutStr = ‘Stored procudure did not return a unique record’
    END

    Reply
  • Thanks for the posting, I did try exact same things except the return output variable is type INT, then I got below error:

    Msg 214, Level 16, State 2, Procedure sp_executesql, Line 1 [Batch Start Line 0]
    Procedure expects parameter ‘@statement’ of type ‘ntext/nchar/nvarchar’.

    Can i know why ? Thank you.

    Reply
  • hi, i send datas to this stored procedure by using API,i have a problem about Dynamic tables, i could not get id of my recorded data as output.pls help me to solve this problem
    ALTER PROC Experience
    @Subject1 INT
    ,@Subject2 INT
    ,@TableNumber INT
    ,@IDR INT OUTPUT
    AS
    BEGIN
    DECLARE @CMDS VARCHAR(MAX);
    DECLARE @Subj1 VARCHAR(10);
    SET @Subj1=CONVERT(varchar(10),@Subject1);
    DECLARE @Subj2 VARCHAR(10);
    SET @Subj2=CONVERT(varchar(10),@Subject2);
    DECLARE @TableNames VARCHAR(100);
    SET @TableNames=’TableNo_’+CONVERT(varchar(50),@TableNumber);
    SET @CMDS=’INSERT INTO ‘+@TableNames+’ (@Subject1,@Subject1) VALUES(”’+@Subj1+”’,”’+@Subj2+”’);’
    EXEC (@CMDS);
    SELECT @IDR = SCOPE_IDENTITY();
    END

    Reply
  • how about , if the query returning a json / xml how to catch result xml / json in to nvarchar(max) variable?

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.