Loading content...
Every stored procedure begins with the CREATE PROCEDURE statement—the DDL command that transforms SQL logic into a named, reusable database object. While the core concept is universal across database systems, the syntax and capabilities vary significantly between SQL Server's T-SQL, Oracle's PL/SQL, PostgreSQL's PL/pgSQL, and MySQL's SQL/PSM.
Mastering CREATE PROCEDURE means understanding not just where to put your SQL, but how to structure procedures for maintainability, how naming conventions affect discoverability, how to document your work, and how to avoid common pitfalls that make procedures difficult to debug and maintain.
By the end of this page, you will understand the complete anatomy of CREATE PROCEDURE across major database systems, how to structure procedure bodies effectively, naming conventions used in professional environments, documentation practices, and the differences between CREATE, ALTER, and CREATE OR REPLACE semantics.
The CREATE PROCEDURE statement follows a general pattern across all databases, though specific syntax elements differ:
Universal Structure:
CREATE PROCEDURE procedure_name
( parameter_list )
[ additional_clauses ]
AS | BEGIN
procedure_body
END;
The key components are:
12345678910111213141516171819202122
-- Basic procedure structure in SQL ServerCREATE PROCEDURE dbo.GetActiveCustomersASBEGIN -- SET NOCOUNT ON prevents row count messages SET NOCOUNT ON; SELECT CustomerID, CustomerName, Email, LastOrderDate FROM Customers WHERE Status = 'Active' ORDER BY CustomerName;END;GO -- Calling the procedureEXEC dbo.GetActiveCustomers;-- orEXECUTE dbo.GetActiveCustomers;PostgreSQL introduced procedures in version 11 primarily for transaction control (COMMIT/ROLLBACK within procedures). For returning result sets, PostgreSQL uses functions with RETURNS TABLE or RETURNS SETOF. This differs from SQL Server and MySQL where procedures can freely return result sets.
Professional database development demands consistent, meaningful naming conventions. Procedure names should communicate purpose at a glance.
Common naming patterns:
| Pattern | Example | Use Case |
|---|---|---|
action_entity | get_customer, update_order | Simple, readable (PostgreSQL style) |
sp_ActionEntity | sp_GetCustomer, sp_UpdateOrder | Legacy SQL Server style (avoid reserved names) |
usp_EntityAction | usp_CustomerGet, usp_OrderUpdate | User stored procedure prefix |
Entity_Action | Customer_Get, Order_Update | Entity-first organization |
proc_ModuleAction | proc_SalesGetTotal, proc_InvAdjust | Module-prefixed grouping |
CalculateOrderTotal not CalcOTsp_ for system procedures; use usp_ insteadOrder_Create, Order_Cancel, Order_ShipRather than encoding module names in procedure names (proc_SalesGetTotal), consider using schemas: sales.GetTotal, inventory.AdjustStock. Schemas provide cleaner naming, easier permission management, and better tooling support.
123456789101112131415161718192021222324252627282930313233
-- Schema-based organization (recommended)CREATE SCHEMA Sales;GO CREATE PROCEDURE Sales.GetOrderTotal @OrderID INTASBEGIN SELECT SUM(Quantity * UnitPrice) AS Total FROM Sales.OrderLines WHERE OrderID = @OrderID;END;GO CREATE PROCEDURE Sales.ProcessReturn @OrderID INT, @Reason NVARCHAR(500)ASBEGIN -- Return processing logicEND;GO -- Calling with schema qualificationEXEC Sales.GetOrderTotal @OrderID = 12345; -- Compare with prefixed naming (less recommended)CREATE PROCEDURE usp_Sales_GetOrderTotal @OrderID INTASBEGIN -- Same logic but name is longer and less cleanEND;Procedure management involves creating new procedures and modifying existing ones. Different databases offer different mechanisms:
CREATE PROCEDURE:
ALTER PROCEDURE:
CREATE OR REPLACE:
DROP + CREATE:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
-- CREATE: Fails if existsCREATE PROCEDURE dbo.CalculateTax @Amount DECIMAL(18,2), @State CHAR(2), @TaxAmount DECIMAL(18,2) OUTPUTASBEGIN SET @TaxAmount = CASE @State WHEN 'CA' THEN @Amount * 0.0725 WHEN 'NY' THEN @Amount * 0.08 ELSE @Amount * 0.06 END;END;GO -- ALTER: Modify existing (preserves permissions)ALTER PROCEDURE dbo.CalculateTax @Amount DECIMAL(18,2), @State CHAR(2), @TaxAmount DECIMAL(18,2) OUTPUTASBEGIN -- Updated logic with new states SET @TaxAmount = CASE @State WHEN 'CA' THEN @Amount * 0.0725 WHEN 'NY' THEN @Amount * 0.08875 -- Updated rate WHEN 'TX' THEN @Amount * 0.0625 -- Added Texas WHEN 'FL' THEN @Amount * 0.06 ELSE @Amount * 0.05 END;END;GO -- CREATE OR ALTER (SQL Server 2016+): Best of bothCREATE OR ALTER PROCEDURE dbo.CalculateTax @Amount DECIMAL(18,2), @State CHAR(2), @TaxAmount DECIMAL(18,2) OUTPUTASBEGIN -- This works whether procedure exists or not SET @TaxAmount = CASE @State WHEN 'CA' THEN @Amount * 0.0725 WHEN 'NY' THEN @Amount * 0.08875 WHEN 'TX' THEN @Amount * 0.0625 WHEN 'FL' THEN @Amount * 0.06 ELSE @Amount * 0.05 END;END;GODROP PROCEDURE removes all GRANT permissions. In MySQL where DROP/CREATE is required, always script permission grants alongside procedure definitions. In SQL Server, prefer ALTER or CREATE OR ALTER to preserve permissions.
The procedure body contains the logic to execute. Different databases structure this differently, but common elements include:
Declaration Section:
Execution Section:
Exception Section (optional):
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
CREATE PROCEDURE Sales.ProcessOrder @OrderID INT, @Success BIT OUTPUT, @ErrorMessage NVARCHAR(500) OUTPUTASBEGIN -- ============================================= -- Declaration Section -- ============================================= SET NOCOUNT ON; -- Suppress row count messages DECLARE @TotalAmount DECIMAL(18,2); DECLARE @CustomerID INT; DECLARE @InventoryOK BIT = 1; -- ============================================= -- Execution Section -- ============================================= BEGIN TRY BEGIN TRANSACTION; -- Get order details SELECT @CustomerID = CustomerID, @TotalAmount = TotalAmount FROM Sales.Orders WHERE OrderID = @OrderID; -- Validate inventory IF EXISTS ( SELECT 1 FROM Sales.OrderLines ol JOIN Inventory.Stock s ON ol.ProductID = s.ProductID WHERE ol.OrderID = @OrderID AND ol.Quantity > s.AvailableQuantity ) BEGIN SET @InventoryOK = 0; SET @ErrorMessage = 'Insufficient inventory'; END IF @InventoryOK = 1 BEGIN -- Deduct inventory UPDATE s SET s.AvailableQuantity = s.AvailableQuantity - ol.Quantity FROM Inventory.Stock s JOIN Sales.OrderLines ol ON s.ProductID = ol.ProductID WHERE ol.OrderID = @OrderID; -- Update order status UPDATE Sales.Orders SET Status = 'Processed', ProcessedDate = GETDATE() WHERE OrderID = @OrderID; -- Record transaction INSERT INTO Sales.Transactions (OrderID, Amount, TransactionDate) VALUES (@OrderID, @TotalAmount, GETDATE()); SET @Success = 1; SET @ErrorMessage = NULL; END ELSE BEGIN SET @Success = 0; END COMMIT TRANSACTION; END TRY -- ============================================= -- Exception Section -- ============================================= BEGIN CATCH ROLLBACK TRANSACTION; SET @Success = 0; SET @ErrorMessage = ERROR_MESSAGE(); END CATCHEND;MySQL requires special handling for procedure definitions because the default statement delimiter (semicolon) conflicts with semicolons inside the procedure body.
The problem:
When you type CREATE PROCEDURE ... BEGIN ... END;, the MySQL client sees the first semicolon inside the procedure body and thinks the statement is complete. The procedure definition fails.
The solution:
Temporarily change the delimiter to something else (commonly // or $$), define the procedure, then restore the delimiter.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
-- Common error: This FAILS without delimiter change-- CREATE PROCEDURE GetCount()-- BEGIN-- SELECT COUNT(*) FROM users; <-- Client stops here-- END; -- Correct approach: Change delimiterDELIMITER // CREATE PROCEDURE inventory.calculate_reorder_point( IN p_product_id INT, OUT p_reorder_point INT, OUT p_reorder_quantity INT)BEGIN DECLARE v_avg_daily_sales DECIMAL(10,2); DECLARE v_lead_time_days INT; DECLARE v_safety_stock INT; -- Calculate average daily sales SELECT AVG(quantity) INTO v_avg_daily_sales FROM sales_history WHERE product_id = p_product_id AND sale_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY); -- Get product lead time SELECT lead_time_days, safety_stock_days INTO v_lead_time_days, v_safety_stock FROM products WHERE product_id = p_product_id; -- Calculate reorder point: (avg daily sales × lead time) + safety stock SET p_reorder_point = CEILING( v_avg_daily_sales * (v_lead_time_days + v_safety_stock) ); -- Economic order quantity (simplified) SET p_reorder_quantity = CEILING(v_avg_daily_sales * 30); END // DELIMITER ; -- Now you can call normallyCALL inventory.calculate_reorder_point(101, @reorder_pt, @reorder_qty);SELECT @reorder_pt AS reorder_point, @reorder_qty AS reorder_quantity;Always restore the delimiter to ; after your procedure definition. Use a consistent delimiter across your team (// or $$). When working with tools like MySQL Workbench, the tool may handle delimiters automatically, but scripts should always be explicit.
Beyond the basic structure, procedures can have various attributes that control their behavior, security context, and optimization:
Common options across databases:
| Attribute | SQL Server | PostgreSQL | MySQL | Oracle |
|---|---|---|---|---|
| Security Context | EXECUTE AS | SECURITY DEFINER/INVOKER | SQL SECURITY | AUTHID |
| Language | Implicit T-SQL | LANGUAGE plpgsql/sql | Implicit SQL/PSM | Implicit PL/SQL |
| Determinism | N/A | IMMUTABLE/STABLE/VOLATILE | DETERMINISTIC/NOT DETERMINISTIC | DETERMINISTIC |
| Read/Modify | N/A | N/A | READS SQL DATA/MODIFIES SQL DATA | N/A |
| Encryption | WITH ENCRYPTION | N/A | N/A | WRAPPED (obfuscation) |
123456789101112131415161718192021222324252627282930313233343536373839
-- Security context optionsCREATE PROCEDURE dbo.GetSensitiveDataWITH EXECUTE AS OWNER -- Runs with procedure owner's permissionsASBEGIN SELECT * FROM ConfidentialRecords;END;GO -- Encryption: Hide procedure source codeCREATE PROCEDURE dbo.SecretAlgorithmWITH ENCRYPTIONASBEGIN -- Source will not be visible in system views SELECT 'Secret calculation' AS Result;END;GO -- Recompile: Generate fresh plan on each executionCREATE PROCEDURE dbo.DynamicReportWITH RECOMPILEASBEGIN -- Good for procedures with highly variable data distributions SELECT * FROM SalesData WHERE Region = 'Dynamic';END;GO -- Native compilation (In-Memory OLTP)CREATE PROCEDURE dbo.FastInsertWITH NATIVE_COMPILATION, SCHEMABINDINGASBEGIN ATOMIC WITH (TRANSACTION ISOLATION LEVEL = SNAPSHOT, LANGUAGE = N'English') INSERT INTO dbo.InMemoryTable (Col1, Col2) VALUES (1, 2);END;GOSECURITY DEFINER/EXECUTE AS OWNER can create security vulnerabilities if not used carefully. In PostgreSQL, always SET search_path explicitly to prevent privilege escalation attacks. Document why elevated permissions are necessary.
Well-documented procedures are maintainable procedures. Professional database development requires consistent documentation standards.
Essential documentation elements:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
-- ===========================================================================-- Procedure: Sales.ProcessRefund-- ===========================================================================-- Purpose: Processes a refund for a completed order. Restores inventory,-- creates a refund transaction record, and updates order status.---- Parameters:-- @OrderID INT - The order to refund (required)-- @RefundReason NVARCHAR(500) - Reason for refund (required)-- @PartialAmount DECIMAL(18,2) - Partial refund amount; NULL for full refund-- @RefundID INT OUTPUT - Returns the ID of the created refund record-- @Success BIT OUTPUT - 1 if successful, 0 if failed-- @ErrorMessage NVARCHAR(500) OUTPUT - Error description if failed---- Returns: Outputs populated, no result set---- Dependencies:-- - Tables: Sales.Orders, Sales.OrderLines, Sales.Refunds, Inventory.Stock-- - Procedures: Inventory.RestoreStock---- Example:-- DECLARE @rid INT, @ok BIT, @err NVARCHAR(500);-- EXEC Sales.ProcessRefund -- @OrderID = 12345,-- @RefundReason = 'Customer request - item not as described',-- @PartialAmount = NULL, -- Full refund-- @RefundID = @rid OUTPUT,-- @Success = @ok OUTPUT,-- @ErrorMessage = @err OUTPUT;-- SELECT @rid AS RefundID, @ok AS Success, @err AS Error;---- Change History:-- 2024-01-15 Smith, J. Initial creation-- 2024-03-22 Doe, A. Added partial refund support-- 2024-06-10 Smith, J. Fixed inventory restoration for multi-line orders---- Notes:-- - Full refunds automatically restore all inventory-- - Partial refunds do NOT restore inventory (manual process)-- - Order must be in 'Completed' or 'Shipped' status-- - Creates audit trail in Sales.RefundAudit-- ===========================================================================CREATE OR ALTER PROCEDURE Sales.ProcessRefund @OrderID INT, @RefundReason NVARCHAR(500), @PartialAmount DECIMAL(18,2) = NULL, @RefundID INT OUTPUT, @Success BIT OUTPUT, @ErrorMessage NVARCHAR(500) OUTPUTASBEGIN SET NOCOUNT ON; -- Implementation... END;GOBeyond inline comments, use database-level comments (COMMENT ON in PostgreSQL, sp_addextendedproperty in SQL Server). These are queryable and can integrate with documentation systems.
Avoid these common pitfalls when creating stored procedures:
sp_ in SQL Server causes the engine to search master database first, adding lookup overheadSELECT * FROM Orders may resolve to wrong schema; always qualify: Sales.Orders12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
-- ❌ BAD: Multiple common mistakesCREATE PROCEDURE sp_ProcessOrder -- Reserved prefix @id int -- Cryptic nameAS SELECT * FROM Orders WHERE OrderID = @id -- No schema, SELECT * UPDATE Orders SET Status = 'Done' WHERE OrderID = @id -- No transactionGO -- ✅ GOOD: Professional implementationCREATE PROCEDURE Sales.ProcessOrder @OrderID INT, @Success BIT OUTPUT, @ErrorMessage NVARCHAR(500) OUTPUTASBEGIN SET NOCOUNT ON; -- Suppress row counts -- Validate input IF @OrderID IS NULL OR @OrderID <= 0 BEGIN SET @Success = 0; SET @ErrorMessage = 'Invalid OrderID parameter'; RETURN; END BEGIN TRY BEGIN TRANSACTION; -- Check order exists IF NOT EXISTS (SELECT 1 FROM Sales.Orders WHERE OrderID = @OrderID) BEGIN SET @Success = 0; SET @ErrorMessage = 'Order not found: ' + CAST(@OrderID AS NVARCHAR); RETURN; END -- Update with specific columns UPDATE Sales.Orders SET Status = 'Processed', ProcessedDate = GETDATE(), ProcessedBy = SYSTEM_USER WHERE OrderID = @OrderID; COMMIT TRANSACTION; SET @Success = 1; SET @ErrorMessage = NULL; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; SET @Success = 0; SET @ErrorMessage = ERROR_MESSAGE(); END CATCHEND;GOWe've explored the CREATE PROCEDURE statement in depth. Let's consolidate the key takeaways:
What's next:
With CREATE PROCEDURE mastered, we move to parameters—the mechanism for passing data into and out of procedures. The next page covers IN, OUT, and INOUT parameters, default values, and parameter passing patterns.
You now understand how to create stored procedures across major database systems, including proper naming, modification strategies, body structure, attributes, and documentation. Next, we'll learn how to parameterize procedures for flexible, reusable code.