Last modified: May 11, 2025
This article is written in: 🇺🇸
In the realm of relational databases, stored procedures and functions are powerful tools that allow developers to encapsulate reusable pieces of SQL code. They enhance performance by caching execution plans, promote code reusability, and keep business logic close to the data. By understanding how to create and use stored procedures and functions, you can write more efficient and maintainable database applications.
A stored procedure is a pre-compiled collection of one or more SQL statements (plus optional control-of-flow logic) saved in the database under a single name. Procedures can
They shine when you need to run the same multi-statement operation repeatedly or enforce consistent business rules.
Benefit | Why it matters |
Performance | Execution plan is compiled once and reused, reducing parsing/optimization overhead. |
Reusability | One definition can be called from many places (apps, jobs, other procs). |
Security | GRANT rights on the procedure even if callers have no direct rights on the tables it touches. |
Maintainability | Fix or extend business logic in one spot without redeploying application code. |
To make our examples concrete, we’ll first create a simple Customers
table and insert a couple of rows. This seed data will serve as the foundation for demonstrating stored procedure operations.
-- Customers table used in the examples
CREATE TABLE dbo.Customers
(
CustomerID INT IDENTITY(1,1) PRIMARY KEY,
FirstName VARCHAR(50) NOT NULL,
LastName VARCHAR(50) NOT NULL,
Email VARCHAR(100) UNIQUE,
Phone VARCHAR(20) NULL
);
-- A couple of starter rows
INSERT INTO dbo.Customers (FirstName, LastName, Email, Phone) VALUES
('Alice', 'Smith', 'alice.smith@example.com', '555-0100'),
('Bob', 'Brown', 'bob.brown@example.com', '555-0110');
Current contents before we add anything else:
CustomerID | FirstName | LastName | Phone | |
1 | Alice | Smith | alice.smith@example.com | 555-0100 |
2 | Bob | Brown | bob.brown@example.com | 555-0110 |
Creating a stored procedure involves specifying its name, parameters, and the logic to execute. The example below defines a procedure to insert a new customer and return its generated primary key.
CREATE PROCEDURE dbo.AddCustomer
@FirstName VARCHAR(50),
@LastName VARCHAR(50),
@Email VARCHAR(100),
@CustomerID INT OUTPUT -- Returns the newly created PK
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO dbo.Customers (FirstName, LastName, Email)
VALUES (@FirstName, @LastName, @Email);
-- Return the identity generated in THIS scope
SET @CustomerID = SCOPE_IDENTITY();
END;
SET NOCOUNT ON;
keeps the “(1 row affected)” message from interfering with client libraries expecting a clean result set.SCOPE_IDENTITY()
is safer than @@IDENTITY
because it ignores inserts done by triggers further down the chain.Once created, you invoke a stored procedure using EXEC
(or EXECUTE
). You can pass input values and capture any output parameters. The example below adds “John Doe” to our Customers
table and retrieves the new ID.
DECLARE @NewID INT;
EXEC dbo.AddCustomer
@FirstName = 'John',
@LastName = 'Doe',
@Email = 'john.doe@example.com',
@CustomerID = @NewID OUTPUT;
SELECT @NewID AS NewCustomerID;
Sample output:
NewCustomerID |
3 |
Table contents afterwards:
CustomerID | FirstName | LastName | Phone | |
1 | Alice | Smith | alice.smith@example.com | 555-0100 |
2 | Bob | Brown | bob.brown@example.com | 555-0110 |
3 | John | Doe | john.doe@example.com | NULL |
As requirements evolve, you can alter an existing procedure to accept new parameters or change logic without dropping and recreating it. Here we add a Phone
parameter so the procedure can store customer phone numbers as well.
ALTER PROCEDURE dbo.AddCustomer
@FirstName VARCHAR(50),
@LastName VARCHAR(50),
@Email VARCHAR(100),
@Phone VARCHAR(20), -- NEW parameter
@CustomerID INT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO dbo.Customers (FirstName, LastName, Email, Phone)
VALUES (@FirstName, @LastName, @Email, @Phone);
SET @CustomerID = SCOPE_IDENTITY();
END;
New call using the updated procedure:
DECLARE @NewID INT;
EXEC dbo.AddCustomer
@FirstName = 'Carla',
@LastName = 'Mendez',
@Email = 'carla.mendez@example.com',
@Phone = '555-0122',
@CustomerID = @NewID OUTPUT;
SELECT @NewID AS NewCustomerID;
NewCustomerID |
4 |
CustomerID | FirstName | LastName | Phone | |
… | … | … | … | … |
4 | Carla | Mendez | carla.mendez@example.com | 555-0122 |
If a stored procedure is no longer needed or must be replaced entirely, you can drop it from the database. Be cautious—dependent code will break until it’s recreated.
DROP PROCEDURE dbo.AddCustomer;
Irreversible: once dropped, dependent code will fail until the procedure is recreated.
Functions in SQL Server let you encapsulate reusable logic that computes and returns a value or a table. Unlike stored procedures, functions can be embedded directly within queries—such as in SELECT
, WHERE
, or JOIN
clauses—making them highly composable. They’re ideal for encapsulating calculations, formatting routines, or filtering logic that you want to reuse across multiple queries or views.
A function encapsulates logic that returns exactly one scalar value or a table. Unlike procedures, functions can be used inline in SELECT
, WHERE
, or JOIN
clauses. They cannot use side-effects such as INSERT
/UPDATE
(with the exception of special CLR or system functions).
SQL Server supports several function types, each suited to different scenarios. Scalar functions return a single value, while table-valued functions return result sets that you can query as if they were regular tables or views. Multi-statement TVFs allow more complex row-by-row processing but can incur more overhead.
Type | Returns | Typical use-case |
Scalar | Single value | Calculations, formatting, lookups |
Inline table-valued (iTVF) | Table defined by one SELECT |
Reusable filtered views |
Multi-statement TVF | Table assembled through multiple steps | Complex row-by-row transformations |
Functions promote modularity and code reuse by abstracting complex computations behind a simple call. Inline TVFs, in particular, can yield performance benefits because the optimizer can expand them directly into the calling query.
JOIN
, or view.To illustrate functions, we’ll create a basic Orders
table linked to our Customers
table and populate it with sample orders. This provides a dataset for demonstrating both scalar and table-valued functions.
-- Orders table used in the examples
CREATE TABLE dbo.Orders
(
OrderID INT IDENTITY(1,1) PRIMARY KEY,
CustomerID INT NOT NULL FOREIGN KEY REFERENCES dbo.Customers(CustomerID),
OrderDate DATE NOT NULL,
Amount DECIMAL(10,2) NOT NULL
);
INSERT INTO dbo.Orders (CustomerID, OrderDate, Amount) VALUES
(1, '2025-05-01', 120.00),
(1, '2025-05-03', 60.50),
(3, '2025-05-04', 210.75);
OrderID | CustomerID | OrderDate | Amount |
1 | 1 | 2025-05-01 | 120.00 |
2 | 1 | 2025-05-03 | 60.50 |
3 | 3 | 2025-05-04 | 210.75 |
Scalar functions let you encapsulate formulae or lookups that return a single value. In the example below, we calculate sales tax based on an amount and tax rate.
CREATE FUNCTION dbo.CalculateTax
(
@Amount DECIMAL(10,2),
@TaxRate DECIMAL(5,2) -- e.g. 8.25 means 8.25 %
)
RETURNS DECIMAL(10,2)
AS
BEGIN
RETURN @Amount * (@TaxRate / 100);
END;
Usage – compute sales tax in a query:
SELECT
OrderID,
Amount,
dbo.CalculateTax(Amount, 8.25) AS TaxAmount
FROM dbo.Orders;
OrderID | Amount | TaxAmount |
1 | 120.00 | 9.90 |
2 | 60.50 | 4.99 |
3 | 210.75 | 17.38 |
Inline TVFs are essentially parameterized views. You define a single SELECT
that returns a result set. The optimizer can integrate this directly into your queries for efficient execution.
CREATE FUNCTION dbo.GetCustomerOrders (@CustomerID INT)
RETURNS TABLE
AS
RETURN
(
SELECT OrderID, OrderDate, Amount
FROM dbo.Orders
WHERE CustomerID = @CustomerID
);
Querying orders for customer #1:
SELECT *
FROM dbo.GetCustomerOrders(1);
OrderID | OrderDate | Amount |
1 | 2025-05-01 | 120.00 |
2 | 2025-05-03 | 60.50 |
When you need to extend or tweak logic, you can alter an existing function. For example, adding a discount parameter to our tax calculation function allows for more flexible scenarios.
ALTER FUNCTION dbo.CalculateTax
(
@Amount DECIMAL(10,2),
@TaxRate DECIMAL(5,2),
@Discount DECIMAL(10,2) = 0 -- default 0
)
RETURNS DECIMAL(10,2)
AS
BEGIN
RETURN (@Amount - @Discount) * (@TaxRate / 100);
END;
Test the new logic:
SELECT dbo.CalculateTax(100.00, 8.25, 10.00) AS TaxOnDiscounted100;
TaxOnDiscounted100 |
7.43 |
If a function is obsolete or must be replaced entirely, you can remove it with DROP
. As with stored procedures, any dependent code will break until the function is recreated.
DROP FUNCTION dbo.CalculateTax;
SELECT
or WHERE
clauses, while stored procedures must be invoked independently and cannot be part of an SQL expression.INSERT
, UPDATE
, or DELETE
, whereas functions are typically designed to be deterministic and avoid modifying database state.Get
, Add
, Update
, or Calculate
, to clarify their purpose.TRY...CATCH
blocks to gracefully handle and log exceptions during execution.dbo.AddCustomer
) to avoid ambiguity.SET NOCOUNT ON;
inside procs to reduce unnecessary network traffic.