How to execute a stored procedure in SQL Server: every way, with examples
EXEC with named and positional parameters, defaults, OUTPUT parameters, return codes, INSERT...EXEC, WITH RESULT SETS, calling one procedure from another, and the errors you'll meet.
Running a stored procedure is one line: EXEC dbo.ProcedureName. Passing parameters, getting values back and capturing what it returns have a few more rules, and a few error messages you’ll meet sooner or later. This article covers them all, with examples you can run against the SpDevDemo database.
Two procedures to call:
CREATE OR ALTER PROCEDURE dbo.GetOrders
@CustomerId INT,
@Status VARCHAR(20) = NULL, -- optional: NULL means any status
@Top INT = 5
AS
BEGIN
SET NOCOUNT ON;
SELECT TOP (@Top) OrderId, OrderNumber, Status, Total
FROM dbo.Orders
WHERE CustomerId = @CustomerId
AND (@Status IS NULL OR Status = @Status)
ORDER BY OrderDate DESC;
END
GO
CREATE OR ALTER PROCEDURE dbo.CountOrders
@CustomerId INT,
@Count INT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SELECT @Count = COUNT(*) FROM dbo.Orders WHERE CustomerId = @CustomerId;
RETURN CASE WHEN @Count = 0 THEN 1 ELSE 0 END;
END
EXEC with named parameters
EXEC dbo.GetOrders @CustomerId = 42, @Status = 'Open';
Name the parameters. The call still works when someone adds a parameter or changes their order, and it says what each value is. EXEC and EXECUTE are the same keyword.
Positional parameters
EXEC dbo.GetOrders 42, 'Open', 3;
Values are matched to parameters in declaration order. You can start positionally and switch to names, but not the other way round:
EXEC dbo.GetOrders @CustomerId = 42, 'Open';
Msg 119, Level 15, State 1
Must pass parameter number 2 and subsequent parameters as '@name = value'.
After the form '@name = value' has been used, all subsequent parameters must be passed in the form '@name = value'.
Default values
A parameter with a default in the declaration (@Top INT = 5) can be left out. To skip one in the middle of a positional call, write DEFAULT:
EXEC dbo.GetOrders 42, DEFAULT, 2; -- any status, top 2
A parameter without a default must be passed. Leave out @CustomerId and you get Procedure or function ‘GetOrders’ expects parameter ‘@CustomerId’, which was not supplied.
OUTPUT parameters and the return code
DECLARE @n INT, @rc INT;
EXEC @rc = dbo.CountOrders @CustomerId = 42, @Count = @n OUTPUT;
SELECT @rc AS ReturnCode, @n AS Orders;
ReturnCode Orders
---------- ------
0 9
Two rules people trip over:
- The caller must write
OUTPUT(orOUT) too. Without it, the call succeeds but your variable isn’t changed. EXEC @rc = ...captures the return code. A procedure without aRETURNstatement returns 0.
Capture a result set: INSERT…EXEC
To work with the rows a procedure returns, insert them into a table whose columns match:
CREATE TABLE #orders (OrderId INT, OrderNumber VARCHAR(20), Status VARCHAR(20), Total DECIMAL(12, 2));
INSERT INTO #orders (OrderId, OrderNumber, Status, Total)
EXEC dbo.GetOrders @CustomerId = 1, @Top = 100;
SELECT COUNT(*) AS Captured, SUM(Total) AS Total FROM #orders;
INSERT...EXEC has limits worth knowing before you build on it:
- The procedure must return exactly one shape of result. Several result sets with the same columns are all inserted; different shapes fail.
- It can’t be nested. If the procedure you call uses
INSERT...EXECitself, you get Msg 8164: An INSERT EXEC statement cannot be nested. - The insert and the procedure run in one transaction. A
ROLLBACKinside the procedure fails the whole thing.
When you control the procedure, an output parameter, a temp table the procedure fills, or an inline table-valued function is usually a better way to pass rows around.
Rename or check the result: WITH RESULT SETS
WITH RESULT SETS (SQL Server 2012 and later) declares the shape you expect. It renames and converts the columns, and fails clearly if the procedure returns something else:
EXEC dbo.GetOrders @CustomerId = 42, @Top = 2
WITH RESULT SETS ((
Id INT,
Number VARCHAR(20),
State VARCHAR(20),
Amount DECIMAL(12, 2)
));
It’s also how you give a stable contract to callers that read columns by name, while the procedure’s internals change.
Execute a procedure from another procedure
Inside a procedure, call another one the same way:
CREATE OR ALTER PROCEDURE dbo.CustomerReport @CustomerId INT
AS
BEGIN
SET NOCOUNT ON;
DECLARE @n INT;
EXEC dbo.CountOrders @CustomerId = @CustomerId, @Count = @n OUTPUT;
IF @n = 0 RETURN;
EXEC dbo.GetOrders @CustomerId = @CustomerId, @Top = 3;
END
GO
EXEC dbo.CustomerReport @CustomerId = 42;
The result sets of the inner procedure go straight to the client, as part of the outer call. Procedures can nest 32 levels deep. Temp tables created by the caller are visible to the procedures it calls, which is a common way to pass a set of rows down. (A temp table created inside the called procedure disappears when it returns.)
Procedures in another database or server
EXEC OtherDb.dbo.GetOrders @CustomerId = 42; -- another database, same server
EXEC [LINKEDSRV].SalesDb.dbo.GetOrders @CustomerId = 42; -- through a linked server
For a linked server, the server must allow RPC (EXEC sp_serveroption 'LINKEDSRV', 'rpc out', 'true').
A procedure whose name is in a variable
EXEC accepts a variable holding the name:
DECLARE @proc SYSNAME = N'dbo.GetOrders';
EXEC @proc @CustomerId = 42, @Top = 1;
Only use this with names from a fixed list you control, never with text from a user. For dynamic SQL in general, use sp_executesql with parameters, not string concatenation.
From SSMS and VS Code
- SSMS: right-click the procedure in Object Explorer, choose Execute Stored Procedure…, fill in the values, and SSMS writes and runs the
EXECfor you. - VS Code: with the MSSQL extension, run an
EXECstatement from any query editor. With SpDev, open the procedure’s.sqlfile and click Run above it. SpDev asks for each parameter, remembers your values for next time, and runs your local file, so you can test a change before deploying it. Debug does the same and stops on the first line.
From .NET
using var command = new SqlCommand("dbo.CountOrders", connection) { CommandType = CommandType.StoredProcedure };
command.Parameters.Add("@CustomerId", SqlDbType.Int).Value = 42;
var count = command.Parameters.Add("@Count", SqlDbType.Int);
count.Direction = ParameterDirection.Output;
await command.ExecuteNonQueryAsync();
ADO.NET, Dapper and EF Core each have their own way to pass output parameters and read return codes; the C# article covers all three.