Stored Procedures
Stored Procedure (SP) là một tập hợp các câu lệnh T-SQL được biên dịch, đặt tên và lưu trữ trong database. SP là công cụ lập trình quan trọng nhất trong SQL Server, cho phép đóng gói business logic, tăng bảo mật và cải thiện hiệu năng.
1. Lợi ích của Stored Procedures
| Lợi ích | Giải thích |
|---|---|
| Code Reuse | Viết một lần, gọi từ nhiều nơi |
| Security | Cấp quyền EXECUTE thay vì trực tiếp trên tables |
| Performance | Execution plan được compile và cache |
| Reduced Network Traffic | Chỉ truyền tên SP + tham số |
| Maintainability | Thay đổi logic ở một chỗ |
| Abstraction | Ẩn schema chi tiết khỏi application |
Execution Plan Caching
-- Lần đầu: SQL Server compile SP → lưu plan vào cache
EXEC dbo.GetCustomerOrders @CustomerId = 1;
-- Những lần sau: reuse plan từ cache (nhanh hơn)
EXEC dbo.GetCustomerOrders @CustomerId = 2;
-- Xem execution plan trong cache
SELECT
cp.usecounts,
cp.objtype,
qt.text
FROM sys.dm_exec_cached_plans cp
CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) qt
WHERE qt.text LIKE '%GetCustomerOrders%';
2. Tạo và Gọi Stored Procedure
CREATE PROCEDURE
-- Cú pháp cơ bản
CREATE PROCEDURE dbo.GetActiveOrders
AS
BEGIN
SET NOCOUNT ON; -- Tắt "X rows affected", giảm network traffic
SELECT
OrderId,
CustomerId,
OrderDate,
TotalAmount
FROM Orders
WHERE Status = 'Active'
ORDER BY OrderDate DESC;
END;
GO
ALTER PROCEDURE và DROP PROCEDURE
-- Thay đổi SP (giữ nguyên permissions)
ALTER PROCEDURE dbo.GetActiveOrders
AS
BEGIN
SET NOCOUNT ON;
SELECT * FROM Orders WHERE Status = 'Active' AND IsDeleted = 0;
END;
GO
-- Xóa SP
DROP PROCEDURE IF EXISTS dbo.GetActiveOrders; -- SQL 2016+
-- hoặc
IF OBJECT_ID('dbo.GetActiveOrders', 'P') IS NOT NULL
DROP PROCEDURE dbo.GetActiveOrders;
Input Parameters
CREATE PROCEDURE dbo.GetOrdersByCustomer
@CustomerId INT,
@Status NVARCHAR(50),
@StartDate DATE = NULL, -- Tham số có giá trị mặc định
@EndDate DATE = NULL,
@MaxRows INT = 100
AS
BEGIN
SET NOCOUNT ON;
SELECT TOP (@MaxRows)
o.OrderId,
o.OrderDate,
o.TotalAmount,
c.CustomerName
FROM Orders o
INNER JOIN Customers c ON o.CustomerId = c.CustomerId
WHERE
o.CustomerId = @CustomerId
AND o.Status = @Status
AND (@StartDate IS NULL OR o.OrderDate >= @StartDate)
AND (@EndDate IS NULL OR o.OrderDate <= @EndDate)
ORDER BY o.OrderDate DESC;
END;
GO
-- Gọi SP với named parameters (khuyến cáo)
EXEC dbo.GetOrdersByCustomer
@CustomerId = 1,
@Status = 'Shipped',
@StartDate = '2025-01-01';
-- Gọi với positional parameters (không khuyến cáo)
EXEC dbo.GetOrdersByCustomer 1, 'Shipped';
OUTPUT Parameters
CREATE PROCEDURE dbo.CreateOrder
@CustomerId INT,
@TotalAmount DECIMAL(18,2),
@OrderId INT OUTPUT, -- OUTPUT parameter
@ErrorMessage NVARCHAR(500) OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SET @ErrorMessage = NULL;
IF @TotalAmount <= 0
BEGIN
SET @ErrorMessage = 'TotalAmount phải lớn hơn 0';
SET @OrderId = -1;
RETURN -1; -- Return code
END;
INSERT INTO Orders (CustomerId, TotalAmount, OrderDate, Status)
VALUES (@CustomerId, @TotalAmount, GETDATE(), 'Pending');
SET @OrderId = SCOPE_IDENTITY(); -- Lấy ID vừa insert
RETURN 0; -- Thành công
END;
GO
-- Gọi SP với OUTPUT parameters
DECLARE @NewOrderId INT;
DECLARE @ErrMsg NVARCHAR(500);
DECLARE @ReturnCode INT;
EXEC @ReturnCode = dbo.CreateOrder
@CustomerId = 5,
@TotalAmount = 299.99,
@OrderId = @NewOrderId OUTPUT,
@ErrorMessage = @ErrMsg OUTPUT;
IF @ReturnCode = 0
PRINT 'Order created: ' + CAST(@NewOrderId AS VARCHAR);
ELSE
PRINT 'Error: ' + @ErrMsg;
EXEC vs EXECUTE
-- Hai cú pháp tương đương
EXEC dbo.GetActiveOrders;
EXECUTE dbo.GetActiveOrders;
-- EXEC cũng có thể chạy dynamic string
EXEC ('SELECT GETDATE()');
3. Control Flow
IF/ELSE
CREATE PROCEDURE dbo.ProcessPayment
@OrderId INT,
@Amount DECIMAL(18,2)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @CurrentBalance DECIMAL(18,2);
SELECT @CurrentBalance = Balance
FROM CustomerAccounts
WHERE CustomerId = (SELECT CustomerId FROM Orders WHERE OrderId = @OrderId);
IF @CurrentBalance >= @Amount
BEGIN
-- Block code khi điều kiện đúng
UPDATE CustomerAccounts
SET Balance = Balance - @Amount
WHERE CustomerId = (SELECT CustomerId FROM Orders WHERE OrderId = @OrderId);
UPDATE Orders SET Status = 'Paid' WHERE OrderId = @OrderId;
PRINT 'Thanh toán thành công';
END
ELSE IF @CurrentBalance > 0
BEGIN
PRINT 'Số dư không đủ. Còn: ' + CAST(@CurrentBalance AS VARCHAR(20));
END
ELSE
BEGIN
PRINT 'Tài khoản không có số dư';
END;
END;
WHILE, BREAK, CONTINUE
CREATE PROCEDURE dbo.GenerateMonthlySummary
@Year INT
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Month INT = 1;
WHILE @Month <= 12
BEGIN
-- CONTINUE: bỏ qua tháng nếu không có dữ liệu
IF NOT EXISTS (SELECT 1 FROM Orders
WHERE YEAR(OrderDate) = @Year
AND MONTH(OrderDate) = @Month)
BEGIN
SET @Month = @Month + 1;
CONTINUE;
END;
-- Xử lý tháng hiện tại
INSERT INTO MonthlySummary (Year, Month, TotalOrders, TotalRevenue)
SELECT
@Year,
@Month,
COUNT(*),
SUM(TotalAmount)
FROM Orders
WHERE YEAR(OrderDate) = @Year AND MONTH(OrderDate) = @Month;
-- BREAK: dừng vòng lặp nếu đủ 6 tháng đầu
IF @Month = 6
BEGIN
BREAK;
END;
SET @Month = @Month + 1;
END;
END;
RETURN
CREATE PROCEDURE dbo.ValidateCustomer
@CustomerId INT
AS
BEGIN
IF @CustomerId <= 0
BEGIN
RETURN -1; -- Return code âm = lỗi
END;
IF NOT EXISTS (SELECT 1 FROM Customers WHERE CustomerId = @CustomerId AND IsActive = 1)
BEGIN
RETURN -2; -- Customer không tồn tại hoặc inactive
END;
-- Tiếp tục xử lý...
RETURN 0; -- Thành công
END;
GO
DECLARE @Result INT;
EXEC @Result = dbo.ValidateCustomer @CustomerId = 5;
IF @Result <> 0
PRINT 'Validation failed with code: ' + CAST(@Result AS VARCHAR);
GOTO (Tại sao nên tránh)
-- GOTO làm code khó đọc, khó maintain - NÊN TRÁNH
CREATE PROCEDURE dbo.ProcessWithGoto
AS
BEGIN
DECLARE @Counter INT = 0;
StartLoop:
IF @Counter >= 10 GOTO EndLoop;
SET @Counter = @Counter + 1;
IF @Counter = 5 GOTO SkipFive;
PRINT @Counter;
SkipFive:
GOTO StartLoop;
EndLoop:
PRINT 'Done';
END;
-- THAY THẾ BẰNG: WHILE với CONTINUE là rõ ràng hơn
4. Variables
CREATE PROCEDURE dbo.CalculateOrderStats
@CustomerId INT
AS
BEGIN
SET NOCOUNT ON;
-- DECLARE nhiều biến
DECLARE
@TotalOrders INT,
@TotalSpent DECIMAL(18,2),
@AverageOrder DECIMAL(18,2),
@CustomerName NVARCHAR(200);
-- SET: gán một giá trị cụ thể
SET @CustomerName = (
SELECT CustomerName
FROM Customers
WHERE CustomerId = @CustomerId
);
-- SELECT: gán từ query (linh hoạt hơn cho multiple columns)
SELECT
@TotalOrders = COUNT(*),
@TotalSpent = SUM(TotalAmount)
FROM Orders
WHERE CustomerId = @CustomerId AND Status = 'Completed';
-- Tính toán
SET @AverageOrder = CASE
WHEN @TotalOrders > 0 THEN @TotalSpent / @TotalOrders
ELSE 0
END;
-- Kết quả
SELECT
@CustomerName AS CustomerName,
@TotalOrders AS TotalOrders,
@TotalSpent AS TotalSpent,
@AverageOrder AS AverageOrderValue;
END;
Lưu ý quan trọng: Nếu
SELECT @Variable = column FROM table WHERE conditionkhông tìm thấy row nào, biến giữ nguyên giá trị trước đó (không về NULL). Đây là một nguy cơ bug phổ biến!
5. Temp Tables trong Stored Procedures
Local Temp Table (#) vs Global Temp Table (##)
CREATE PROCEDURE dbo.ProcessLargeDataset
@BatchSize INT = 1000
AS
BEGIN
SET NOCOUNT ON;
-- LOCAL temp table: chỉ visible trong session hiện tại
-- Tự động drop khi SP kết thúc
CREATE TABLE #TempOrders (
OrderId INT,
CustomerId INT,
TotalAmount DECIMAL(18,2),
INDEX IX_Temp_CustomerId (CustomerId) -- Có thể thêm index!
);
INSERT INTO #TempOrders (OrderId, CustomerId, TotalAmount)
SELECT OrderId, CustomerId, TotalAmount
FROM Orders
WHERE Status = 'Pending' AND ProcessedFlag = 0;
-- GLOBAL temp table: visible cho tất cả sessions
-- Dùng khi cần share data giữa các sessions/connections
-- CREATE TABLE ##GlobalTemp (...) -- Hiếm khi dùng
-- Xử lý từng batch
WHILE EXISTS (SELECT 1 FROM #TempOrders WHERE ProcessedFlag IS NULL)
BEGIN
UPDATE TOP (@BatchSize) #TempOrders
SET ProcessedFlag = 1
OUTPUT inserted.OrderId, inserted.TotalAmount
INTO OrderProcessingLog (OrderId, Amount)
WHERE ProcessedFlag IS NULL;
END;
-- Explicit drop (optional, sẽ tự drop khi SP kết thúc)
DROP TABLE IF EXISTS #TempOrders;
END;
Temp Table vs Table Variable
-- Table Variable: nhỏ, no statistics, ít log overhead
DECLARE @OrderSummary TABLE (
CustomerId INT,
OrderCount INT,
TotalAmount DECIMAL(18,2)
);
INSERT INTO @OrderSummary
SELECT CustomerId, COUNT(*), SUM(TotalAmount)
FROM Orders
GROUP BY CustomerId;
-- Temp Table: lớn, có statistics, optimizer dùng được
CREATE TABLE #OrderSummary (
CustomerId INT PRIMARY KEY,
OrderCount INT,
TotalAmount DECIMAL(18,2)
);
-- Dùng Temp Table khi:
-- 1. Dataset lớn (> vài nghìn rows)
-- 2. Cần index phức tạp
-- 3. Cần thống kê (statistics) để optimizer plan tốt
-- 4. Reuse trong nhiều queries
6. Error Handling
TRY…CATCH
CREATE PROCEDURE dbo.TransferFunds
@FromAccountId INT,
@ToAccountId INT,
@Amount DECIMAL(18,2)
AS
BEGIN
SET NOCOUNT ON;
IF @Amount <= 0
THROW 50001, 'Số tiền chuyển phải lớn hơn 0', 1;
BEGIN TRY
BEGIN TRANSACTION;
-- Kiểm tra số dư
DECLARE @Balance DECIMAL(18,2);
SELECT @Balance = Balance FROM Accounts WHERE AccountId = @FromAccountId;
IF @Balance < @Amount
THROW 50002, 'Số dư không đủ để thực hiện giao dịch', 1;
-- Thực hiện transfer
UPDATE Accounts SET Balance = Balance - @Amount WHERE AccountId = @FromAccountId;
UPDATE Accounts SET Balance = Balance + @Amount WHERE AccountId = @ToAccountId;
-- Ghi log
INSERT INTO TransactionLog (FromAccount, ToAccount, Amount, TransactionDate)
VALUES (@FromAccountId, @ToAccountId, @Amount, GETDATEUTCDATE());
COMMIT TRANSACTION;
PRINT 'Transfer thành công';
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
-- Lấy thông tin lỗi
DECLARE
@ErrorNumber INT = ERROR_NUMBER(),
@ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE(),
@ErrorSeverity INT = ERROR_SEVERITY(),
@ErrorState INT = ERROR_STATE(),
@ErrorLine INT = ERROR_LINE(),
@ErrorProc NVARCHAR(200) = ERROR_PROCEDURE();
-- Ghi log lỗi
INSERT INTO ErrorLog (ErrorNumber, ErrorMessage, ErrorProcedure, ErrorLine, CreatedAt)
VALUES (@ErrorNumber, @ErrorMessage, @ErrorProc, @ErrorLine, GETDATE());
-- Re-throw lỗi cho caller
THROW;
END CATCH;
END;
ERROR_ Functions
-- Các function lấy thông tin lỗi trong CATCH block
BEGIN CATCH
SELECT
ERROR_NUMBER() AS ErrorNumber, -- Số hiệu lỗi
ERROR_MESSAGE() AS ErrorMessage, -- Thông điệp lỗi
ERROR_SEVERITY() AS ErrorSeverity, -- Mức độ nghiêm trọng (1-25)
ERROR_STATE() AS ErrorState, -- State code
ERROR_LINE() AS ErrorLine, -- Dòng code gây lỗi
ERROR_PROCEDURE() AS ErrorProcedure; -- Tên SP/trigger gây lỗi
END CATCH;
RAISERROR vs THROW
-- RAISERROR (cũ, nhưng vẫn dùng)
RAISERROR('Lỗi xử lý đơn hàng %d', 16, 1, @OrderId);
-- %d = int placeholder, %s = string placeholder
-- THROW (SQL 2012+, khuyến cáo dùng)
THROW 50001, 'Lỗi xử lý đơn hàng', 1;
-- Re-throw trong CATCH (không tham số)
BEGIN CATCH
ROLLBACK TRANSACTION;
THROW; -- Giữ nguyên error number, message, state gốc
END CATCH;
7. Nested Stored Procedures
-- SP cấp 1: Outer
CREATE PROCEDURE dbo.ProcessDailyBatch
AS
BEGIN
SET NOCOUNT ON;
PRINT 'NESTLEVEL: ' + CAST(@@NESTLEVEL AS VARCHAR); -- 1
-- Gọi SP khác
EXEC dbo.ValidateOrders;
EXEC dbo.UpdateInventory;
EXEC dbo.SendNotifications;
END;
-- SP cấp 2: Inner
CREATE PROCEDURE dbo.ValidateOrders
AS
BEGIN
SET NOCOUNT ON;
PRINT 'NESTLEVEL: ' + CAST(@@NESTLEVEL AS VARCHAR); -- 2
EXEC dbo.CheckOrderRules; -- Level 3
END;
-- Giới hạn: tối đa 32 levels
-- @@NESTLEVEL = 0 khi gọi trực tiếp từ client
8. Transactions trong Stored Procedures
CREATE PROCEDURE dbo.PlaceOrder
@CustomerId INT,
@Items OrderItemType READONLY, -- Table-valued parameter
@OrderId INT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON; -- Tự động rollback khi có lỗi runtime
BEGIN TRY
BEGIN TRANSACTION;
-- Tạo Order
INSERT INTO Orders (CustomerId, OrderDate, Status)
VALUES (@CustomerId, GETDATE(), 'Pending');
SET @OrderId = SCOPE_IDENTITY();
-- Insert Order Items
INSERT INTO OrderItems (OrderId, ProductId, Quantity, UnitPrice)
SELECT @OrderId, ProductId, Quantity, UnitPrice
FROM @Items;
-- Cập nhật stock
UPDATE p
SET p.Stock = p.Stock - i.Quantity
FROM Products p
INNER JOIN @Items i ON p.ProductId = i.ProductId;
-- Kiểm tra stock âm
IF EXISTS (SELECT 1 FROM Products p INNER JOIN @Items i ON p.ProductId = i.ProductId WHERE p.Stock < 0)
THROW 50003, 'Sản phẩm không đủ hàng trong kho', 1;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
THROW;
END CATCH;
END;
@@TRANCOUNT và SAVE TRANSACTION
CREATE PROCEDURE dbo.UpdateWithSavepoint
AS
BEGIN
-- @@TRANCOUNT: số transaction đang active
PRINT '@@TRANCOUNT trước: ' + CAST(@@TRANCOUNT AS VARCHAR); -- 0
BEGIN TRANSACTION; -- @@TRANCOUNT = 1
INSERT INTO AuditLog (Event) VALUES ('Process started');
SAVE TRANSACTION MySavepoint; -- Tạo savepoint
BEGIN TRY
-- Thao tác có thể lỗi
INSERT INTO Orders (CustomerId, TotalAmount) VALUES (999999, -100);
END TRY
BEGIN CATCH
-- Rollback về savepoint, giữ lại INSERT AuditLog
ROLLBACK TRANSACTION MySavepoint;
INSERT INTO AuditLog (Event) VALUES ('Order insert failed: ' + ERROR_MESSAGE());
END CATCH;
COMMIT TRANSACTION; -- Commit AuditLog inserts
END;
9. Dynamic SQL
sp_executesql (Khuyến cáo)
CREATE PROCEDURE dbo.SearchProducts
@SearchTerm NVARCHAR(100),
@CategoryId INT = NULL,
@SortColumn NVARCHAR(50) = 'ProductName',
@SortDirection NVARCHAR(4) = 'ASC'
AS
BEGIN
SET NOCOUNT ON;
-- Whitelist cho sort columns (ngăn SQL injection)
IF @SortColumn NOT IN ('ProductName', 'Price', 'CreatedDate', 'Stock')
SET @SortColumn = 'ProductName';
IF @SortDirection NOT IN ('ASC', 'DESC')
SET @SortDirection = 'ASC';
DECLARE @SQL NVARCHAR(MAX);
DECLARE @Params NVARCHAR(500);
SET @SQL = N'
SELECT ProductId, ProductName, Price, CategoryId
FROM Products
WHERE
(ProductName LIKE @SearchTerm OR Description LIKE @SearchTerm)
AND (@CategoryId IS NULL OR CategoryId = @CategoryId)
AND IsActive = 1
ORDER BY ' + QUOTENAME(@SortColumn) + ' ' + @SortDirection; -- QUOTENAME bảo vệ tên column
SET @Params = N'@SearchTerm NVARCHAR(100), @CategoryId INT';
EXEC sp_executesql
@SQL,
@Params,
@SearchTerm = '%' + @SearchTerm + '%',
@CategoryId = @CategoryId;
END;
Nguy hiểm SQL Injection với EXEC()
-- ĐỪNG làm thế này - dễ bị SQL injection
DECLARE @SQL NVARCHAR(MAX);
SET @SQL = 'SELECT * FROM Products WHERE Name = ''' + @UserInput + '''';
EXEC (@SQL);
-- Nếu @UserInput = "'; DROP TABLE Products; --" thì GG!
-- ĐÚNG: Dùng sp_executesql với parameters
SET @SQL = N'SELECT * FROM Products WHERE Name = @Name';
EXEC sp_executesql @SQL, N'@Name NVARCHAR(200)', @Name = @UserInput;
10. Recompilation và Parameter Sniffing
Vấn đề Parameter Sniffing
-- SP này có vấn đề parameter sniffing
CREATE PROCEDURE dbo.GetOrders @CustomerId INT
AS
SELECT * FROM Orders WHERE CustomerId = @CustomerId;
-- Nếu compile với CustomerId = 1 (ít orders) → Index Seek plan
-- Khi gọi với CustomerId = 999 (nhiều orders) → vẫn dùng Seek plan → chậm!
Các giải pháp
-- Giải pháp 1: OPTION (RECOMPILE) trên query
-- Recompile query này mỗi lần, tốt cho queries không consistent
CREATE PROCEDURE dbo.GetOrders @CustomerId INT
AS
SELECT * FROM Orders
WHERE CustomerId = @CustomerId
OPTION (RECOMPILE); -- Recompile mỗi lần, overhead nhỏ
-- Giải pháp 2: WITH RECOMPILE trên SP
-- Recompile toàn bộ SP mỗi lần
CREATE PROCEDURE dbo.GetOrders @CustomerId INT
WITH RECOMPILE -- Không cache plan
AS
SELECT * FROM Orders WHERE CustomerId = @CustomerId;
-- Giải pháp 3: Local variable (ngăn sniffing)
-- Optimizer không biết giá trị → dùng statistics thay vì parameter value
CREATE PROCEDURE dbo.GetOrders @CustomerId INT
AS
DECLARE @LocalId INT = @CustomerId;
SELECT * FROM Orders WHERE CustomerId = @LocalId;
-- Giải pháp 4: OPTIMIZE FOR hint
CREATE PROCEDURE dbo.GetOrders @CustomerId INT
AS
SELECT * FROM Orders
WHERE CustomerId = @CustomerId
OPTION (OPTIMIZE FOR (@CustomerId = 100)); -- Optimize cho giá trị điển hình
-- Giải pháp 5: OPTIMIZE FOR UNKNOWN
CREATE PROCEDURE dbo.GetOrders @CustomerId INT
AS
SELECT * FROM Orders
WHERE CustomerId = @CustomerId
OPTION (OPTIMIZE FOR (@CustomerId UNKNOWN)); -- Dùng statistics thay vì parameter value
sp_recompile
-- Force recompile SP lần sau khi gọi
EXEC sp_recompile 'dbo.GetOrders';
-- Force recompile tất cả SPs dùng bảng này
EXEC sp_recompile 'dbo.Orders';
11. System Stored Procedures
-- Thông tin về object
EXEC sp_help 'Orders'; -- Thông tin table/view
EXEC sp_helpindex 'Orders'; -- Danh sách indexes
EXEC sp_helpconstraint 'Orders'; -- Constraints
-- Monitoring
EXEC sp_who2; -- Các connections hiện tại
EXEC sp_lock; -- Locks đang active
-- Dynamic SQL (đã nói ở trên)
EXEC sp_executesql @SQL, @Params;
-- Thống kê
EXEC sp_updatestats; -- Update tất cả statistics
-- Metadata
SELECT
name,
OBJECT_DEFINITION(object_id) AS Definition,
create_date,
modify_date
FROM sys.procedures
WHERE name LIKE 'GetOrder%';
-- Tìm SP nào reference đến một table
SELECT DISTINCT
o.name AS ProcedureName
FROM sys.sql_modules m
INNER JOIN sys.objects o ON m.object_id = o.object_id
WHERE
o.type = 'P'
AND m.definition LIKE '%Orders%';
Best Practices Tóm Tắt
-- Template cho một Stored Procedure chuẩn
CREATE PROCEDURE dbo.MyProcedure
@Param1 INT,
@Param2 NVARCHAR(100) = NULL
AS
BEGIN
SET NOCOUNT ON; -- Tắt "X rows affected"
SET XACT_ABORT ON; -- Auto rollback khi lỗi
-- Validate inputs
IF @Param1 IS NULL OR @Param1 <= 0
THROW 50001, 'Param1 không hợp lệ', 1;
BEGIN TRY
BEGIN TRANSACTION;
-- Business logic ở đây
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
-- Log error
INSERT INTO dbo.ErrorLog (ErrorMessage, ErrorProcedure, ErrorLine, CreatedAt)
VALUES (ERROR_MESSAGE(), ERROR_PROCEDURE(), ERROR_LINE(), GETDATE());
THROW; -- Re-throw cho caller
END CATCH;
END;
GO
| Best Practice | Lý do |
|---|---|
SET NOCOUNT ON | Giảm network traffic |
SET XACT_ABORT ON | Auto rollback an toàn |
| Validate parameters đầu vào | Fail fast với thông báo rõ ràng |
Dùng schema prefix (dbo.) | Tránh resolution ambiguity |
Dùng sp_executesql cho dynamic SQL | Ngăn SQL injection, plan reuse |
Tránh SELECT * | Explicit columns tốt hơn |
Dùng THROW thay RAISERROR | Modern, giữ context tốt hơn |
| Comment business logic phức tạp | Maintainability |