What is a stored procedure in SQL Server? A practical introduction
What a stored procedure is, how to create and run one, what it's good for and where it hurts, with examples you can run on SQL Server.
A stored procedure is a named block of T-SQL saved in the database. You call it by name, pass it parameters, and it runs its statements on the server: queries, inserts and updates, loops, transactions, calls to other procedures. It can return rows, output parameters and a status code.
If you write .NET or Java against SQL Server, you have almost certainly called one, even if your ORM hid it. This article explains what they are, how to write and run one, and when they help.
The examples run on any SQL Server since 2016, against the SpDevDemo database.
A first stored procedure
CREATE OR ALTER PROCEDURE dbo.GetOpenOrders
@CustomerId INT
AS
BEGIN
SET NOCOUNT ON;
SELECT OrderId, OrderNumber, OrderDate, Total
FROM dbo.Orders
WHERE CustomerId = @CustomerId
AND Status = 'Open'
ORDER BY OrderDate DESC;
END
The parts:
CREATE OR ALTER PROCEDUREcreates the procedure, or replaces it if it exists (SQL Server 2016 SP1 and later; before that,CREATEthenALTER).dbo.GetOpenOrdersis its name, with its schema. Always write the schema.@CustomerId INTis a parameter. A procedure can have up to 2,100.SET NOCOUNT ONstops the “(n rows affected)” message after each statement, which clients don’t need.- The body between
BEGINandENDis ordinary T-SQL.
Run it with EXEC:
EXEC dbo.GetOpenOrders @CustomerId = 42;
It returns customer 42’s three open orders as a result set, exactly as if you had run the SELECT yourself.
What a procedure can return
A procedure has three ways to hand data back, and can use all three at once:
CREATE OR ALTER PROCEDURE dbo.CustomerSummary
@CustomerId INT,
@OrderCount INT OUTPUT,
@TotalSpent DECIMAL(14, 2) OUTPUT
AS
BEGIN
SET NOCOUNT ON;
IF NOT EXISTS (SELECT 1 FROM dbo.Customers WHERE CustomerId = @CustomerId)
RETURN 1; -- status code: not found
SELECT @OrderCount = COUNT(*), @TotalSpent = SUM(Total)
FROM dbo.Orders
WHERE CustomerId = @CustomerId;
SELECT TOP (5) OrderNumber, OrderDate, Total -- a result set
FROM dbo.Orders
WHERE CustomerId = @CustomerId
ORDER BY OrderDate DESC;
RETURN 0; -- status code: success
END
GO
DECLARE @status INT, @count INT, @spent DECIMAL(14, 2);
EXEC @status = dbo.CustomerSummary @CustomerId = 42, @OrderCount = @count OUTPUT, @TotalSpent = @spent OUTPUT;
SELECT @status AS Status, @count AS OrderCount, @spent AS TotalSpent;
- Result sets come from
SELECTstatements, any number of them. - Output parameters return single values. The caller must write
OUTPUTtoo. - The return code is an integer, by convention 0 for success. Use it for status, not for data.
There is a whole article on output parameters, return values and result sets.
Why use stored procedures
One round trip for a lot of work. A procedure can read, decide, update three tables and log the result in one call. From an application, the same logic might be a dozen round trips, each paying network latency.
A stable interface to the data. Applications call dbo.PlaceOrder; how orders are stored can change behind it. Several applications in different languages can share the same logic.
Security. You can grant EXECUTE on a procedure without granting any rights on the tables it uses. Users can then change data only in the ways the procedures allow. Because parameters are typed values, not text spliced into SQL, a procedure that doesn’t build dynamic SQL from its inputs isn’t open to SQL injection through them.
Cached plans. The plan for each statement in a procedure is compiled on first use and reused. Parameterized queries from an application get the same benefit, so this is less of an advantage than it once was, and it’s also the source of parameter sniffing.
Transactions near the data. A procedure can wrap several changes in one transaction and keep it short, instead of holding locks while an application makes several calls.
Where they hurt
Tooling. C# has debuggers, profilers, refactoring and unit test runners. T-SQL in SSMS has none of those: the T-SQL debugger was removed from SSMS after version 17.9. Debugging usually means PRINT statements. That’s the gap SpDev was built to close.
Versioning and deployment. A procedure lives in the database, not in your build. Keep every procedure in source control as a .sql file, and deploy it with your schema migrations, or the database becomes the only copy.
Business logic in two places. When some rules are in C# and some in procedures, nobody is sure where to look. Decide which kind of logic goes where, and keep to it.
Testing. Procedures can be unit tested (tSQLt is the common framework), but it takes more setup than testing application code.
Stored procedure, function or view?
All three are named T-SQL saved in the database, but they do different jobs:
| Stored procedure | Function | View | |
|---|---|---|---|
| Called with | EXEC |
Inside a query | Inside a query, like a table |
| Parameters | Yes, including output | Input only | No |
| Can change data | Yes | No | Through it, with limits |
| Transactions, TRY…CATCH, dynamic SQL | Yes | No | No |
| Can be joined to | No | Table-valued ones, yes | Yes |
The detailed comparisons: stored procedure vs function and stored procedure vs view.
Finding the procedures in a database
SELECT s.name AS schema_name, p.name AS procedure_name, p.create_date, p.modify_date
FROM sys.procedures AS p
JOIN sys.schemas AS s ON s.schema_id = p.schema_id
ORDER BY s.name, p.name;
To read one’s code, use sp_helptext 'dbo.GetOpenOrders' or SELECT OBJECT_DEFINITION(OBJECT_ID('dbo.GetOpenOrders')). To search all of them for a table or a phrase, see how to find text in stored procedures.
Next steps
- How to write a stored procedure that holds up in production: a template with error handling and transactions.
- How to execute a stored procedure in every way SQL Server allows.
- How to debug a stored procedure now that SSMS can’t.