Mở đầu — vì sao bài này quan trọng
Hãy hình dung bạn là Technical BA trong một dự án ngân hàng. Đội kỹ thuật đang tranh cãi nảy lửa: một nửa muốn xử lý logic tính lãi suất ngay trong database bằng Stored Procedure, nửa còn lại muốn để toàn bộ logic ở tầng ứng dụng (application layer). Sếp quay sang hỏi bạn: "Theo BA, mình nên làm cách nào?". Nếu bạn lắc đầu nói "cái này là việc của dev", bạn vừa đánh mất một phần lớn giá trị của vai trò Technical BA.
Stored Procedure và Trigger là hai cơ chế cho phép chúng ta đưa logic nghiệp vụ vào bên trong database engine, thay vì để toàn bộ ở code ứng dụng. Đây không đơn thuần là chuyện kỹ thuật của developer. Quyết định "đặt logic ở đâu" ảnh hưởng trực tiếp đến cách bạn viết spec, cách bạn truy vết một con số sai trên báo cáo, cách bạn ước lượng rủi ro khi bảo trì, và cả cách bạn trả lời câu hỏi của auditor: "Tại sao số dư tài khoản này lại tự động thay đổi mà không có ai bấm nút?".
Trong các hệ thống Việt Nam — đặc biệt là core banking, ERP, hệ thống bảo hiểm cũ — bạn sẽ gặp Stored Procedure và Trigger ở khắp nơi. Có những hệ thống mà 60-70% logic nghiệp vụ nằm trong vài nghìn stored procedure. Một Technical BA không hiểu hai khái niệm này sẽ giống như đọc một cuốn tiểu thuyết mà thiếu mất nửa số chương. Bài này sẽ giúp bạn hiểu rõ bản chất, biết khi nào nên dùng — khi nào không nên dùng, và quan trọng nhất là biết cách viết spec sao cho dev triển khai đúng.
Khái niệm cốt lõi
Stored Procedure là gì?
Stored Procedure (thủ tục lưu trữ) là một đoạn code SQL được biên dịch và lưu sẵn bên trong database engine. Bạn gọi nó bằng lệnh CALL ten_procedure(tham_so). Thay vì gửi 5-7 câu lệnh SQL từ ứng dụng lên database, bạn gửi đúng một lệnh gọi, và toàn bộ xử lý diễn ra ngay trong database.
Ví dụ tối giản trong MySQL:
DELIMITER //
CREATE PROCEDURE chuyen_tien(
IN tk_nguon VARCHAR(20),
IN tk_dich VARCHAR(20),
IN so_tien DECIMAL(18,2)
)
BEGIN
START TRANSACTION;
UPDATE tai_khoan SET so_du = so_du - so_tien WHERE so_tk = tk_nguon;
UPDATE tai_khoan SET so_du = so_du + so_tien WHERE so_tk = tk_dich;
INSERT INTO lich_su_gd(tk_nguon, tk_dich, so_tien, thoi_gian)
VALUES (tk_nguon, tk_dich, so_tien, NOW());
COMMIT;
END //
DELIMITER //
Điểm mấu chốt: cả ba thao tác trên (trừ tiền, cộng tiền, ghi lịch sử) được đóng gói thành một đơn vị nghiệp vụ duy nhất, chạy sát dữ liệu, và có thể bọc trong một transaction để đảm bảo "được ăn cả, ngã về không".
Trigger là gì?
Trigger (kích hoạt) là một đoạn code tự động chạy khi có một sự kiện xảy ra trên bảng — thường là INSERT, UPDATE, hoặc DELETE. Điểm khác biệt cốt tử so với Stored Procedure: không ai gọi Trigger trực tiếp. Nó tự động "nổ" mỗi khi điều kiện được thỏa.
CREATE TRIGGER ghi_log_thay_doi_luong
AFTER UPDATE ON nhan_vien
FOR EACH ROW
BEGIN
IF OLD.luong <> NEW.luong THEN
INSERT INTO log_luong(nhan_vien_id, luong_cu, luong_moi, thoi_gian)
VALUES (NEW.id, OLD.luong, NEW.luong, NOW());
END IF;
END;
Trigger trên tự động ghi nhật ký mỗi khi lương của một nhân viên thay đổi. Lập trình viên cập nhật bảng nhan_vien thậm chí không cần biết log này tồn tại — nó vẫn được ghi.
Khác biệt cốt lõi giữa hai cơ chế
| Tiêu chí | Stored Procedure | Trigger |
|---|---|---|
| Cách kích hoạt | Gọi thủ công bằng CALL | Tự động khi có sự kiện |
| Tính minh bạch | Dễ thấy ai gọi, gọi khi nào | "Ẩn" — chạy ngầm, khó truy vết |
| Tham số đầu vào | Có thể nhận tham số | Không nhận tham số, chỉ dùng OLD/NEW |
| Mục đích điển hình | Đóng gói nghiệp vụ phức tạp | Audit log, ràng buộc dữ liệu, đồng bộ |
Đặt logic ở đâu — câu hỏi nền tảng
Đây là phần quan trọng nhất với BA. Có ba "nơi" để đặt logic nghiệp vụ:
- Trong database (Stored Procedure/Trigger): chạy nhanh vì sát dữ liệu, dùng chung cho mọi ứng dụng kết nối vào DB, nhưng khó debug, khó version control, khó scale theo chiều ngang.
- Ở tầng ứng dụng (Java, .NET, Node...): dễ test, dễ quản lý bằng Git, dễ tích hợp CI/CD, nhưng phải kéo dữ liệu từ DB lên xử lý — tốn round-trip.
- Kết hợp cả hai: thực tế đa số hệ thống làm vậy, và nhiệm vụ của BA là chỉ rõ ranh giới.
Tình huống thực tế
Tình huống 1 — Ngân hàng số tại TP.HCM và "con số ma" trên báo cáo
Một ngân hàng số tầm trung tại TP.HCM gặp sự cố: báo cáo tổng dư nợ cuối ngày luôn lệch khoảng 0.3% so với sổ cái. Đội vận hành mất gần hai tuần dò tìm. Technical BA tham gia điều tra phát hiện nguyên nhân: có một Trigger trên bảng khoan_vay được viết từ 4 năm trước, tự động cập nhật trường du_no mỗi khi có giao dịch trả nợ. Trong khi đó, đội phát triển ứng dụng mới — không biết Trigger này tồn tại — cũng viết một đoạn code ở tầng application để tính lại dư nợ. Kết quả: cùng một con số bị cập nhật hai lần trong vài trường hợp hiếm.
Diễn giải: Đây là mặt tối kinh điển của Trigger — tính "vô hình". Vì Trigger chạy ngầm, dev mới không nhìn thấy nó trong code ứng dụng, dẫn đến logic bị trùng lặp. BA điều tra sự cố này đã phải dùng lệnh SHOW TRIGGERS trên cả hệ thống để lập danh sách toàn bộ trigger đang hoạt động — một việc lẽ ra phải nằm trong tài liệu kỹ thuật ngay từ đầu.
Bài học rút ra: Trigger rất mạnh nhưng cực kỳ nguy hiểm khi không được tài liệu hóa. Là BA, mỗi khi spec có dòng "hệ thống tự động cập nhật...", bạn phải hỏi: việc tự động này được thực hiện ở đâu — trigger, scheduled job, hay code ứng dụng? Và nó có được liệt kê trong tài liệu vận hành không?
Tình huống 2 — Sàn thương mại điện tử và bài toán trừ tồn kho
Một sàn thương mại điện tử kiểu Tiki/Shopee trong nước có chương trình flash sale. Cao điểm có thể 5.000 đơn/giây cùng giành một mặt hàng. Đội kỹ thuật ban đầu để logic trừ tồn kho ở tầng ứng dụng: đọc số lượng tồn, kiểm tra, rồi trừ. Hậu quả là oversell — bán vượt số lượng thực có — vì giữa lúc đọc và lúc trừ, hàng chục request khác đã chen vào.
Giải pháp họ chọn: viết một Stored Procedure tru_ton_kho(san_pham_id, so_luong) thực hiện kiểm tra và trừ trong một câu lệnh atomic duy nhất, có khóa hàng (row lock), trả về mã lỗi nếu hết hàng. Toàn bộ thao tác diễn ra trong database, loại bỏ khoảng trống thời gian giữa đọc và ghi.
Diễn giải: Đây là trường hợp Stored Procedure tỏa sáng. Khi cần đảm bảo tính toàn vẹn dưới tải cao và tránh race condition, việc đẩy logic sát dữ liệu giúp tận dụng cơ chế khóa của database engine một cách tự nhiên và hiệu quả hơn nhiều so với xử lý qua nhiều round-trip mạng.
Bài học rút ra: Khi viết spec cho các thao tác nhạy cảm với đồng thời (concurrency) như trừ tồn kho, đặt vé, rút tiền, BA nên nêu rõ yêu cầu phi chức năng: "Thao tác phải atomic, không cho phép oversell dưới tải 5.000 req/s". Chính yêu cầu này thường dẫn dev đến lựa chọn Stored Procedure.
Tình huống 3 — Công ty bảo hiểm và gánh nặng 3.000 Stored Procedure
Một công ty bảo hiểm lâu năm có hệ thống core chứa khoảng 3.000 Stored Procedure tích lũy suốt 15 năm. Khi cần thêm tính năng tính phí bảo hiểm theo độ tuổi mới, mỗi thay đổi nhỏ kéo theo rủi ro khổng lồ vì không ai dám chắc procedure nào gọi procedure nào, không có unit test, không version control rõ ràng. Một lần chỉnh sửa nhỏ làm sai phí của hàng nghìn hợp đồng, phát hiện sau ba tuần.
Diễn giải: Đây là mặt trái của việc lạm dụng Stored Procedure. Khi quá nhiều logic nghiệp vụ phình to trong database, hệ thống trở thành "hộp đen" khó test, khó bảo trì, gắn chặt với một loại database cụ thể (vendor lock-in), và khó áp dụng quy trình DevOps hiện đại.
Bài học rút ra: Stored Procedure không phải "càng nhiều càng tốt". Khi tư vấn cho dự án mới, BA nên cảnh báo về chi phí bảo trì dài hạn và đề xuất giữ logic nghiệp vụ hay thay đổi ở tầng ứng dụng, chỉ dùng Stored Procedure cho những phần thực sự cần sát dữ liệu.
Hướng dẫn từng bước
Với vai trò Technical BA, đây là quy trình khi bạn gặp một yêu cầu có thể cần Stored Procedure hoặc Trigger:
Bước 1 — Xác định bản chất của logic. Hỏi: logic này là ràng buộc toàn vẹn dữ liệu (luôn đúng dù truy cập từ đâu) hay quy tắc nghiệp vụ (thay đổi theo chính sách kinh doanh)? Loại đầu nghiêng về DB, loại sau nghiêng về application.
Bước 2 — Đánh giá yêu cầu hiệu năng và đồng thời. Thao tác có chịu tải cao không? Có nguy cơ race condition không? Có cần atomic không? Nếu có, ghi rõ vào NFR — đây là tín hiệu cho Stored Procedure.
Bước 3 — Cân nhắc tính minh bạch. Nếu logic cần "tự động chạy ngầm" như ghi audit log, đồng bộ bảng phụ, thì Trigger phù hợp. Nhưng phải chấp nhận đánh đổi về khả năng truy vết.
Bước 4 — Viết spec rõ ràng. Với mỗi Stored Procedure, mô tả: tên, mục đích, tham số đầu vào (kiểu, bắt buộc/tùy chọn), giá trị trả về, các mã lỗi, và transaction boundary. Với mỗi Trigger, mô tả: bảng nào, sự kiện gì (INSERT/UPDATE/DELETE), thời điểm (BEFORE/AFTER), và hành động.
Bước 5 — Bắt buộc tài liệu hóa và đăng ký. Yêu cầu mọi Trigger và Stored Procedure phải có trong tài liệu kỹ thuật, kèm sơ đồ phụ thuộc nếu chúng gọi lẫn nhau. Đây là điều khoản bạn nên đưa vào Definition of Done.
Bước 6 — Thống nhất quy trình thay đổi. Logic trong DB cũng phải được version control (lưu file .sql trong Git), review, và deploy qua pipeline như code ứng dụng — không được sửa trực tiếp trên production.
Lỗi thường gặp & mẹo
Lỗi 1 — Trigger "dây chuyền" (cascading trigger). Trigger A cập nhật bảng X, kích hoạt Trigger B trên bảng X, Trigger B lại cập nhật bảng Y... Hệ quả là một câu UPDATE đơn giản gây ra một chuỗi phản ứng khó lường, thậm chí lặp vô hạn. Mẹo: khi spec có nhiều trigger trên các bảng liên quan, hãy vẽ sơ đồ luồng kích hoạt để phát hiện vòng lặp.
Lỗi 2 — Đặt logic nghiệp vụ "dễ đổi" vào Stored Procedure. Quy tắc tính khuyến mãi, hạn mức, chính sách giá... thay đổi liên tục. Nhét chúng vào DB khiến mỗi lần đổi chính sách phải sửa và deploy procedure. Mẹo: tách phần quy tắc thường đổi ra cấu hình hoặc tầng ứng dụng.
Lỗi 3 — Quên transaction boundary. Một Stored Procedure cập nhật ba bảng nhưng không bọc trong transaction. Nếu lỗi giữa chừng, dữ liệu rơi vào trạng thái nửa vời. Mẹo: spec phải nêu rõ "các thao tác này phải nằm trong một transaction, rollback toàn bộ nếu lỗi".
Lỗi 4 — Trigger làm chậm ghi dữ liệu. Trigger chạy đồng bộ với thao tác gốc; nếu trigger nặng, mọi lần INSERT/UPDATE đều chậm theo. Mẹo: với tác vụ nặng và không cần tức thời (gửi email, đồng bộ hệ thống ngoài), nên dùng hàng đợi/job bất đồng bộ thay vì trigger.
Mẹo vàng cho BA: Mỗi khi nghe câu "hệ thống tự động làm X", phản xạ đầu tiên của bạn phải là hỏi "tự động đó nằm ở đâu — trigger, procedure, job hay code?". Câu hỏi nhỏ này giúp bạn tránh được vô số sự cố "con số ma" về sau.
Bài tập thực hành
- Phân loại logic. Cho danh sách sau, hãy quyết định nên đặt ở Stored Procedure, Trigger, hay tầng ứng dụng — và giải thích lý do: (a) ghi nhật ký mỗi khi đổi địa chỉ khách hàng; (b) tính điểm tích lũy thành viên theo chương trình thay đổi hàng quý; (c) trừ số ghế còn trống khi đặt vé máy bay dưới tải cao; (d) kiểm tra email đúng định dạng khi tạo tài khoản.
- Đọc và mô tả Trigger. Lấy đoạn Trigger
ghi_log_thay_doi_luongở phần Khái niệm. Viết một đoạn mô tả nghiệp vụ bằng ngôn ngữ business (không dùng SQL) để giải thích cho người dùng cuối hiểu chuyện gì xảy ra mỗi khi lương thay đổi.
- Viết spec. Soạn một đặc tả ngắn cho Stored Procedure
huy_don_hang(don_id): liệt kê tham số, các bảng bị tác động, transaction boundary, ít nhất ba mã lỗi (ví dụ: đơn không tồn tại, đơn đã giao, đơn đã hủy trước đó), và giá trị trả về.
- Săn rủi ro. Giả sử bạn nhận bàn giao một hệ thống cũ. Hãy liệt kê 5 câu hỏi bạn sẽ đặt cho đội kỹ thuật để đánh giá mức độ rủi ro liên quan đến Stored Procedure và Trigger hiện có.
Tóm tắt
Stored Procedure là code SQL đóng gói sẵn trong database, gọi bằng CALL, lý tưởng cho các thao tác cần sát dữ liệu, atomic và chịu tải đồng thời cao như trừ tồn kho hay chuyển tiền. Trigger là code tự động chạy khi có sự kiện trên bảng, rất hợp cho audit log và đồng bộ dữ liệu, nhưng mang tính "vô hình" nên dễ gây sự cố nếu không được tài liệu hóa.
Là Technical BA, giá trị của bạn không nằm ở việc tự viết được procedure, mà ở việc biết khi nào nên dùng — khi nào không nên, và viết được spec rõ ràng về tham số, transaction, mã lỗi cùng yêu cầu tài liệu hóa. Hãy nhớ ba nguyên tắc: logic toàn vẹn dữ liệu thì cân nhắc gần DB, logic nghiệp vụ hay đổi thì để tầng ứng dụng; mọi Trigger và Procedure đều phải được đăng ký và version control; và mỗi khi nghe "hệ thống tự động", hãy luôn hỏi "tự động ở đâu". Nắm vững những điều này, bạn sẽ tự tin ngồi vào bàn tranh luận kỹ thuật và đóng góp quyết định thực sự có giá trị.