29. Triggers & Audit Logging -- Automated Actions
Level: Advanced SQL Server for real projects
- Trigger syntax (CREATE TRIGGER)
- AFTER triggers (execute after INSERT/UPDATE/DELETE)
- INSTEAD OF triggers (replace action)
- INSERTED and DELETED pseudo-tables
- Audit logging with triggers
- Prevent invalid operations
- Trigger performance impact
- Common trigger mistakes
- Best practices
Triggers automate actions when data changes. Log every edit. Enforce rules. This article teaches automated database logic.
Why Triggers Matter for Backend Developers
Automated enforcement without backend code:
Requirement: "Log every student insert, update, delete
Prevent exam result insert without valid student"
v
Without trigger: Backend must handle logging and validation
WITH trigger: Database handles automatically
v
Consistent enforcement regardless of client
Audit trail automatic
Triggers enforce rules at database layer.
Trigger Syntax
CREATE TRIGGER TriggerName
ON TableName
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
-- Trigger logic
-- Access INSERTED and DELETED pseudo-tables
END;
Key concepts:
- AFTER: Fires after INSERT/UPDATE/DELETE
- INSTEAD OF: Fires instead of action (can prevent it)
- INSERTED: Contains new values
- DELETED: Contains old values
Example 1: Audit Log Trigger (INSERT)
Log every new student:
CREATE TRIGGER trg_Student_Insert
ON Student
AFTER INSERT
AS
BEGIN
INSERT INTO AuditLog (Action, EntityName, EntityId, OldValues, NewValues, ChangedBy, ChangedAt)
SELECT
'INSERT',
'Student',
i.StudentId,
NULL,
CONCAT('Name=', i.Name, ', ClassName=', i.ClassName),
SYSTEM_USER,
GETDATE()
FROM INSERTED i;
END;
-- When you insert student:
INSERT INTO Student (Name, ClassName, Status) VALUES ('Ravi Kumar', '10-A', 'Active');
-- Trigger automatically logs to AuditLog
INSERTED table: Contains new student row. Trigger logs it.
Example 2: Audit Log Trigger (UPDATE)
Track changes to student data:
CREATE TRIGGER trg_Student_Update
ON Student
AFTER UPDATE
AS
BEGIN
INSERT INTO AuditLog (Action, EntityName, EntityId, OldValues, NewValues, ChangedBy, ChangedAt)
SELECT
'UPDATE',
'Student',
d.StudentId,
CONCAT('Name=', d.Name, ', ClassName=', d.ClassName),
CONCAT('Name=', i.Name, ', ClassName=', i.ClassName),
SYSTEM_USER,
GETDATE()
FROM DELETED d
INNER JOIN INSERTED i ON d.StudentId = i.StudentId;
END;
-- When you update student:
UPDATE Student SET ClassName = '11-A' WHERE StudentId = 1;
-- Trigger logs: DELETED has old ClassName, INSERTED has new ClassName
DELETED: Old values. INSERTED: New values. Compare for audit.
Example 3: Prevent Invalid Operation (INSTEAD OF)
Prevent exam result insert without valid student:
CREATE TRIGGER trg_ExamResult_InsertValidation
ON ExamResult
INSTEAD OF INSERT
AS
BEGIN
-- Check if student exists
IF EXISTS (
SELECT 1 FROM INSERTED i
LEFT JOIN Student s ON i.StudentId = s.StudentId
WHERE s.StudentId IS NULL
)
BEGIN
RAISERROR('Invalid student ID. Exam result not inserted.', 16, 1);
ROLLBACK;
RETURN;
END;
-- If valid, insert
INSERT INTO ExamResult (StudentId, ExamId, MarksObtained, IsAbsent)
SELECT StudentId, ExamId, MarksObtained, IsAbsent FROM INSERTED;
END;
-- Try to insert with invalid student:
INSERT INTO ExamResult (StudentId, ExamId, MarksObtained)
VALUES (999, 1, 85);
-- Trigger blocks: ERROR: Invalid student ID
INSTEAD OF: Replace action. Validate before allowing insert.
Example 4: Prevent Deletion (INSTEAD OF DELETE)
Prevent student deletion (soft delete instead):
CREATE TRIGGER trg_Student_DeletePrevent
ON Student
INSTEAD OF DELETE
AS
BEGIN
-- Instead of deleting, mark as inactive
UPDATE Student
SET Status = 'Inactive'
WHERE StudentId IN (SELECT StudentId FROM DELETED);
-- Log the action
INSERT INTO AuditLog (Action, EntityName, EntityId, ChangedBy, ChangedAt)
SELECT
'DELETE (soft)',
'Student',
d.StudentId,
SYSTEM_USER,
GETDATE()
FROM DELETED d;
END;
-- Try to delete student:
DELETE FROM Student WHERE StudentId = 1;
-- Trigger: Sets Status = 'Inactive' instead of deleting
-- Row still in table (soft delete)
INSTEAD OF DELETE: Prevent permanent deletion. Soft delete instead.
Example 5: Prevent Duplicate Exam Results
Block inserting same exam twice:
CREATE TRIGGER trg_ExamResult_NoDuplicate
ON ExamResult
INSTEAD OF INSERT
AS
BEGIN
-- Check for existing result
IF EXISTS (
SELECT 1 FROM INSERTED i
WHERE EXISTS (
SELECT 1 FROM ExamResult er
WHERE er.StudentId = i.StudentId
AND er.ExamId = i.ExamId
)
)
BEGIN
RAISERROR('Exam result already exists for this student.', 16, 1);
ROLLBACK;
RETURN;
END;
-- Insert valid results
INSERT INTO ExamResult (StudentId, ExamId, MarksObtained, IsAbsent)
SELECT StudentId, ExamId, MarksObtained, IsAbsent FROM INSERTED;
END;
Prevents duplicate insertions at database level.
Example 6: Auto-Update Timestamp
Automatically update LastModified:
CREATE TRIGGER trg_Student_UpdateTimestamp
ON Student
AFTER UPDATE
AS
BEGIN
UPDATE Student
SET LastModified = GETDATE()
WHERE StudentId IN (SELECT StudentId FROM INSERTED);
END;
-- Update student:
UPDATE Student SET Name = 'Ravi Kumar Updated' WHERE StudentId = 1;
-- Trigger: Automatically sets LastModified = GETDATE()
Trigger adds timestamp without backend code.
Example 7: Cascading Updates (Trigger vs FK)
Sync class count when student added/removed:
CREATE TRIGGER trg_Student_UpdateClassCount
ON Student
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
-- Update class counts
UPDATE Class
SET StudentCount = (
SELECT COUNT(*) FROM Student
WHERE ClassName = Class.ClassName
)
WHERE ClassName IN (
SELECT DISTINCT ClassName FROM INSERTED
UNION
SELECT DISTINCT ClassName FROM DELETED
);
END;
-- When student added:
INSERT INTO Student (Name, ClassName) VALUES ('New Student', '10-A');
-- Trigger: Updates Class.StudentCount for 10-A
Trigger maintains denormalized data (count field).
Common Trigger Mistakes
Mistake 1: Trigger causing infinite loop
Wrong:
CREATE TRIGGER trg_Student_Update
ON Student
AFTER UPDATE
AS
BEGIN
UPDATE Student SET LastModified = GETDATE();
-- Trigger fires on UPDATE
-- Trigger UPDATE fires trigger again (infinite loop)
END;
Result: Recursion, performance crash.
Fix: Add recursion check:
IF @@NESTLEVEL > 1
RETURN;
UPDATE Student SET LastModified = GETDATE();
Or use DISABLED trigger and manual update.
Mistake 2: Trigger using cursor (slow)
Wrong:
CREATE TRIGGER trg_ExamResult_Insert
ON ExamResult
AFTER INSERT
AS
BEGIN
DECLARE c CURSOR FOR SELECT StudentId FROM INSERTED;
OPEN c;
FETCH NEXT FROM c INTO @StudentId;
WHILE @@FETCH_STATUS = 0
BEGIN
-- Process each row (cursor = slow)
FETCH NEXT FROM c INTO @StudentId;
END;
CLOSE c;
END;
-- Slow for bulk inserts
Fix: Use set-based logic:
CREATE TRIGGER trg_ExamResult_Insert
ON ExamResult
AFTER INSERT
AS
BEGIN
-- Set-based: All rows at once (fast)
INSERT INTO AuditLog (...)
SELECT ... FROM INSERTED;
END;
Mistake 3: INSTEAD OF with row-by-row logic
Wrong:
CREATE TRIGGER trg_Student_Insert
INSTEAD OF INSERT
AS
BEGIN
-- Doesn't handle multiple inserts well
INSERT INTO Student SELECT * FROM INSERTED;
UPDATE CLASS SET Count = Count + @@ROWCOUNT;
-- @@ROWCOUNT applies to last statement (UPDATE, not INSERT)
END;
Fix: Reference INSERTED properly:
INSERT INTO Student SELECT * FROM INSERTED;
UPDATE Class
SET StudentCount = StudentCount + (SELECT COUNT(*) FROM INSERTED)
WHERE ClassName IN (SELECT DISTINCT ClassName FROM INSERTED);
Mistake 4: Trigger without error handling
Wrong:
CREATE TRIGGER trg_ExamResult_Insert
INSTEAD OF INSERT
AS
BEGIN
-- No error handling
IF (SELECT COUNT(*) FROM INSERTED) > 100
RAISERROR('Too many inserts', 16, 1);
-- But still inserts rows if no error
END;
Fix: ROLLBACK on error:
BEGIN TRY
IF (SELECT COUNT(*) FROM INSERTED) > 100
BEGIN
THROW 50001, 'Too many inserts', 1;
END;
INSERT INTO ...;
END TRY
BEGIN CATCH
ROLLBACK;
THROW;
END CATCH
Mistake 5: Performance impact (trigger on every operation)
Wrong:
-- Triggers on every INSERT, UPDATE, DELETE
CREATE TRIGGER trg1 ON Student AFTER INSERT ...;
CREATE TRIGGER trg2 ON Student AFTER UPDATE ...;
CREATE TRIGGER trg3 ON Student AFTER DELETE ...;
CREATE TRIGGER trg4 ON Student AFTER INSERT ...;
-- 4 triggers x bulk insert = slow
Result: Bulk operations slow down.
Fix: Combine logic:
CREATE TRIGGER trg_Student_All
ON Student
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
IF EXISTS (SELECT 1 FROM INSERTED)
-- Handle insert/update
IF EXISTS (SELECT 1 FROM DELETED)
-- Handle delete
END;
Best Practices for Triggers
- Use set-based logic -- Not cursors
- Keep triggers simple -- One responsibility
- Handle bulk operations -- INSERTED/DELETED can have many rows
- Avoid recursive triggers -- Check @@NESTLEVEL
- Error handling required -- ROLLBACK on validation failure
- Document purpose -- Why trigger exists
- Test performance -- Bulk operations especially
- Use instead of cascading in code -- Centralize in database
AFTER: Fires after INSERT/UPDATE/DELETE (action completed).
AFTER INSERT: Student inserted, then trigger runs
INSTEAD OF: Fires instead of action (replaces it).
INSTEAD OF DELETE: Trigger runs, original DELETE prevented
AFTER: Log after change. INSTEAD OF: Validate/prevent change.
INSERTED: New values (INSERT, UPDATE).
INSERT into Student: INSERTED contains new row
DELETED: Old values (UPDATE, DELETE).
UPDATE: DELETED has old data, INSERTED has new
DELETE: DELETED has deleted row
Compare to audit changes.
Check @@NESTLEVEL:
CREATE TRIGGER trg_Student_Update
ON Student
AFTER UPDATE
AS
BEGIN
IF @@NESTLEVEL > 1
RETURN; -- Prevent recursion
-- Trigger logic
END;
Or disable recursive triggers in server settings.
Yes:
CREATE TRIGGER trg_Student_Insert
ON Student
AFTER INSERT
AS
BEGIN
EXEC sp_LogStudentInsert; -- Call procedure
END;
Keep procedure simple, avoid nesting.
Triggers for:
- Audit logging (automatic)
- Enforcing rules (consistent)
- Preventing invalid operations
Application for:
- Complex business logic
- Multiple tables
- User-friendly error messages
Rule: Database enforces integrity (triggers). Application handles workflow (code).
Disable (keep trigger):
DISABLE TRIGGER trg_Student_Insert ON Student;
Enable:
ENABLE TRIGGER trg_Student_Insert ON Student;
Drop (delete):
DROP TRIGGER trg_Student_Insert;
Use DISABLE for testing.
Use ChatGPT, Claude, or Copilot to go deeper on Triggers and audit logging in SQL Server. Try these prompts:
"Show trigger to log every INSERT into Student table""How do I prevent duplicate exam results using INSTEAD OF?""Explain INSERTED and DELETED pseudo-tables with UPDATE example""How do I perform soft delete (prevent permanent deletion)?""Quiz me: ask 5 questions about triggers, AFTER/INSTEAD OF, audit logging"
💡 Tip: After reading this article, paste your own code into AI and ask "What could go wrong here and why?" — fastest way to find edge cases and deepen understanding.