Stored Procedures trong SQL
Vấn đề: SQL Lặp đi Lặp lại trong Ứng dụng
Một ứng dụng xử lý đơn hàng: kiểm tra tồn kho, trừ số lượng, tạo đơn hàng, ghi log — cùng 120 dòng SQL chạy ở mọi API endpoint, batch job và mobile app. Thay đổi phải áp dụng ở mọi nơi, không nhất quán.
Stored procedures giải quyết bằng cách lưu logic SQL bên trong database. Ứng dụng chỉ gọi một tên, database thực thi logic đã lưu sẵn.
Stored Procedure là gì?
Stored procedure là một khối mã SQL được đặt tên, lưu trữ trong database, có thể thực thi theo yêu cầu. Nó nhận tham số đầu vào/đầu ra và chứa logic điều khiển luồng (Control Flow). Đặc điểm chính:
- Được biên dịch trước (Pre-compiled) — kế hoạch thực thi (Execution Plan) được cache và tái sử dụng
- Có tham số (Parameterized) — nhận tham số đầu vào và đầu ra
- Đóng gói (Encapsulated) — ẩn chi tiết triển khai khỏi phía gọi
- Giao dịch (Transactional) — có thể bao nhiều câu lệnh trong một giao dịch (Transaction) duy nhất
Cú pháp Cơ bản
Ví dụ sử dụng cú pháp SQL Server. Khác biệt chính của PostgreSQL/MySQL được ghi chú.
Tạo stored procedure
-- SQL Server
CREATE PROCEDURE GetEmployeesByDepartment
@DeptId INT
AS
BEGIN
SELECT id, name, salary
FROM employees
WHERE dept_id = @DeptId
ORDER BY name;
END;
-- PostgreSQL (sử dụng FUNCTION, không phải PROCEDURE, để trả về giá trị)
CREATE OR REPLACE FUNCTION GetEmployeesByDepartment(dept_id INT)
RETURNS TABLE(id INT, name VARCHAR, salary DECIMAL)
LANGUAGE sql AS $$
SELECT id, name, salary FROM employees
WHERE dept_id = $1 ORDER BY name;
$$;
-- MySQL
DELIMITER //
CREATE PROCEDURE GetEmployeesByDepartment(IN dept_id INT)
BEGIN
SELECT id, name, salary FROM employees
WHERE dept_id = dept_id ORDER BY name;
END //
DELIMITER ;
Thực thi, sửa đổi và xóa
-- Thực thi
EXEC GetEmployeesByDepartment @DeptId = 10; -- SQL Server
SELECT * FROM GetEmployeesByDepartment(10); -- PostgreSQL
CALL GetEmployeesByDepartment(10); -- MySQL
-- Sửa đổi
ALTER PROCEDURE GetEmployeesByDepartment
@DeptId INT
AS
BEGIN
SELECT id, name, salary, hire_date
FROM employees WHERE dept_id = @DeptId
ORDER BY hire_date DESC;
END;
-- Xóa
DROP PROCEDURE GetEmployeesByDepartment; -- SQL Server / MySQL
DROP FUNCTION GetEmployeesByDepartment; -- PostgreSQL
Tham số (Parameters)
Tham số đầu vào với bộ lọc tùy chọn
CREATE PROCEDURE SearchEmployees
@NameFilter NVARCHAR(100) = NULL,
@MinSalary DECIMAL(18,2) = 0,
@DeptId INT = NULL
AS
BEGIN
SELECT id, name, salary, dept_id
FROM employees
WHERE (@NameFilter IS NULL OR name LIKE '%' + @NameFilter + '%')
AND salary >= @MinSalary
AND (@DeptId IS NULL OR dept_id = @DeptId)
ORDER BY salary DESC;
END;
EXEC SearchEmployees @NameFilter = 'Alice', @MinSalary = 50000, @DeptId = 10;
EXEC SearchEmployees @DeptId = 10; -- mặc định: NameFilter=NULL, MinSalary=0
EXEC SearchEmployees; -- trả về tất cả nhân viên
Sử dụng giá trị mặc định NULL cho tham số tùy chọn. Mẫu WHERE (@Param IS NULL OR column = @Param) giúp bộ lọc trở nên tùy chọn — khi truyền NULL, điều kiện đó bị bỏ qua.
Tham số đầu ra (Output Parameters)
CREATE PROCEDURE GetDepartmentStats
@DeptId INT,
@EmployeeCount INT OUTPUT,
@AvgSalary DECIMAL(18,2) OUTPUT
AS
BEGIN
SELECT @EmployeeCount = COUNT(*),
@AvgSalary = AVG(salary)
FROM employees WHERE dept_id = @DeptId;
END;
DECLARE @Count INT, @Avg DECIMAL(18,2);
EXEC GetDepartmentStats
@DeptId = 10,
@EmployeeCount = @Count OUTPUT,
@AvgSalary = @Avg OUTPUT;
SELECT @Count AS employee_count, @Avg AS avg_salary;
Tham số giá trị bảng (Table-Valued Parameters) (SQL Server)
Truyền nhiều dòng dưới dạng một tham số duy nhất cho thao tác hàng loạt:
-- 1. Tạo kiểu bảng (Table Type)
CREATE TYPE EmployeeTableType AS TABLE (
name NVARCHAR(100),
dept_id INT,
salary DECIMAL(18,2)
);
-- 2. Sử dụng làm tham số
CREATE PROCEDURE BulkInsertEmployees
@Employees EmployeeTableType READONLY
AS
BEGIN
INSERT INTO employees (name, dept_id, salary)
SELECT name, dept_id, salary FROM @Employees;
END;
-- 3. Gọi
DECLARE @NewEmployees AS EmployeeTableType;
INSERT INTO @NewEmployees VALUES
('Alice', 10, 75000), ('Bob', 20, 65000), ('Carol', 30, 80000);
EXEC BulkInsertEmployees @Employees = @NewEmployees;
Luồng Điều khiển (Control Flow)
Stored procedures hỗ trợ logic thủ tục bên trong database.
Biến và Điều kiện
CREATE PROCEDURE ApplyRaise
@EmployeeId INT,
@RaisePercent DECIMAL(5,2)
AS
BEGIN
DECLARE @CurrentSalary DECIMAL(18,2);
DECLARE @NewSalary DECIMAL(18,2);
DECLARE @MaxSalary DECIMAL(18,2) = 200000;
SELECT @CurrentSalary = salary FROM employees WHERE id = @EmployeeId;
SET @NewSalary = @CurrentSalary * (1 + @RaisePercent / 100);
IF @NewSalary > @MaxSalary
BEGIN
SET @NewSalary = @MaxSalary;
PRINT 'Salary capped at maximum';
END
UPDATE employees SET salary = @NewSalary WHERE id = @EmployeeId;
SELECT @CurrentSalary AS old_salary, @NewSalary AS new_salary;
END;
Vòng lặp
CREATE PROCEDURE GenerateMonthlyReport @Year INT
AS
BEGIN
DECLARE @Month INT = 1;
CREATE TABLE #MonthlyStats (
month INT, order_count INT, total_revenue DECIMAL(18,2)
);
WHILE @Month <= 12
BEGIN
INSERT INTO #MonthlyStats (month, order_count, total_revenue)
SELECT @Month, COUNT(*), ISNULL(SUM(amount), 0)
FROM orders
WHERE YEAR(order_date) = @Year AND MONTH(order_date) = @Month;
SET @Month = @Month + 1;
END
SELECT * FROM #MonthlyStats ORDER BY month;
DROP TABLE #MonthlyStats;
END;
Xử lý Lỗi (Error Handling)
TRY...CATCH (SQL Server)
CREATE PROCEDURE TransferFunds
@FromAccountId INT,
@ToAccountId INT,
@Amount DECIMAL(18,2)
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION;
IF @Amount <= 0
RAISERROR('Transfer amount must be positive', 16, 1);
DECLARE @Balance DECIMAL(18,2);
SELECT @Balance = balance FROM accounts WHERE id = @FromAccountId;
IF @Balance < @Amount
RAISERROR('Insufficient funds: balance is %s', 16, 1, CAST(@Balance AS VARCHAR(20)));
UPDATE accounts SET balance = balance - @Amount WHERE id = @FromAccountId;
UPDATE accounts SET balance = balance + @Amount WHERE id = @ToAccountId;
INSERT INTO transfer_log (from_account, to_account, amount, transfer_date)
VALUES (@FromAccountId, @ToAccountId, @Amount, GETDATE());
COMMIT TRANSACTION;
SELECT 'Transfer successful' AS result;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
SELECT
ERROR_NUMBER() AS error_number,
ERROR_MESSAGE() AS error_message,
ERROR_SEVERITY() AS error_severity,
ERROR_PROCEDURE() AS error_procedure,
ERROR_LINE() AS error_line;
END CATCH
END;
Xử lý EXCEPTION (PostgreSQL)
CREATE OR REPLACE FUNCTION TransferFunds(
from_account_id INT, to_account_id INT, amount DECIMAL
)
RETURNS TEXT LANGUAGE plpgsql AS $$
DECLARE balance DECIMAL;
BEGIN
IF amount <= 0 THEN
RAISE EXCEPTION 'Transfer amount must be positive';
END IF;
SELECT balance INTO balance FROM accounts WHERE id = from_account_id;
IF balance < amount THEN
RAISE EXCEPTION 'Insufficient funds: balance is %', balance;
END IF;
UPDATE accounts SET balance = balance - amount WHERE id = from_account_id;
UPDATE accounts SET balance = balance + amount WHERE id = to_account_id;
INSERT INTO transfer_log (from_account, to_account, amount, transfer_date)
VALUES (from_account_id, to_account_id, amount, NOW());
RETURN 'Transfer successful';
EXCEPTION
WHEN OTHERS THEN
RETURN 'Transfer failed: ' || SQLERRM;
END;
$$;
Stored Procedures so với Hàm do Người dùng Định nghĩa (User-Defined Functions)
Cả hai đều là database object được đặt tên, nhưng hành vi rất khác nhau:
| Stored Procedure | Scalar Function (Hàm vô hướng) | Table-Valued Function (Hàm trả về bảng) | |
|---|---|---|---|
| Giá trị trả về | Tùy chọn (0+ result set) | Một giá trị vô hướng duy nhất | Một bảng |
| Sử dụng | Chỉ EXEC / CALL | Trong SELECT, WHERE | Trong mệnh đề FROM |
| Sửa đổi dữ liệu | Có (INSERT, UPDATE, DELETE) | Không — chỉ đọc | Không — chỉ đọc |
| Giao dịch | Có thể BEGIN/COMMIT/ROLLBACK | Không thể | Không thể |
| Xử lý lỗi | TRY/CATCH đầy đủ | Hạn chế | Hạn chế |
| Side effect | Được phép | Không được phép | Không được phép |
Scalar functions trong SELECT hoặc WHERE thực thi một lần cho mỗi dòng. Trên bảng 1M dòng, hàm được gọi 1M lần — nguyên nhân giảm hiệu năng nghiêm trọng. Sử dụng inline table-valued functions hoặc SQL thuần thay thế.