No Negative Quantities
Create a Trigger: don't allow SOLUONG < 0 in SANPHAM.
Objective: Understand how SQL Server automatically runs a block of statements when data in a table undergoes an INSERT, UPDATE, or DELETE operation.
01 · WHAT IS A TRIGGER?
INSERT, UPDATE, or DELETE operation occurs.Example: "don't allow a product's quantity to go below 0." When a user runs UPDATE SANPHAM SET SOLUONG = -10 WHERE MASP = 'SP01';, the Trigger detects SOLUONG < 0 ➜ raises an error ➜ ROLLBACK ➜ cancels the operation.
Triggers are typically used to automatically enforce rules right at the database layer:
02 · TYPES OF TRIGGER
SQL Server supports two main types of Trigger. SQL Server does not support BEFORE TRIGGER the way some other database systems do.
AFTER - runs after the DML operation completes successfully (AFTER INSERT, AFTER UPDATE, AFTER DELETE). INSTEAD OF - replaces the original DML operation with the statements defined inside the Trigger.
| Type | How It Works | Example |
|---|---|---|
AFTER | The original operation happens first, then the Trigger processes | Validate and update data |
INSTEAD OF | The Trigger replaces the original operation | Prevent deletion |
BEFORE | Not supported in SQL Server | - |
03 · TRIGGER SYNTAX
CREATE TRIGGER <Trigger_Name>
ON <Table_Name>
AFTER | INSTEAD OF INSERT, UPDATE, DELETE
AS
BEGIN
-- Statements to run
END;FOR can also be used in place of AFTER.
CREATE TRIGGER trg_AfterInsert_KhachHang
ON KHACHHANG
AFTER INSERT
AS
BEGIN
-- processing
END;CREATE TRIGGER creates a new Trigger. The name should be descriptive, e.g. trg_AfterInsert_KhachHang, trg_CheckQuantity_SanPham, trg_PreventDelete_NhanVien. ON KHACHHANG attaches the Trigger to that table. AFTER INSERT runs after an INSERT occurs. BEGIN...END contains the statements to execute.
When a new customer is added, display a message:
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;Test it:
INSERT INTO KHACHHANG
VALUES (
'KH99',
N'Nguyễn Văn A',
N'Hà Nội',
'0909000000',
'2026-08-11',
0
);INSERT succeeds.Prevent customers from being deleted:
CREATE TRIGGER trg_PreventDelete_KhachHang
ON KHACHHANG
INSTEAD OF DELETE
AS
BEGIN
PRINT N'Không được xóa khách hàng.';
END;When running DELETE FROM KHACHHANG WHERE MAKH = 'KH01';, instead of deleting, the system just prints "Customers cannot be deleted."
04 · ERROR CHECKING AND HANDLING
Problem: the quantity in SANPHAM must not be less than 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;IF EXISTS(...) checks whether a record violating the condition exists. RAISERROR(N'...', 16, 1) raises an error - severity level 16 is the level typically used for user-caused errors (bad input, rule violations). ROLLBACK cancels the current transaction, returning the data to its previous state.
RAISERROR and ROLLBACK but forgetting RETURN right after. If the Trigger has any statements after that point, they'll still run even though the transaction was already cancelled, potentially causing errors or unintended behavior. Get in the habit of always writing the full trio RAISERROR ➜ ROLLBACK ➜ RETURN, not stopping at just ROLLBACK.| RAISERROR | ||
|---|---|---|
| Purpose | Notification | Error reporting |
| Can stop the operation | No | Yes |
| Used with ROLLBACK | Possible, but not automatic | Usually paired together |
| Example | "Customer added" | "Quantity cannot be negative" |
QUICK CHECK
To fully cancel an UPDATE operation when a Trigger detects invalid data, what command is needed besides RAISERROR?
RAISERROR only reports the error - ROLLBACK is what actually cancels the transaction and restores the data to its previous state.05 · INSERTED AND DELETED
INSERTED and DELETED. They only exist within the scope of the Trigger, and hold data related to the DML operation currently taking place.| Operation | INSERTED | DELETED |
|---|---|---|
INSERT | The new record | - |
DELETE | - | The deleted record |
UPDATE | The new value | The old value |
INSERTED = the new one. DELETED = the old / removed one.@, declared with DECLARE - it only exists within the scope of the current batch/Trigger, and is used to hold a value pulled from INSERTED/DELETED before comparing it or processing it further.DECLARE @VariableName DataType;Declare several variables at once, separated by commas:
DECLARE @MaKH CHAR(4), @NgayDK SMALLDATETIME;A newly declared variable starts out as NULL. There are 2 ways to assign a value:
| Method | Syntax | When to Use |
|---|---|---|
SET | SET @MaKH = 'KH01'; | Assign a single known value, or a subquery guaranteed to return exactly 1 row. |
SELECT | SELECT @MaKH = MAKH FROM inserted; | Assign directly from the result of a SELECT. |
DECLARE @MaKH CHAR(4);
SELECT @MaKH = MAKH
FROM inserted;
PRINT N'Customer just added: ' + @MaKH;SET and SELECT: If the subquery/source table returns more than 1 row, SET @x = (SELECT ...) will raise an error immediately - but SELECT @x = column FROM table raises no error at all. It just silently assigns the value from the last row processed (the order isn't guaranteed) and drops every other row. This is exactly where the "Trigger only works for 1 row" bug covered in section 07 comes from - there's no error message to catch it, the code just runs "fine" while being silently wrong.Click a button to see what INSERTED/DELETED contain for each operation (example: renaming a customer from "Nguyễn Văn A" to "Nguyễn Văn B"):
VISUAL DEMO
INSERT ➜ only INSERTED has data · DELETE ➜ only DELETED has data · UPDATE ➜ both have data.
INSERT INTO KHACHHANG (MAKH, HOTEN)
VALUES ('KH01', N'Nguyễn Văn A');
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;CREATE TRIGGER trg_AfterDelete_KhachHang
ON KHACHHANG
AFTER DELETE
AS
BEGIN
SELECT *
FROM DELETED;
END;On UPDATE, DELETED holds the old value and INSERTED holds the new value, so you can compare "old value ➜ new value":
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 · REAL-WORLD USES
CHECK CONSTRAINT or FOREIGN KEY already handles (e.g. just to block SOLUONG < 0, or to set a default value). A Trigger runs after the DML operation has already happened, adding extra processing cost - only use a Trigger when the logic goes beyond what a Constraint can do - for example needing to automatically update data in another table, or checking a condition based on data across multiple tables.Don't allow adding an invoice if the customer doesn't exist:
IF EXISTS checks whether data matching a condition exists; IF NOT EXISTS checks the opposite - that no matching data exists.
Tables HOADON(MAHD, TRIGIA) and CTHD(MAHD, MASP, SOLUONG, DONGIA). When INSERT INTO CTHD happens, the Trigger recalculates TRIGIA and runs UPDATE HOADON.
A more advanced problem: adding/editing an invoice ➜ the Trigger computes the total value ➜ updates DOANHSO in KHACHHANG.
Don't allow deleting an employee if they've already made sales:
When adding a new customer, if NGDK isn't provided, the system automatically uses the current date via GETDATE().
07 · TRIGGERS THAT HANDLE MULTIPLE ROWS
Students often write:
DECLARE @MAKH CHAR(4);
SELECT @MAKH = MAKH
FROM INSERTED;This is easy to understand when illustrating a single record, but a single INSERT INTO ... SELECT ... statement can insert many rows at once - and as section 05 already pointed out, SELECT @x = column FROM inserted raises no error at all with multiple rows, it just silently drops the rest.
These are 2 common ways to write the same data-validation problem in a Trigger:
Local variable (DECLARE + SELECT @x = ...) | IF EXISTS/NOT EXISTS + JOIN | |
|---|---|---|
| Correct with multiple rows? | Only correct when the operation is guaranteed to affect exactly 1 row | Always correct, whether 1 row or many |
| How it checks | Assign a value into a variable, then compare | Check "does a violating row exist" across the whole inserted/deleted set |
| When to use | Simple illustrative examples, or when it's 100% guaranteed to be 1 row | Should be the default for every data-validation Trigger |
-- Local variable - only safe with exactly 1 row
DECLARE @SoLuong INT;
SELECT @SoLuong = SOLUONG FROM inserted;
IF (@SoLuong < 0)
BEGIN
RAISERROR (N'Quantity cannot be negative.', 16, 1);
ROLLBACK TRANSACTION;
RETURN;
END-- IF EXISTS - correct for any number of rows
IF EXISTS (SELECT * FROM inserted WHERE SOLUONG < 0)
BEGIN
RAISERROR (N'Quantity cannot be negative.', 16, 1);
ROLLBACK TRANSACTION;
RETURN;
ENDIn the BTTH5 exercises, most questions should present IF EXISTS/NOT EXISTS as the main answer - the local-variable approach is still kept in the answer key as an "Alternative approach" for reference, with a clear note on its limitation: it's only correct when the operation affects exactly 1 row.
08 · BUILDING A TRIGGER, STEP BY STEP
When facing a Trigger exercise, follow these 6 steps:
Dropping a Trigger:
DROP TRIGGER trg_AfterInsert_KhachHang;Viewing Trigger information:
SELECT *
FROM INFORMATION_SCHEMA.TRIGGERS;Week 1: creating a database. Week 2: querying. Week 3: advanced querying. Week 4: processing data (CAST/CASE/Subquery). Week 5: automation & control with Triggers, INSERTED/DELETED, RAISERROR/ROLLBACK. Per the course outline, Week 6 will be review and a practical exam - the timing and format of the exam will be announced later.
09 · PRACTICE EXERCISES
See the full assignment, attached files, and detailed answers (access code required) for BTTH5.
Extra Trigger problems to practice with - not part of the BTTH5 submission. Each has a hidden hint - click to reveal.
Create a Trigger: don't allow SOLUONG < 0 in SANPHAM.
When a customer's name changes, display: 'Customer name changed from "..." to "..."'.
Don't allow deleting an employee if they already have invoices.
When an invoice line item is added, automatically update the invoice total.
When an invoice is added or updated, automatically update the customer's sales total.