Loading learning content...
Every database developer eventually faces this question: "Should I use a function or a stored procedure?" The answer isn't always obvious because both serve similar purposes—encapsulating reusable logic in the database. Yet they're fundamentally different constructs designed for different use cases, and choosing incorrectly leads to awkward workarounds, performance problems, or unnecessary limitations.
The confusion is understandable. Both functions and procedures:
But beneath these similarities lie critical differences in their design philosophy, capability constraints, and invocation patterns that make each suited to specific scenarios. Understanding these differences transforms ambiguous choices into clear decisions.
By the end of this page, you will understand the fundamental philosophical differences between functions and procedures, learn the specific capability constraints each faces, develop a decision framework for choosing between them, and recognize refactoring patterns for converting between the two when requirements change.
The distinction between functions and procedures traces back to fundamental programming paradigm differences:
Functions embody the mathematical concept of a mapping. A function receives inputs and produces an output. It answers the question: "Given these values, what is the result?" The emphasis is on computation and transformation. In an idealized form, functions are pure—they don't change anything outside themselves; they simply compute and return.
Procedures embody the imperative concept of a command sequence. A procedure receives parameters and performs actions. It answers the question: "Given these parameters, do these things." The emphasis is on action and side effects. Procedures modify database state, handle transactions, and orchestrate complex multi-step operations.
This philosophical difference manifests in every aspect of how databases treat these constructs.
Ask yourself: 'Is this logic computing or transforming a value?' → Function. 'Is this logic performing an action or orchestrating a process?' → Procedure. This simple question correctly categorizes 90% of cases.
While functions and procedures share conceptual similarities, databases impose specific capability constraints on each that reflect their intended purposes. These constraints vary by database system but follow consistent patterns.
| Capability | Functions | Stored Procedures |
|---|---|---|
| Return value | Required (scalar or table) | Optional (integer return code for status) |
| Output parameters | Not allowed | Supported (OUT, INOUT) |
| Result sets (SELECT without INTO) | Not allowed in scalar functions | Unlimited result sets |
| DML statements (INSERT, UPDATE, DELETE) | Not allowed on permanent tables* | Fully supported |
| DDL statements (CREATE, ALTER, DROP) | Not allowed | Fully supported |
| Transaction control (BEGIN/COMMIT/ROLLBACK) | Not allowed | Fully supported |
| Error handling (TRY/CATCH) | Limited or not allowed | Fully supported |
| Execute dynamic SQL (EXEC, sp_executesql) | Not allowed | Fully supported |
| Call procedures | Not allowed | Can call other procedures |
| Be called from SELECT | Yes | No |
| Use in WHERE/JOIN conditions | Yes | No |
| Use in computed columns | Scalar: Yes (with constraints) | No |
| Use in CHECK constraints | Yes (deterministic) | No |
*Note: Functions can modify table variables within the function body but cannot modify permanent tables in most databases.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
-- =====================================================-- FUNCTION LIMITATIONS: What you CANNOT do-- ===================================================== -- ERROR: Cannot modify permanent tablesCREATE FUNCTION dbo.LogAndCompute(@Value INT)RETURNS INTASBEGIN -- This is NOT ALLOWED: -- INSERT INTO AuditLog (Value, LogTime) VALUES (@Value, GETDATE()); RETURN @Value * 2;END; -- ERROR: Cannot use output parameters-- CREATE FUNCTION dbo.BadFunction(@Input INT, @Output INT OUTPUT) -- Invalid! -- ERROR: Cannot return multiple result setsCREATE FUNCTION dbo.BadFunction(@Id INT)RETURNS INTASBEGIN -- This is NOT ALLOWED: -- SELECT * FROM Table1 WHERE Id = @Id; -- SELECT * FROM Table2 WHERE ParentId = @Id; RETURN @Id;END; -- ERROR: Cannot use dynamic SQLCREATE FUNCTION dbo.BadFunction(@TableName VARCHAR(100))RETURNS INTASBEGIN -- This is NOT ALLOWED: -- EXEC('SELECT COUNT(*) FROM ' + @TableName); RETURN 0;END; -- ERROR: Cannot call stored proceduresCREATE FUNCTION dbo.BadFunction(@Id INT)RETURNS INTASBEGIN -- This is NOT ALLOWED: -- EXEC dbo.SomeStoredProcedure @Id; RETURN @Id * 2;END; -- =====================================================-- PROCEDURE CAPABILITIES: Full flexibility-- ===================================================== CREATE PROCEDURE dbo.ProcessOrder @OrderId INT, @ProcessedCount INT OUTPUT, -- Output parameter @Status VARCHAR(50) OUTPUTASBEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; -- Transaction control -- DML operations UPDATE Orders SET Status = 'PROCESSING' WHERE OrderId = @OrderId; INSERT INTO OrderHistory (OrderId, Action, ActionTime) VALUES (@OrderId, 'Started Processing', GETDATE()); -- Dynamic SQL DECLARE @SQL NVARCHAR(MAX); SET @SQL = N'SELECT @Count = COUNT(*) FROM OrderItems WHERE OrderId = @OId'; EXEC sp_executesql @SQL, N'@OId INT, @Count INT OUTPUT', @OrderId, @ProcessedCount OUTPUT; -- Call another procedure EXEC dbo.SendNotification @OrderId; COMMIT TRANSACTION; SET @Status = 'SUCCESS'; END TRY BEGIN CATCH ROLLBACK TRANSACTION; SET @Status = 'ERROR: ' + ERROR_MESSAGE(); END CATCH; -- Return multiple result sets SELECT * FROM Orders WHERE OrderId = @OrderId; SELECT * FROM OrderItems WHERE OrderId = @OrderId; RETURN 0; -- Return codeEND;Perhaps the most practical difference between functions and procedures is where and how they can be called. Functions integrate into SQL's expression syntax; procedures are invoked as standalone commands. This distinction determines which construct fits your use case.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
-- =====================================================-- FUNCTION INVOCATION: Inside SQL expressions-- ===================================================== -- In SELECT listSELECT ProductId, ProductName, Price, dbo.CalculateTax(Price, 0.08) AS TaxFROM Products; -- In WHERE clauseSELECT * FROM EmployeesWHERE dbo.GetAge(BirthDate, GETDATE()) >= 21; -- In JOIN conditionsSELECT c.CustomerName, o.*FROM Customers cINNER JOIN Orders o ON dbo.NormalizeKey(c.CustomerId) = dbo.NormalizeKey(o.CustomerId); -- As computed column definitionALTER TABLE Products ADD TaxAmount AS dbo.CalculateTax(Price, 0.08); -- In CHECK constraintALTER TABLE Employees ADD CONSTRAINT chk_age CHECK (dbo.GetAge(BirthDate, GETDATE()) >= 16); -- In CASE expressionSELECT CASE dbo.GetCustomerTier(CustomerId) WHEN 'GOLD' THEN Price * 0.9 WHEN 'SILVER' THEN Price * 0.95 ELSE Price END AS FinalPriceFROM Orders; -- In ORDER BYSELECT * FROM Products ORDER BY dbo.CalculatePopularityScore(ProductId) DESC; -- Table-valued function in FROM clauseSELECT * FROM dbo.GetOrdersByCustomer(12345) WHERE Total > 100; -- =====================================================-- PROCEDURE INVOCATION: Standalone commands only-- ===================================================== -- Basic executionEXEC dbo.ProcessOrder @OrderId = 123; -- Or with full syntaxEXECUTE dbo.ProcessOrder @OrderId = 123; -- With output parametersDECLARE @ItemCount INT, @Status VARCHAR(50);EXEC dbo.ProcessOrder @OrderId = 123, @ProcessedCount = @ItemCount OUTPUT, @Status = @Status OUTPUT;PRINT @Status; -- Capturing return codeDECLARE @ReturnCode INT;EXEC @ReturnCode = dbo.ProcessOrder @OrderId = 123;IF @ReturnCode <> 0 PRINT 'Procedure failed'; -- CANNOT do these with procedures:-- SELECT *, dbo.ProcessOrder(OrderId) FROM Orders; -- Invalid!-- SELECT * FROM dbo.ProcessOrder(123); -- Invalid for most procedures!-- WHERE dbo.ProcessOrder(OrderId) = 0; -- Invalid! -- PostgreSQL: CALL for procedures, SELECT for functions-- CALL process_order(123); -- Procedure-- SELECT * FROM get_orders_by_customer(123); -- FunctionIf you need to use the logic inside a SELECT list, WHERE clause, JOIN condition, or any SQL expression, you MUST use a function. Procedures cannot appear in these contexts. This single test often determines your choice.
Performance characteristics differ significantly between functions and procedures, primarily due to how they're executed within the query engine.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
-- Performance scenario: Processing 1 million rows -- FUNCTION APPROACH (potential performance problem)CREATE FUNCTION dbo.CalculateComplexMetric(@Value1 INT, @Value2 INT, @Value3 INT)RETURNS DECIMAL(18,6)ASBEGIN -- Complex calculation DECLARE @Result DECIMAL(18,6); SET @Result = POWER(CAST(@Value1 AS FLOAT), 2) / SQRT(CAST(@Value2 AS FLOAT)) * LOG(CAST(@Value3 AS FLOAT) + 1); RETURN @Result;END; -- This query invokes the function 1 MILLION timesSELECT Id, dbo.CalculateComplexMetric(Col1, Col2, Col3) AS MetricFROM LargeTable; -- 1 million rows-- Elapsed time: 45 seconds (function call overhead dominates) -- INLINE EXPRESSION APPROACH (much faster)SELECT Id, POWER(CAST(Col1 AS FLOAT), 2) / SQRT(CAST(Col2 AS FLOAT)) * LOG(CAST(Col3 AS FLOAT) + 1) AS MetricFROM LargeTable;-- Elapsed time: 2 seconds (optimizer handles efficiently) -- PROCEDURE APPROACH (for batch update)CREATE PROCEDURE dbo.UpdateAllMetricsASBEGIN UPDATE LargeTable SET Metric = POWER(CAST(Col1 AS FLOAT), 2) / SQRT(CAST(Col2 AS FLOAT)) * LOG(CAST(Col3 AS FLOAT) + 1);END; EXEC dbo.UpdateAllMetrics;-- Elapsed time: 3 seconds (single set-based operation) -- When function is REQUIRED (e.g., computed column), consider:-- 1. Ensure schema binding for potential inlining-- 2. Use persisted computed column to compute once-- 3. Index the computed column if frequently filteredDifferent database systems have varying rules for function and procedure capabilities. Understanding these differences is crucial for portable database development and migration projects.
1234567891011121314151617181920212223
-- SQL Server: Strictest function limitations -- Functions CANNOT:-- - Modify permanent tables (INSERT/UPDATE/DELETE)-- - Use dynamic SQL (EXEC, sp_executesql)-- - Call stored procedures-- - Use temporary tables-- - Use TRY/CATCH (before SQL Server 2019)-- - Return multiple result sets-- - Have output parameters -- Functions CAN:-- - Modify table variables (within the function)-- - Call other functions-- - Use CASE, IF, WHILE within limits-- - Be used in computed columns, views, constraints -- Procedures have no such limitations -- Unique SQL Server features:-- - Scalar UDF inlining (2019+)-- - SCHEMABINDING for determinism-- - Inline vs multi-statement TVF distinctionGiven the differences we've explored, how do you decide between a function and a procedure? Here's a systematic decision framework based on your requirements.
12345678910111213141516171819202122232425262728293031323334353637
FUNCTION VS PROCEDURE DECISION TREE===================================== START: What does your logic need to do? ├─► Need to use in SELECT/WHERE/JOIN expressions?│ └─► YES → Must use FUNCTION│├─► Need to return a value used in SQL expressions?│ └─► YES → Must use FUNCTION│├─► Need to modify permanent tables (INSERT/UPDATE/DELETE)?│ ├─► SQL Server → Must use PROCEDURE│ └─► PostgreSQL/Oracle/MySQL → PROCEDURE preferred, some functions allowed│├─► Need transaction control (COMMIT/ROLLBACK)?│ └─► YES → Must use PROCEDURE│├─► Need to return multiple result sets?│ └─► YES → Must use PROCEDURE│├─► Need output parameters?│ └─► YES → PROCEDURE (or table-valued function returning single row)│├─► Need dynamic SQL?│ └─► YES → PROCEDURE (SQL Server) or check platform│├─► Use in computed column or indexed view?│ └─► YES → Must use FUNCTION (deterministic)│├─► Check constraint referencing the logic?│ └─► YES → Must use FUNCTION (deterministic)│└─► None of the above? └─► Consider PURPOSE: ├─► Pure computation → FUNCTION (enables more usage contexts) └─► Action/orchestration → PROCEDURE (more capabilities)Sometimes you need capabilities from both functions and procedures. Rather than choosing, you can combine them using hybrid patterns that leverage the strengths of each.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
-- =====================================================-- PATTERN 1: Function for calculation, Procedure for action-- Separate concerns into appropriate constructs-- ===================================================== -- Pure calculation function (deterministic, usable anywhere)CREATE FUNCTION dbo.CalculateOrderTotal(@OrderId INT)RETURNS DECIMAL(18,2)ASBEGIN DECLARE @Total DECIMAL(18,2); SELECT @Total = SUM(Quantity * UnitPrice * (1 - Discount)) FROM OrderItems WHERE OrderId = @OrderId; RETURN ISNULL(@Total, 0);END; -- Action procedure that uses the functionCREATE PROCEDURE dbo.FinalizeOrder(@OrderId INT)ASBEGIN DECLARE @Total DECIMAL(18,2) = dbo.CalculateOrderTotal(@OrderId); -- Use function result in procedure actions UPDATE Orders SET TotalAmount = @Total, Status = 'FINALIZED', FinalizedDate = GETDATE() WHERE OrderId = @OrderId; INSERT INTO OrderAudit (OrderId, Action, Amount, ActionDate) VALUES (@OrderId, 'FINALIZED', @Total, GETDATE()); -- Send notification (procedure call) EXEC dbo.SendOrderNotification @OrderId, @Total;END; -- Benefit: CalculateOrderTotal can be used in SELECTs, WHEREs, etc.-- FinalizeOrder handles all the action/side-effect logic -- =====================================================-- PATTERN 2: TVF for complex queries, Procedure for orchestration-- ===================================================== -- Reusable query as inline TVFCREATE FUNCTION dbo.GetPendingOrdersForCustomer(@CustomerId INT)RETURNS TABLEASRETURN( SELECT o.OrderId, o.OrderDate, o.TotalAmount, o.Status FROM Orders o WHERE o.CustomerId = @CustomerId AND o.Status = 'PENDING'); -- Procedure uses TVF for reporting and actionsCREATE PROCEDURE dbo.ProcessCustomerOrders(@CustomerId INT)ASBEGIN -- First, report current state using TVF SELECT * FROM dbo.GetPendingOrdersForCustomer(@CustomerId); -- Then take action UPDATE o SET Status = 'PROCESSING' FROM Orders o INNER JOIN dbo.GetPendingOrdersForCustomer(@CustomerId) AS pending ON o.OrderId = pending.OrderId; -- Log action count INSERT INTO ProcessingLog (CustomerId, OrdersProcessed, ProcessedAt) VALUES (@CustomerId, @@ROWCOUNT, GETDATE());END; -- =====================================================-- PATTERN 3: Wrapper procedure for function with logging-- When you need function semantics but also want logging-- ===================================================== -- Core logic as functionCREATE FUNCTION dbo.CalculateTaxAmount(@Amount DECIMAL(18,2), @TaxRate DECIMAL(5,4))RETURNS DECIMAL(18,2)WITH SCHEMABINDINGASBEGIN RETURN ROUND(@Amount * @TaxRate, 2);END; -- Wrapper procedure that logs (for API calls)CREATE PROCEDURE dbo.CalculateAndLogTax @Amount DECIMAL(18,2), @TaxRate DECIMAL(5,4), @Result DECIMAL(18,2) OUTPUTASBEGIN -- Use function for calculation SET @Result = dbo.CalculateTaxAmount(@Amount, @TaxRate); -- Log for auditing INSERT INTO TaxCalculationLog (Amount, Rate, Result, CalculatedAt, RequestedBy) VALUES (@Amount, @TaxRate, @Result, GETDATE(), SUSER_NAME());END; -- Application calls procedure for audit trail-- Database constraints use function directlyWhen possible, separate pure calculation logic (function) from action logic (procedure). This enables maximum reusability—the function can be used in computed columns, constraints, and ad-hoc queries, while the procedure orchestrates business processes that use those calculations.
Requirements change, and sometimes you need to convert between functions and procedures. Understanding transformation patterns helps you refactor effectively.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
-- =====================================================-- PROCEDURE → FUNCTION: Extract pure computation-- ===================================================== -- Original: Procedure that computes AND actsCREATE PROCEDURE dbo.GetDiscountedPrice_Old @ProductId INT, @CustomerId INT, @Quantity INT, @FinalPrice DECIMAL(18,2) OUTPUTASBEGIN DECLARE @BasePrice DECIMAL(18,2); DECLARE @DiscountRate DECIMAL(5,4); SELECT @BasePrice = Price FROM Products WHERE ProductId = @ProductId; SELECT @DiscountRate = DiscountRate FROM CustomerDiscounts WHERE CustomerId = @CustomerId; SET @FinalPrice = @BasePrice * @Quantity * (1 - ISNULL(@DiscountRate, 0)); -- Log this calculation INSERT INTO PriceCalculationLog VALUES (@ProductId, @CustomerId, @FinalPrice, GETDATE());END; -- Refactored: Pure function + wrapper procedureCREATE FUNCTION dbo.CalculateDiscountedPrice( @BasePrice DECIMAL(18,2), @Quantity INT, @DiscountRate DECIMAL(5,4))RETURNS DECIMAL(18,2)WITH SCHEMABINDINGASBEGIN RETURN @BasePrice * @Quantity * (1 - ISNULL(@DiscountRate, 0));END; -- Function can now be used in queries:SELECT ProductId, dbo.CalculateDiscountedPrice(Price, 10, 0.1) AS BulkPriceFROM Products; -- =====================================================-- FUNCTION → PROCEDURE: Add side effects-- ===================================================== -- Original: Function that needs to log (impossible in SQL Server)CREATE FUNCTION dbo.CalculateShipping_Old(@Weight DECIMAL(10,2), @ZoneId INT)RETURNS DECIMAL(8,2)ASBEGIN -- CANNOT log: INSERT INTO ShippingCalcLog VALUES (@Weight, @ZoneId, GETDATE()); RETURN @Weight * 0.5 + @ZoneId * 2.0;END; -- Refactored: Keep function, add procedure wrapper-- Function stays pure for use in constraints, computed columnsCREATE FUNCTION dbo.CalculateShipping(@Weight DECIMAL(10,2), @ZoneId INT)RETURNS DECIMAL(8,2)WITH SCHEMABINDINGASBEGIN RETURN @Weight * 0.5 + @ZoneId * 2.0;END; -- Procedure adds loggingCREATE PROCEDURE dbo.CalculateShippingWithLog @Weight DECIMAL(10,2), @ZoneId INT, @ShippingCost DECIMAL(8,2) OUTPUTASBEGIN SET @ShippingCost = dbo.CalculateShipping(@Weight, @ZoneId); INSERT INTO ShippingCalcLog (Weight, ZoneId, Result, CalcDate) VALUES (@Weight, @ZoneId, @ShippingCost, GETDATE());END; -- =====================================================-- MULTI-STATEMENT TVF → INLINE TVF (for performance)-- ===================================================== -- Original: Multi-statement TVF (performance problem)CREATE FUNCTION dbo.GetActiveOrders_Old(@CustomerId INT)RETURNS @Orders TABLE (OrderId INT, OrderDate DATE, Total DECIMAL(18,2))ASBEGIN INSERT INTO @Orders SELECT OrderId, OrderDate, TotalAmount FROM Orders WHERE CustomerId = @CustomerId AND Status = 'ACTIVE'; RETURN;END; -- Refactored: Inline TVF (optimizer-friendly)CREATE FUNCTION dbo.GetActiveOrders(@CustomerId INT)RETURNS TABLEASRETURN( SELECT OrderId, OrderDate, TotalAmount AS Total FROM Orders WHERE CustomerId = @CustomerId AND Status = 'ACTIVE');Functions and stored procedures serve different purposes in database programming. Understanding their philosophical foundations, capability constraints, and appropriate use cases enables you to choose the right tool for each situation.
Congratulations! You have completed the Functions module. You now understand user-defined functions comprehensively—from basic scalar functions through table-valued functions, determinism, and the critical distinctions from stored procedures. Apply this knowledge to encapsulate reusable logic effectively in your database applications.