IT Learning HubIT Learning Hub
THỰC HÀNH 05 · BTHT5

Trigger trong SQL Server

5 tiếtDatabaseSQL Server

Mục tiêu: Hiểu cách SQL Server tự động thực hiện một nhóm câu lệnh khi dữ liệu trong bảng phát sinh các thao tác INSERT, UPDATE, DELETE.

01 · TRIGGER LÀ GÌ?

Trigger là gì?

Trigger là một đoạn chương trình SQL được gắn với một bảng hoặc view, được SQL Server tự động kích hoạt khi xảy ra thao tác INSERT, UPDATE hoặc DELETE.

Không có Trigger, so với có Trigger

Người dùng ↓ INSERT / UPDATE / DELETE ↓ Trigger được kích hoạt ↓ Kiểm tra / xử lý / thông báo ↓ Dữ liệu được cập nhật

Ví dụ: "không cho phép số lượng sản phẩm nhỏ hơn 0." Khi người dùng chạy UPDATE SANPHAM SET SOLUONG = -10 WHERE MASP = 'SP01';, Trigger phát hiện SOLUONG < 0 ➜ Báo lỗi ➜ ROLLBACK ➜ Hủy thao tác.

Tại sao cần Trigger?

Trigger thường dùng để tự động kiểm soát dữ liệu ngay tại tầng cơ sở dữ liệu:

  • Kiểm tra dữ liệu: Số lượng sản phẩm ≥ 0
  • Ngăn thao tác không hợp lệ: Không cho xóa khách hàng đã phát sinh giao dịch
  • Tự động cập nhật: Thêm CTHD ➜ Tự động cập nhật trị giá hóa đơn
  • Theo dõi thay đổi: Tên khách hàng cũ ➜ Mới
  • Đồng bộ dữ liệu: CTHD thay đổi ➜ Cập nhật tổng tiền hóa đơn ➜ Cập nhật doanh số khách hàng

02 · CÁC LOẠI TRIGGER

AFTER và INSTEAD OF

SQL Server hỗ trợ hai loại Trigger chính. SQL Server không hỗ trợ BEFORE TRIGGER theo cách một số hệ quản trị CSDL khác có.

AFTER - Chạy sau khi thao tác DML hoàn tất thành công (AFTER INSERT, AFTER UPDATE, AFTER DELETE). INSTEAD OF - thay thế thao tác DML ban đầu bằng các câu lệnh định nghĩa bên trong Trigger.

LoạiCách hoạt độngVí dụ
AFTERThao tác gốc xảy ra trước, Trigger xử lý sauKiểm tra và cập nhật dữ liệu
INSTEAD OFTrigger thay thế thao tác gốcKhông cho phép xóa
BEFOREKhông hỗ trợ trong SQL Server-
AFTER: DELETE ➜ Xóa dữ liệu ➜ Trigger ➜ Xử lý tiếp INSTEAD OF: DELETE ➜ Trigger ➜ KHÔNG thực hiện DELETE gốc

03 · CÚ PHÁP TRIGGER

Cú pháp tạo Trigger

CREATE TRIGGER <Tên_Trigger>
ON <Tên_Bảng>
AFTER | INSTEAD OF INSERT, UPDATE, DELETE
AS
BEGIN
    -- Các câu lệnh xử lý
END;

FOR cũng có thể được dùng thay cho AFTER.

Giải thích từng thành phần

CREATE TRIGGER trg_AfterInsert_KhachHang
ON KHACHHANG
AFTER INSERT
AS
BEGIN
    -- xử lý
END;

CREATE TRIGGER tạo Trigger mới. Tên nên dễ hiểu, ví dụ trg_AfterInsert_KhachHang, trg_CheckQuantity_SanPham, trg_PreventDelete_NhanVien. ON KHACHHANG gắn Trigger vào bảng đó. AFTER INSERT chạy sau khi có INSERT. BEGIN...END chứa các câu lệnh thực hiện.

Ví dụ - AFTER INSERT

Khi thêm khách hàng mới, hiển thị thông báo:

CREATE TRIGGER trg_AfterInsert_KhachHang
ON KHACHHANG
AFTER INSERT
AS
BEGIN
    PRINT N'Khách hàng mới đã được thêm vào KHACHHANG.';
END;

Thử nghiệm:

INSERT INTO KHACHHANG
VALUES (
    'KH99',
    N'Nguyễn Văn A',
    N'Hà Nội',
    '0909000000',
    '2026-08-11',
    0
);
Người dùng không cần gọi Trigger bằng tay - SQL Server tự động kích hoạt Trigger ngay sau khi INSERT thành công.

Ví dụ - INSTEAD OF DELETE

Không cho phép xóa khách hàng:

CREATE TRIGGER trg_PreventDelete_KhachHang
ON KHACHHANG
INSTEAD OF DELETE
AS
BEGIN
    PRINT N'Không được xóa khách hàng.';
END;

Khi chạy DELETE FROM KHACHHANG WHERE MAKH = 'KH01';, thay vì xóa, hệ thống chỉ in ra "Không được xóa khách hàng."

04 · KIỂM TRA VÀ XỬ LÝ LỖI

Trigger kiểm tra dữ liệu

Bài toán: Số lượng sản phẩm trong SANPHAM không được nhỏ hơn 0.

CREATE TRIGGER trg_CheckQuantity_SanPham
ON SANPHAM
AFTER INSERT, UPDATE
AS
BEGIN

    IF EXISTS (
        SELECT *
        FROM SANPHAM
        WHERE SOLUONG < 0
    )
    BEGIN
        RAISERROR (
            N'Số lượng không thể âm.',
            16,
            1
        );

        ROLLBACK;
        RETURN;
    END

END;

Phân tích

IF EXISTS ➜ RAISERROR ➜ ROLLBACK ➜ RETURN ➜ Hủy thao tác sai

IF EXISTS(...) kiểm tra có tồn tại bản ghi vi phạm điều kiện hay không. RAISERROR(N'...', 16, 1) báo lỗi - Mức độ 16 là mức thường dùng để biểu diễn lỗi do người dùng gây ra (dữ liệu nhập sai, vi phạm quy tắc). ROLLBACK hủy giao dịch đang thực hiện, đưa dữ liệu về trạng thái trước đó.

Lỗi hay gặp: Gọi RAISERROR và ROLLBACK nhưng quên RETURN ngay sau đó. Nếu Trigger còn câu lệnh nào phía sau, chúng vẫn sẽ tiếp tục chạy dù giao dịch đã bị hủy, có thể gây lỗi hoặc hành vi không mong muốn. Nên tập thói quen luôn viết đủ bộ ba RAISERROR ➜ ROLLBACK ➜ RETURN, không chỉ dừng ở ROLLBACK.

PRINT và RAISERROR - Phân biệt

PRINTRAISERROR
Mục đíchThông báoBáo lỗi
Có thể dừng thao tácKhôngCó
Dùng với ROLLBACKCó thể nhưng không tự độngThường kết hợp
Ví dụ"Đã thêm khách hàng""Số lượng không thể âm"

KIỂM TRA NHANH

Muốn hủy hẳn một thao tác UPDATE khi phát hiện dữ liệu sai trong Trigger, cần dùng thêm lệnh nào ngoài RAISERROR?

05 · INSERTED VÀ DELETED

INSERTED và DELETED

Trong Trigger, SQL Server cung cấp hai bảng ảo: INSERTED và DELETED. Chúng chỉ tồn tại trong phạm vi Trigger, chứa dữ liệu liên quan đến thao tác DML đang xảy ra.
Thao tácINSERTEDDELETED
INSERTBản ghi mới-
DELETE-Bản ghi bị xóa
UPDATEGiá trị mớiGiá trị cũ
Mẹo nhớ: INSERTED = cái mới. DELETED = cái cũ / bị xóa.

Khai báo biến tạm (@)

Biến tạm (local variable) trong T-SQL luôn bắt đầu bằng dấu @, khai báo bằng DECLARE - Chỉ tồn tại trong phạm vi batch/Trigger đang chạy, dùng để lưu tạm 1 giá trị lấy ra từ INSERTED/DELETED trước khi so sánh hay xử lý tiếp.
DECLARE @TenBien KieuDuLieu;

Khai báo nhiều biến cùng lúc, ngăn cách bằng dấu phẩy:

DECLARE @MaKH CHAR(4), @NgayDK SMALLDATETIME;

Biến mới khai báo mặc định mang giá trị NULL. Có 2 cách gán giá trị:

Cách gánCú phápKhi nào dùng
SETSET @MaKH = 'KH01';Gán 1 giá trị đơn, biết trước hoặc lấy từ subquery đúng 1 dòng.
SELECTSELECT @MaKH = MAKH FROM inserted;Gán trực tiếp từ kết quả 1 câu SELECT.
DECLARE @MaKH CHAR(4);

SELECT @MaKH = MAKH
FROM inserted;

PRINT N'Khách hàng vừa thêm: ' + @MaKH;
Khác biệt quan trọng giữa SET và SELECT: Nếu subquery/bảng nguồn trả về nhiều hơn 1 dòng, SET @x = (SELECT ...) sẽ báo lỗi ngay lập tức - Nhưng SELECT @x = cột FROM bảng thì không báo lỗi gì cả, nó chỉ âm thầm gán giá trị của dòng cuối cùng được xử lý (thứ tự không đảm bảo) rồi bỏ qua tất cả các dòng còn lại. Đây chính là nguồn gốc của lỗi "Trigger chỉ đúng với 1 dòng" sẽ nói kỹ ở mục 07 - Không có thông báo lỗi nào để nhận biết, code vẫn chạy "được" nhưng sai âm thầm.

Thử trực tiếp

Bấm một nút để xem INSERTED/DELETED chứa gì với từng thao tác (ví dụ: Đổi tên khách hàng từ "Nguyễn Văn A" thành "Nguyễn Văn B"):

DEMO TRỰC QUAN

DELETED (dữ liệu cũ)
(trống)
INSERTED (dữ liệu mới)
HOTEN = 'Nguyễn Văn A' (bản ghi mới)

INSERT ➜ Chỉ INSERTED có dữ liệu · DELETE ➜ Chỉ DELETED có dữ liệu · UPDATE ➜ Cả hai đều có dữ liệu.

CÂU LỆNH TƯƠNG ỨNG
INSERT INTO KHACHHANG (MAKH, HOTEN)
VALUES ('KH01', N'Nguyễn Văn A');

Ví dụ - Đọc INSERTED

CREATE TRIGGER trg_AfterInsert_KhachHang
ON KHACHHANG
AFTER INSERT
AS
BEGIN
    DECLARE @MAKH CHAR(4);

    SELECT @MAKH = MAKH
    FROM INSERTED;

    PRINT N'Khách hàng mới được thêm vào với MAKH: '
        + CAST(@MAKH AS VARCHAR);
END;

Ví dụ - Đọc DELETED

CREATE TRIGGER trg_AfterDelete_KhachHang
ON KHACHHANG
AFTER DELETE
AS
BEGIN
    SELECT *
    FROM DELETED;
END;

Ví dụ hoàn chỉnh - Theo dõi thay đổi tên (UPDATE)

Khi UPDATE, DELETED chứa giá trị cũ và INSERTED chứa giá trị mới, nên có thể so sánh "giá trị cũ ➜ giá trị mới":

CREATE TRIGGER trg_AfterUpdate_KhachHang
ON KHACHHANG
AFTER UPDATE
AS
BEGIN

    DECLARE
        @OldName VARCHAR(40),
        @NewName VARCHAR(40);

    SELECT @OldName = HOTEN
    FROM DELETED;

    SELECT @NewName = HOTEN
    FROM INSERTED;

    PRINT N'Tên khách hàng đã thay đổi từ "'
        + @OldName
        + N'" sang "'
        + @NewName
        + N'".';

END;

06 · ỨNG DỤNG THỰC TẾ

Ứng dụng thực tế của Trigger

Lỗi hay gặp: Dùng Trigger cho những ràng buộc đơn giản mà CHECK CONSTRAINT hoặc FOREIGN KEY đã làm được (ví dụ chỉ để chặn SOLUONG < 0, hoặc đặt giá trị mặc định). Trigger chạy sau khi thao tác DML đã xảy ra nên tốn thêm chi phí xử lý; chỉ nên dùng Trigger khi logic vượt quá khả năng của Constraint - Ví dụ cần tự động cập nhật dữ liệu ở bảng khác, hoặc kiểm tra điều kiện dựa trên dữ liệu ở nhiều bảng.

Kiểm tra tồn tại trước khi thêm

Không cho phép thêm hóa đơn nếu khách hàng chưa tồn tại:

INSERT HOADON ➜ Lấy MAKH từ INSERTED ➜ Kiểm tra KHACHHANG ➜ Có tồn tại? Có ➜ Cho phép | Không ➜ RAISERROR + ROLLBACK

IF EXISTS kiểm tra có tồn tại dữ liệu thỏa điều kiện; IF NOT EXISTS kiểm tra ngược lại - Không tồn tại dữ liệu thỏa điều kiện.

Trigger tự động cập nhật dữ liệu

Bảng HOADON(MAHD, TRIGIA) và CTHD(MAHD, MASP, SOLUONG, DONGIA). Khi INSERT INTO CTHD, Trigger tự động tính lại TRIGIA và UPDATE HOADON.

Trigger cập nhật doanh số khách hàng

Bài toán nâng cao hơn: Thêm/sửa hóa đơn ➜ Trigger tính tổng trị giá ➜ Cập nhật DOANHSO trong KHACHHANG.

Không cho xóa dữ liệu đang được tham chiếu

Không cho phép xóa nhân viên nếu nhân viên đã tham gia bán hàng:

DELETE NHANVIEN ➜ Kiểm tra HOADON ➜ Có hóa đơn? Có ➜ ROLLBACK | Không ➜ Cho phép

Tự động cập nhật ngày đăng ký

Khi thêm khách hàng mới, nếu không nhập NGDK, hệ thống tự động lấy ngày hiện tại bằng GETDATE().

Một số bài toán mở rộng

  • Sản phẩm chỉ được thêm vào hóa đơn nếu sản phẩm tồn tại và đủ tồn kho
  • Tổng giá trị trong CTHD phải khớp với HOADON
  • Hóa đơn vượt 1.000.000.000 phải chuyển trạng thái "Kiểm tra đặc biệt"
  • Tổng số lượng sản phẩm bán ra trong một ngày không vượt 10.000 sản phẩm
  • Không cho nhân viên trở thành cấp trên của chính mình

07 · TRIGGER XỬ LÝ NHIỀU DÒNG

Trigger nên tư duy theo tập dữ liệu

Sinh viên thường viết:

DECLARE @MAKH CHAR(4);

SELECT @MAKH = MAKH
FROM INSERTED;

Cách này dễ hiểu khi minh họa một bản ghi, nhưng một câu INSERT INTO ... SELECT ... có thể thêm nhiều dòng cùng lúc - Và như đã nói ở mục 05, SELECT @x = cột FROM inserted không hề báo lỗi khi có nhiều dòng, nó chỉ âm thầm bỏ sót.

INSERTED ├── dòng 1 ├── dòng 2 ├── dòng 3 ├── ... └── dòng n
Trigger nên được tư duy theo tập hợp dữ liệu (set-based) thay vì mặc định chỉ có một dòng - Đây là kỹ năng quan trọng khi bước từ bài thực hành sang SQL thực tế. Không nên chỉ tư duy "INSERTED ➜ 1 dòng ➜ 1 biến", mà cần xử lý toàn bộ tập dữ liệu trong INSERTED/DELETED.

Biến tạm so với IF EXISTS/JOIN

Đây là 2 cách viết phổ biến cho cùng 1 bài toán kiểm tra dữ liệu trong Trigger:

Biến tạm (DECLARE + SELECT @x = ...)IF EXISTS/NOT EXISTS + JOIN
Đúng với nhiều dòng?Chỉ đúng khi chắc chắn thao tác tác động đúng 1 dòngLuôn đúng, bất kể 1 hay nhiều dòng
Cách kiểm traGán giá trị ra biến rồi so sánhKiểm tra "có tồn tại dòng vi phạm không" trên toàn bộ inserted/deleted
Khi nào nên dùngBài minh họa đơn giản, hoặc chắc chắn 100% chỉ có 1 dòngNên dùng mặc định cho mọi Trigger kiểm tra dữ liệu
-- Biến tạm - chỉ an toàn khi chắc chắn 1 dòng
DECLARE @SoLuong INT;
SELECT @SoLuong = SOLUONG FROM inserted;
IF (@SoLuong < 0)
BEGIN
    RAISERROR (N'Số lượng không thể âm.', 16, 1);
    ROLLBACK TRANSACTION;
    RETURN;
END
-- IF EXISTS - đúng với bất kỳ số dòng nào
IF EXISTS (SELECT * FROM inserted WHERE SOLUONG < 0)
BEGIN
    RAISERROR (N'Số lượng không thể âm.', 16, 1);
    ROLLBACK TRANSACTION;
    RETURN;
END

Trong đề bài BTTH5, đa số câu đều nên trình bày theo hướng IF EXISTS/NOT EXISTS làm đáp án chính - Cách dùng biến tạm vẫn được giữ lại trong đáp án như một "Cách khác" để tham khảo, kèm ghi chú rõ giới hạn chỉ đúng khi thao tác ảnh hưởng đúng 1 dòng.

08 · QUY TRÌNH XÂY TRIGGER

Quy trình 6 bước

Khi gặp bài tập Trigger, có thể làm theo 6 bước:

1. Xác định sự kiện: INSERT / UPDATE / DELETE ↓ 2. Xác định bảng: ON ... ↓ 3. Xác định loại: AFTER hay INSTEAD OF ↓ 4. Xác định dữ liệu cần kiểm tra: INSERTED / DELETED / bảng khác ↓ 5. Viết điều kiện: IF EXISTS / IF NOT EXISTS ↓ 6. Xử lý: PRINT / RAISERROR / ROLLBACK / INSERT / UPDATE / DELETE

Quản lý Trigger

Xóa Trigger:

DROP TRIGGER trg_AfterInsert_KhachHang;

Xem thông tin Trigger:

SELECT *
FROM INFORMATION_SCHEMA.TRIGGERS;

Cheat Sheet Tuần 5

TRIGGER ➜ Tự động chạy khi DML xảy ra INSERT/UPDATE/DELETE ➜ Các thao tác kích hoạt Trigger AFTER ➜ Chạy sau thao tác DML INSTEAD OF ➜ Thay thế thao tác DML INSERTED ➜ Dữ liệu mới DELETED ➜ Dữ liệu cũ / bị xóa DECLARE @x ➜ Khai báo biến tạm SET @x = ... ➜ Gán 1 giá trị, báo lỗi nếu subquery >1 dòng SELECT @x = ...FROM... ➜ Gán từ SELECT, ÂM THẦM chỉ lấy 1 dòng IF EXISTS ➜ Kiểm tra có dữ liệu thỏa điều kiện IF NOT EXISTS ➜ Kiểm tra không tồn tại dữ liệu PRINT ➜ Thông báo RAISERROR ➜ Báo lỗi ROLLBACK ➜ Hủy thao tác / giao dịch

Năm tuần đầu của IT004

Tuần 1: Tạo CSDL. Tuần 2: Truy vấn. Tuần 3: Truy vấn nâng cao. Tuần 4: Xử lý dữ liệu (CAST/CASE/Subquery). Tuần 5: Tự động hóa & kiểm soát bằng Trigger, INSERTED/DELETED, RAISERROR/ROLLBACK. Tuần 6 theo khung môn học sẽ là ôn tập và thi thực hành, thời gian và hình thức thi được thông báo sau.

09 · BÀI TẬP THỰC HÀNH

Thực hành tổng hợp

BÀI TẬP CHÍNH THỨC · BTTH5

Bài tập Trigger

Xem đề bài đầy đủ, file đính kèm và đáp án chi tiết (cần mã truy cập) của BTTH5.

Xem bài tập & đáp án ›
LUYỆN THÊM · KHÔNG BẮT BUỘC

Các bài tự luyện thêm về Trigger - Không thuộc bài nộp BTTH5. Mỗi bài có gợi ý ẩn - Bấm để mở.

01

Không cho số lượng âm

Tạo Trigger: Không cho phép SOLUONG < 0 trong SANPHAM.

Gợi ý
AFTER INSERT, UPDATE + IF EXISTS + RAISERROR + ROLLBACK
02

Theo dõi thay đổi

Khi thay đổi tên khách hàng, hiển thị: 'Tên khách hàng đã thay đổi từ "..." sang "..."'.

Gợi ý
INSERTED + DELETED + AFTER UPDATE
03

Không cho xóa

Không cho phép xóa nhân viên nếu nhân viên đã có hóa đơn.

Gợi ý
INSTEAD OF DELETE + DELETED + IF EXISTS
04

Tự động cập nhật

Khi thêm chi tiết hóa đơn, tự động cập nhật trị giá hóa đơn.

Gợi ý
AFTER INSERT + INSERTED + UPDATE
05★★

Khi thêm hoặc cập nhật hóa đơn, tự động cập nhật doanh số khách hàng.

Gợi ý
AFTER INSERT, UPDATE + INSERTED + UPDATE KHACHHANG.DOANHSO