Stored Procedures với T-SQL
1. Stored Procedure là gì?
Stored Procedure (SP) là một tập hợp các T-SQL statements được lưu trữ và compile sẵn trong database.
Benefits của Stored Procedures
Stored Procedure Benefits
SP Benefits
Performance
Pre-compiled
Cached execution plan
Reduced network traffic
Security
SQL Injection prevention
Permission control
Hide table structure
Maintainability
Centralized logic
Version control
Reusability
| Benefit | Description |
|---|---|
| Performance | Compiled once, executed many times |
| Security | Ngăn SQL Injection, control permissions |
| Maintainability | Centralize business logic |
| Reusability | Gọi từ nhiều nơi |
| Network | Giảm traffic (chỉ gửi SP name + params) |
2. CREATE PROCEDURE - Basic Syntax
2.1 Simple Procedure
SQL
1-- Basic syntax2CREATE PROCEDURE SchemaName.ProcedureName3AS4BEGIN5 -- T-SQL statements6END;7GO89-- Example: Get all active customers10CREATE PROCEDURE Customer.usp_GetActiveCustomers11AS12BEGIN13 SET NOCOUNT ON; -- Không trả về row count message14 15 SELECT 16 CustomerID,17 FirstName,18 LastName,19 Email,20 Phone21 FROM Customer.Customer22 WHERE IsActive = 123 ORDER BY LastName, FirstName;24END;25GO2627-- Execute28EXEC Customer.usp_GetActiveCustomers;29-- Hoặc30EXECUTE Customer.usp_GetActiveCustomers;2.2 Procedure với Input Parameters
SQL
1-- Parameters2CREATE PROCEDURE Customer.usp_GetCustomerByID3 @CustomerID INT -- Input parameter4AS5BEGIN6 SET NOCOUNT ON;7 8 SELECT 9 CustomerID,10 FirstName,11 LastName,12 Email,13 Phone,14 DateOfBirth,15 CreatedDate16 FROM Customer.Customer17 WHERE CustomerID = @CustomerID;18END;19GO2021-- Execute với parameter22EXEC Customer.usp_GetCustomerByID @CustomerID = 1;23-- Hoặc positional24EXEC Customer.usp_GetCustomerByID 1;2.3 Multiple Parameters với Default Values
SQL
1CREATE PROCEDURE Sales.usp_GetOrders2 @CustomerID INT = NULL, -- Optional, default NULL3 @Status VARCHAR(20) = NULL, -- Optional4 @FromDate DATE = NULL, -- Optional5 @ToDate DATE = NULL, -- Optional6 @TopN INT = 100 -- Default value7AS8BEGIN9 SET NOCOUNT ON;10 11 SELECT TOP (@TopN)12 o.OrderID,13 o.OrderNumber,14 o.OrderDate,15 o.Status,16 o.TotalAmount,17 c.FirstName + ' ' + c.LastName AS CustomerName18 FROM Sales.[Order] o19 JOIN Customer.Customer c ON o.CustomerID = c.CustomerID20 WHERE 21 (@CustomerID IS NULL OR o.CustomerID = @CustomerID)22 AND (@Status IS NULL OR o.Status = @Status)23 AND (@FromDate IS NULL OR o.OrderDate >= @FromDate)24 AND (@ToDate IS NULL OR o.OrderDate <= @ToDate)25 ORDER BY o.OrderDate DESC;26END;27GO2829-- Execute với different params30EXEC Sales.usp_GetOrders; -- All orders (top 100)31EXEC Sales.usp_GetOrders @CustomerID = 5; -- Customer 5's orders32EXEC Sales.usp_GetOrders @Status = 'Pending'; -- Pending orders33EXEC Sales.usp_GetOrders @FromDate = '2024-01-01', @TopN = 50; -- 2024 orders, top 503. Output Parameters & Return Values
3.1 Output Parameters
SQL
1CREATE PROCEDURE Customer.usp_CreateCustomer2 @FirstName NVARCHAR(50),3 @LastName NVARCHAR(50),4 @Email VARCHAR(100),5 @Phone VARCHAR(20) = NULL,6 @NewCustomerID INT OUTPUT, -- Output parameter7 @ErrorMessage NVARCHAR(500) OUTPUT8AS9BEGIN10 SET NOCOUNT ON;11 12 -- Validate email13 IF EXISTS (SELECT 1 FROM Customer.Customer WHERE Email = @Email)14 BEGIN15 SET @NewCustomerID = 0;16 SET @ErrorMessage = N'Email already exists';17 RETURN;18 END19 20 -- Insert customer21 INSERT INTO Customer.Customer (FirstName, LastName, Email, Phone)22 VALUES (@FirstName, @LastName, @Email, @Phone);23 24 -- Get new ID25 SET @NewCustomerID = SCOPE_IDENTITY();26 SET @ErrorMessage = NULL;27END;28GO2930-- Execute with OUTPUT31DECLARE @NewID INT, @Error NVARCHAR(500);3233EXEC Customer.usp_CreateCustomer34 @FirstName = N'Nguyễn',35 @LastName = N'Văn A',36 @Email = 'nguyenvana@email.com',37 @Phone = '0901234567',38 @NewCustomerID = @NewID OUTPUT,39 @ErrorMessage = @Error OUTPUT;4041SELECT @NewID AS NewCustomerID, @Error AS ErrorMessage;3.2 Return Values
SQL
1-- Return value (integer) thường dùng cho status code2CREATE PROCEDURE Sales.usp_CancelOrder3 @OrderID INT4AS5BEGIN6 SET NOCOUNT ON;7 8 -- Check if order exists9 IF NOT EXISTS (SELECT 1 FROM Sales.[Order] WHERE OrderID = @OrderID)10 RETURN -1; -- Not found11 12 -- Check if already cancelled13 IF EXISTS (SELECT 1 FROM Sales.[Order] WHERE OrderID = @OrderID AND Status = 'Cancelled')14 RETURN -2; -- Already cancelled15 16 -- Check if delivered (cannot cancel)17 IF EXISTS (SELECT 1 FROM Sales.[Order] WHERE OrderID = @OrderID AND Status = 'Delivered')18 RETURN -3; -- Cannot cancel delivered order19 20 -- Cancel order21 UPDATE Sales.[Order]22 SET Status = 'Cancelled',23 ModifiedDate = SYSDATETIME()24 WHERE OrderID = @OrderID;25 26 RETURN 0; -- Success27END;28GO2930-- Execute and check return value31DECLARE @Result INT;32EXEC @Result = Sales.usp_CancelOrder @OrderID = 123;3334SELECT CASE @Result35 WHEN 0 THEN 'Order cancelled successfully'36 WHEN -1 THEN 'Order not found'37 WHEN -2 THEN 'Order already cancelled'38 WHEN -3 THEN 'Cannot cancel delivered order'39 ELSE 'Unknown error'40END AS ResultMessage;4. Error Handling - TRY CATCH
4.1 Basic TRY CATCH
SQL
1CREATE PROCEDURE Sales.usp_ProcessOrder2 @OrderID INT3AS4BEGIN5 SET NOCOUNT ON;6 7 BEGIN TRY8 BEGIN TRANSACTION;9 10 -- Business logic here11 UPDATE Sales.[Order]12 SET Status = 'Processing'13 WHERE OrderID = @OrderID;14 15 -- Deduct inventory16 UPDATE p17 SET p.StockQuantity = p.StockQuantity - od.Quantity18 FROM Inventory.Product p19 JOIN Sales.OrderDetail od ON p.ProductID = od.ProductID20 WHERE od.OrderID = @OrderID;21 22 COMMIT TRANSACTION;23 END TRY24 BEGIN CATCH25 -- Rollback on error26 IF @@TRANCOUNT > 027 ROLLBACK TRANSACTION;28 29 -- Re-throw error30 THROW;31 END CATCH32END;33GO4.2 Error Handling với Chi tiết
SQL
1CREATE PROCEDURE Sales.usp_CreateOrder2 @CustomerID INT,3 @Products dbo.OrderItemType READONLY, -- Table-valued parameter4 @NewOrderID INT OUTPUT5AS6BEGIN7 SET NOCOUNT ON;8 9 DECLARE @ErrorNumber INT,10 @ErrorMessage NVARCHAR(4000),11 @ErrorSeverity INT,12 @ErrorState INT,13 @ErrorProcedure NVARCHAR(200),14 @ErrorLine INT;15 16 BEGIN TRY17 BEGIN TRANSACTION;18 19 -- Validate customer20 IF NOT EXISTS (SELECT 1 FROM Customer.Customer WHERE CustomerID = @CustomerID AND IsActive = 1)21 BEGIN22 RAISERROR(N'Customer not found or inactive', 16, 1);23 END24 25 -- Generate order number26 DECLARE @OrderNumber VARCHAR(20) = 'ORD-' + FORMAT(SYSDATETIME(), 'yyyyMMddHHmmss');27 28 -- Create order header29 INSERT INTO Sales.[Order] (OrderNumber, CustomerID, OrderDate, Status)30 VALUES (@OrderNumber, @CustomerID, SYSDATETIME(), 'Pending');31 32 SET @NewOrderID = SCOPE_IDENTITY();33 34 -- Create order details35 INSERT INTO Sales.OrderDetail (OrderID, ProductID, Quantity, UnitPrice)36 SELECT 37 @NewOrderID,38 p.ProductID,39 p.Quantity,40 pr.UnitPrice41 FROM @Products p42 JOIN Inventory.Product pr ON p.ProductID = pr.ProductID;43 44 -- Update order totals45 UPDATE Sales.[Order]46 SET SubTotal = (47 SELECT SUM(LineTotal) 48 FROM Sales.OrderDetail 49 WHERE OrderID = @NewOrderID50 )51 WHERE OrderID = @NewOrderID;52 53 COMMIT TRANSACTION;54 55 END TRY56 BEGIN CATCH57 IF @@TRANCOUNT > 058 ROLLBACK TRANSACTION;59 60 -- Capture error details61 SET @ErrorNumber = ERROR_NUMBER();62 SET @ErrorMessage = ERROR_MESSAGE();63 SET @ErrorSeverity = ERROR_SEVERITY();64 SET @ErrorState = ERROR_STATE();65 SET @ErrorProcedure = ERROR_PROCEDURE();66 SET @ErrorLine = ERROR_LINE();67 68 -- Log error69 INSERT INTO dbo.ErrorLog (ErrorNumber, ErrorMessage, ErrorProcedure, ErrorLine, ErrorTime)70 VALUES (@ErrorNumber, @ErrorMessage, @ErrorProcedure, @ErrorLine, SYSDATETIME());71 72 -- Re-raise error73 RAISERROR(@ErrorMessage, @ErrorSeverity, @ErrorState);74 75 SET @NewOrderID = 0;76 END CATCH77END;78GO5. Advanced Techniques
5.1 Table-Valued Parameters
SQL
1-- 1. Create Type2CREATE TYPE dbo.OrderItemType AS TABLE (3 ProductID INT NOT NULL,4 Quantity INT NOT NULL5);6GO78-- 2. Use in procedure9CREATE PROCEDURE Sales.usp_BulkInsertOrderDetails10 @OrderID INT,11 @Items dbo.OrderItemType READONLY12AS13BEGIN14 SET NOCOUNT ON;15 16 INSERT INTO Sales.OrderDetail (OrderID, ProductID, Quantity, UnitPrice)17 SELECT 18 @OrderID,19 i.ProductID,20 i.Quantity,21 p.UnitPrice22 FROM @Items i23 JOIN Inventory.Product p ON i.ProductID = p.ProductID;24END;25GO2627-- 3. Call procedure28DECLARE @OrderItems dbo.OrderItemType;29INSERT INTO @OrderItems (ProductID, Quantity) VALUES (1, 2), (2, 1), (3, 5);3031EXEC Sales.usp_BulkInsertOrderDetails @OrderID = 100, @Items = @OrderItems;5.2 Dynamic SQL
SQL
1CREATE PROCEDURE dbo.usp_DynamicSearch2 @TableName NVARCHAR(128),3 @SearchColumn NVARCHAR(128),4 @SearchValue NVARCHAR(100)5AS6BEGIN7 SET NOCOUNT ON;8 9 DECLARE @SQL NVARCHAR(MAX);10 11 -- Validate table exists12 IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = @TableName)13 BEGIN14 RAISERROR('Table not found', 16, 1);15 RETURN;16 END17 18 -- Build dynamic SQL (use QUOTENAME to prevent injection)19 SET @SQL = N'SELECT * FROM ' + QUOTENAME(@TableName) + 20 N' WHERE ' + QUOTENAME(@SearchColumn) + N' LIKE @Value';21 22 -- Execute with parameterized value23 EXEC sp_executesql @SQL, 24 N'@Value NVARCHAR(100)', 25 @Value = '%' + @SearchValue + '%';26END;27GO2829-- ⚠️ WARNING: Dynamic SQL có risk. Luôn dùng QUOTENAME và sp_executesql với parameters5.3 Cursor (tránh nếu có thể)
SQL
1-- Cursor example (thường nên tránh, dùng set-based operations)2CREATE PROCEDURE Sales.usp_UpdateOrderStatuses3AS4BEGIN5 SET NOCOUNT ON;6 7 DECLARE @OrderID INT, @DaysOld INT;8 9 -- Cursor for orders10 DECLARE order_cursor CURSOR LOCAL FAST_FORWARD FOR11 SELECT OrderID, DATEDIFF(DAY, OrderDate, GETDATE()) AS DaysOld12 FROM Sales.[Order]13 WHERE Status = 'Shipped';14 15 OPEN order_cursor;16 17 FETCH NEXT FROM order_cursor INTO @OrderID, @DaysOld;18 19 WHILE @@FETCH_STATUS = 020 BEGIN21 -- Auto-complete if shipped > 7 days22 IF @DaysOld > 723 BEGIN24 UPDATE Sales.[Order]25 SET Status = 'Delivered'26 WHERE OrderID = @OrderID;27 END28 29 FETCH NEXT FROM order_cursor INTO @OrderID, @DaysOld;30 END31 32 CLOSE order_cursor;33 DEALLOCATE order_cursor;34END;35GO3637-- ✅ Better: Set-based approach38UPDATE Sales.[Order]39SET Status = 'Delivered'40WHERE Status = 'Shipped'41AND DATEDIFF(DAY, OrderDate, GETDATE()) > 7;6. ALTER & DROP Procedure
SQL
1-- ALTER to modify2ALTER PROCEDURE Customer.usp_GetActiveCustomers3AS4BEGIN5 SET NOCOUNT ON;6 7 SELECT 8 CustomerID,9 FirstName,10 LastName,11 Email,12 Phone,13 CreatedDate -- Added column14 FROM Customer.Customer15 WHERE IsActive = 116 ORDER BY CreatedDate DESC; -- Changed order17END;18GO1920-- CREATE OR ALTER (SQL Server 2016+)21CREATE OR ALTER PROCEDURE Customer.usp_GetActiveCustomers22AS23BEGIN24 -- ...25END;26GO2728-- DROP29DROP PROCEDURE Customer.usp_GetActiveCustomers;30DROP PROCEDURE IF EXISTS Customer.usp_GetActiveCustomers;7. AdventureWorks Stored Procedures
SQL
1-- Xem list SPs2SELECT 3 SCHEMA_NAME(schema_id) AS SchemaName,4 name AS ProcedureName,5 create_date,6 modify_date7FROM sys.procedures8ORDER BY SchemaName, name;910-- Xem definition11EXEC sp_helptext 'dbo.uspGetEmployeeManagers';1213-- Execute AdventureWorks SP14EXEC dbo.uspGetEmployeeManagers @BusinessEntityID = 5;1516-- Study pattern: AdventureWorks SPs17EXEC dbo.uspGetManagerEmployees @BusinessEntityID = 2;18EXEC dbo.uspSearchCandidateResumes @searchString = 'SQL';8. Best Practices
8.1 Naming Conventions
SQL
1-- Prefix với usp_ (user stored procedure)2usp_GetCustomers3usp_CreateOrder4usp_UpdateProduct56-- Verb + Noun pattern7usp_Get... -- Retrieve data8usp_Create... -- Insert new9usp_Update... -- Modify existing10usp_Delete... -- Remove11usp_Search... -- Search/filter12usp_Process... -- Complex operations1314-- Schema organization15Customer.usp_GetByID16Sales.usp_CreateOrder17Inventory.usp_UpdateStock8.2 Standard Template
SQL
1CREATE OR ALTER PROCEDURE SchemaName.usp_ProcedureName2 -- Parameters3 @Param1 INT,4 @Param2 NVARCHAR(100) = NULL,5 @OutputParam INT OUTPUT6AS7BEGIN8 /*9 =============================================10 Author: Your Name11 Create date: 2024-01-1512 Description: Brief description of what SP does13 14 Modification History:15 Date Author Description16 ---------- ---------- ---------------------17 2024-01-15 Your Name Initial creation18 =============================================19 */20 21 SET NOCOUNT ON;22 SET XACT_ABORT ON; -- Auto rollback on error23 24 -- Validate inputs25 IF @Param1 IS NULL26 BEGIN27 RAISERROR('Param1 is required', 16, 1);28 RETURN -1;29 END30 31 BEGIN TRY32 BEGIN TRANSACTION;33 34 -- Business logic here35 36 COMMIT TRANSACTION;37 RETURN 0; -- Success38 39 END TRY40 BEGIN CATCH41 IF @@TRANCOUNT > 042 ROLLBACK TRANSACTION;43 44 -- Log error or handle45 THROW;46 END CATCH47END;48GO8.3 Performance Tips
SQL
1-- ✅ DO2SET NOCOUNT ON; -- Reduce network traffic3SET XACT_ABORT ON; -- Consistent error handling4Use parameters, not string concat -- Prevent injection, enable plan cache56-- ❌ DON'T7SELECT * -- Select specific columns8EXEC('SELECT...' + @var) -- Avoid dynamic SQL when possible9Nested cursors -- Use set-based operations9. Hands-on Exercise
Exercise: Create CRUD Procedures
SQL
1-- Complete CRUD for Product23-- 1. READ - Get all products4CREATE OR ALTER PROCEDURE Inventory.usp_GetProducts5 @CategoryID INT = NULL,6 @IsActive BIT = NULL,7 @SearchTerm NVARCHAR(100) = NULL8AS9BEGIN10 SET NOCOUNT ON;11 12 SELECT 13 p.ProductID,14 p.SKU,15 p.ProductName,16 p.Description,17 c.CategoryName,18 p.UnitPrice,19 p.StockQuantity,20 p.IsActive21 FROM Inventory.Product p22 JOIN Inventory.Category c ON p.CategoryID = c.CategoryID23 WHERE 24 (@CategoryID IS NULL OR p.CategoryID = @CategoryID)25 AND (@IsActive IS NULL OR p.IsActive = @IsActive)26 AND (@SearchTerm IS NULL 27 OR p.ProductName LIKE '%' + @SearchTerm + '%'28 OR p.SKU LIKE '%' + @SearchTerm + '%')29 ORDER BY p.ProductName;30END;31GO3233-- 2. READ - Get single product34CREATE OR ALTER PROCEDURE Inventory.usp_GetProductByID35 @ProductID INT36AS37BEGIN38 SET NOCOUNT ON;39 40 SELECT 41 p.*,42 c.CategoryName43 FROM Inventory.Product p44 JOIN Inventory.Category c ON p.CategoryID = c.CategoryID45 WHERE p.ProductID = @ProductID;46END;47GO4849-- 3. CREATE50CREATE OR ALTER PROCEDURE Inventory.usp_CreateProduct51 @SKU VARCHAR(20),52 @ProductName NVARCHAR(200),53 @Description NVARCHAR(MAX) = NULL,54 @CategoryID INT,55 @UnitPrice DECIMAL(19,4),56 @CostPrice DECIMAL(19,4) = NULL,57 @StockQuantity INT = 0,58 @NewProductID INT OUTPUT59AS60BEGIN61 SET NOCOUNT ON;62 63 -- Validate SKU unique64 IF EXISTS (SELECT 1 FROM Inventory.Product WHERE SKU = @SKU)65 BEGIN66 RAISERROR('SKU already exists', 16, 1);67 RETURN -1;68 END69 70 -- Validate category exists71 IF NOT EXISTS (SELECT 1 FROM Inventory.Category WHERE CategoryID = @CategoryID)72 BEGIN73 RAISERROR('Category not found', 16, 1);74 RETURN -2;75 END76 77 INSERT INTO Inventory.Product (SKU, ProductName, Description, CategoryID, UnitPrice, CostPrice, StockQuantity)78 VALUES (@SKU, @ProductName, @Description, @CategoryID, @UnitPrice, @CostPrice, @StockQuantity);79 80 SET @NewProductID = SCOPE_IDENTITY();81 RETURN 0;82END;83GO8485-- 4. UPDATE86CREATE OR ALTER PROCEDURE Inventory.usp_UpdateProduct87 @ProductID INT,88 @ProductName NVARCHAR(200),89 @Description NVARCHAR(MAX) = NULL,90 @CategoryID INT,91 @UnitPrice DECIMAL(19,4),92 @CostPrice DECIMAL(19,4) = NULL,93 @IsActive BIT = 194AS95BEGIN96 SET NOCOUNT ON;97 98 -- Check exists99 IF NOT EXISTS (SELECT 1 FROM Inventory.Product WHERE ProductID = @ProductID)100 BEGIN101 RAISERROR('Product not found', 16, 1);102 RETURN -1;103 END104 105 UPDATE Inventory.Product106 SET ProductName = @ProductName,107 Description = @Description,108 CategoryID = @CategoryID,109 UnitPrice = @UnitPrice,110 CostPrice = @CostPrice,111 IsActive = @IsActive,112 ModifiedDate = SYSDATETIME()113 WHERE ProductID = @ProductID;114 115 RETURN 0;116END;117GO118119-- 5. DELETE (soft delete)120CREATE OR ALTER PROCEDURE Inventory.usp_DeleteProduct121 @ProductID INT122AS123BEGIN124 SET NOCOUNT ON;125 126 -- Check exists127 IF NOT EXISTS (SELECT 1 FROM Inventory.Product WHERE ProductID = @ProductID)128 BEGIN129 RAISERROR('Product not found', 16, 1);130 RETURN -1;131 END132 133 -- Soft delete134 UPDATE Inventory.Product135 SET IsActive = 0,136 ModifiedDate = SYSDATETIME()137 WHERE ProductID = @ProductID;138 139 RETURN 0;140END;141GOTiếp theo
Bài tiếp theo: Indexes - Tối ưu hiệu năng - học cách tạo và quản lý indexes để tăng tốc queries!
