A trigger is a stored procedure that fires automatically in response to INSERT, UPDATE, or DELETE on a table
BEFORE triggers can modify or reject the incoming row; AFTER triggers run after the DML succeeds
ROW-level triggers fire once per affected row; STATEMENT-level triggers fire once per DML statement
Triggers are invisible to the caller — they execute silently, making debugging difficult
Cascading triggers (trigger → triggers another trigger) are the most dangerous pattern in production databases
Use triggers for audit logging and integrity enforcement; avoid them for business logic that belongs in application code
✦ Definition~90s read
What is SQL Triggers?
SQL triggers are procedural code that automatically executes in response to specific database events—INSERT, UPDATE, DELETE—on a table or view. They exist to enforce business rules, maintain audit trails, or synchronize related tables without requiring application-level logic.
★
A trigger is like a tripwire in a warehouse.
Think of them as database-side event handlers that fire before or after the triggering statement, with full access to the old and new row values. The killer feature is that they run inside the same transaction as the triggering statement, meaning any failure in the trigger rolls back the entire operation.
This atomicity is both their superpower and their curse: a poorly written trigger can silently bring your production database to its knees with cascading deadlocks or unexpected row locks, especially under concurrent load.
In the ecosystem, triggers compete with application-level logic, materialized views, and change data capture (CDC) tools like Debezium or AWS DMS. You should NOT use triggers when you need debuggability—they're invisible to most ORMs and application logs—or when you're building a system that might need to migrate between database vendors, since trigger syntax varies wildly between PostgreSQL, MySQL, SQL Server, and Oracle.
They're also a poor fit for complex multi-step workflows that should be explicit in your application code. Where triggers shine is in enforcing invariants that must hold regardless of how data enters the system: audit logging, computed columns, or preventing orphaned rows in a denormalized schema.
The real danger with triggers is cascading: a trigger on table A updates table B, which has a trigger that updates table C, which has a trigger that updates table A. This creates a circular dependency that can deadlock under concurrent transactions, especially when row-level locking interacts with trigger execution order.
Production patterns like audit logging avoid this by writing to a separate append-only table with no triggers, using minimal columns, and never reading from the audit table inside the trigger. The 45-minute cascading deadlock scenario typically happens when a trigger acquires a lock on a row that another concurrent transaction holds, and both transactions are waiting on each other—exactly the kind of problem that's invisible in development but catastrophic under production load.
Plain-English First
A trigger is like a tripwire in a warehouse. You post a rule: 'whenever someone moves inventory from shelf A to shelf B, automatically log it in the logbook.' The warehouse worker does not need to remember to write in the logbook — the tripwire fires the logging action automatically. SQL triggers work the same way — you define them once and the database fires them automatically whenever the specified data change happens.
Every production database eventually reaches a moment where you need something to happen automatically when data changes — log who changed a salary record, prevent a negative inventory count, or replicate a row to an audit table without trusting every application developer to remember. Triggers are the database's mechanism for this.
But triggers are one of the most misused features in SQL. They execute invisibly to the calling application, complicate debugging, can cascade into catastrophic chains, and can silently degrade INSERT/UPDATE/DELETE performance when they grow complex. Understanding their mechanics — and their failure modes — is what separates engineers who use them appropriately from those who create unmaintainable databases.
Why SQL Triggers Are a Double-Edged Sword
An SQL trigger is a procedural block that automatically executes in response to a data change event — INSERT, UPDATE, or DELETE — on a specific table. Unlike constraints or application-level logic, triggers live inside the database engine and fire within the same transaction as the triggering statement. This means they inherit the transaction's locks, isolation level, and rollback scope, making them invisible but powerful modifiers of data integrity.
Triggers execute either BEFORE or AFTER the triggering event, and can be row-level (once per affected row) or statement-level (once per SQL statement). Row-level triggers are O(n) relative to the number of rows modified, which matters when a single UPDATE touches 100,000 rows — each trigger invocation adds latency and lock duration. Triggers also have access to OLD and NEW pseudo-rows, enabling audit trails, derived column updates, or cascading changes without application code.
Use triggers when you must enforce business rules that span tables and cannot be deferred to the application — for example, maintaining a materialized aggregate count, logging all deletions for compliance, or preventing orphaned rows in a denormalized schema. Avoid them for logic that can be expressed as a CHECK constraint, a foreign key with CASCADE, or a computed column. In production, triggers are often the root cause of deadlocks and unexpected write amplification because they execute under the caller's transaction context, silently extending lock duration.
⚠ Trigger ≠ Constraint
A trigger cannot enforce referential integrity reliably — it runs per row, not per statement, and can miss violations if the triggering statement is rolled back partially.
📊 Production Insight
A team added a row-level AFTER INSERT trigger on an orders table to update a customer's last_order_date. A bulk import of 50,000 orders caused the trigger to hold row-level locks on the customers table for the entire import transaction, leading to a cascading deadlock with concurrent read replicas. The rule: never use row-level triggers on high-volume write paths — use statement-level triggers or batch jobs instead.
🎯 Key Takeaway
Triggers run in the caller's transaction — they extend lock duration and can cause deadlocks.
Row-level triggers are O(n) per modified row — avoid them on bulk operations.
Prefer constraints, computed columns, or application logic over triggers unless you need cross-table side effects within the same transaction.
thecodeforge.io
Sql Triggers
BEFORE vs AFTER Triggers — Choosing the Right Timing
A BEFORE trigger fires before the DML operation executes. You can inspect and modify the incoming row values, or raise an error to reject the operation entirely. BEFORE triggers are the right tool for input validation and data normalisation that must happen at the database layer.
An AFTER trigger fires after the DML operation has succeeded. The row is already in the table when the trigger runs. AFTER triggers are the right tool for audit logging, cascading updates to related tables, and any side effect that should only happen if the primary operation succeeded.
In SQL Server, the equivalent of BEFORE triggers are INSTEAD OF triggers — they replace the DML operation entirely. You must explicitly perform the INSERT, UPDATE, or DELETE inside the trigger body if you want the original operation to proceed. They are most commonly used on views to make views updatable.
before_after_triggers.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
-- BEFORE INSERT trigger (PostgreSQL): normalise email case and validateCREATEORREPLACEFUNCTIONnormalise_user_before_insert()
RETURNSTRIGGERAS $$
BEGIN-- Normalise email to lowercase at DB layer regardless of application inputNEW.email = LOWER(TRIM(NEW.email));
-- Reject obviously invalid emails at DB layer as a last defenceIFNEW.email NOTLIKE'%@%'THENRAISEEXCEPTION'Invalid email format: %', NEW.email;
ENDIF;
RETURNNEW; -- RETURN NEW is required in BEFORE triggers to allow the INSERTEND;
$$ LANGUAGE plpgsql;
CREATETRIGGER trg_normalise_user_before_insert
BEFOREINSERTON users
FOREACHROWEXECUTEFUNCTIONnormalise_user_before_insert();
-- AFTER INSERT trigger (PostgreSQL): audit log the creationCREATEORREPLACEFUNCTIONlog_user_creation()
RETURNSTRIGGERAS $$
BEGININSERTINTOaudit_log (table_name, action, record_id, changed_at, new_values)
VALUES ('users', 'INSERT', NEW.user_id, NOW(),
jsonb_build_object('email', NEW.email, 'created_at', NEW.created_at));
RETURNNULL; -- AFTER triggers: return value is ignored; use NULLEND;
$$ LANGUAGE plpgsql;
CREATETRIGGER trg_log_user_creation
AFTERINSERTON users
FOREACHROWEXECUTEFUNCTIONlog_user_creation();
Output
-- INSERT with uppercase email:
INSERT INTO users (email, name) VALUES ('ALICE@EXAMPLE.COM', 'Alice');
In PostgreSQL BEFORE triggers, you must RETURN NEW to allow the operation to proceed with the (possibly modified) row. Returning NULL cancels the operation. In AFTER triggers, the return value is ignored — use RETURN NULL. Getting this wrong is a common source of INSERT/UPDATE mysteriously producing no rows.
📊 Production Insight
BEFORE triggers for normalisation (email lowercase, phone formatting) are valuable because they enforce consistency regardless of which application path writes the data.
AFTER triggers for audit logs are the canonical trigger use case — they fire only on successful DML and the log entry is in the same transaction.
Any trigger failure rolls back the entire triggering transaction — a bug in your audit trigger will cause the original INSERT to fail.
🎯 Key Takeaway
BEFORE = validate and transform before the row is written; AFTER = react to a successful write.
In BEFORE triggers: RETURN NEW to proceed, RETURN NULL to cancel the operation.
A trigger bug rolls back the triggering transaction — test trigger error paths explicitly.
Audit Logging with Triggers — The Production Pattern
Audit logging is the most legitimate and common trigger use case. The requirement: every change to sensitive data (salaries, prices, access permissions) must be recorded with who changed it, when, what it was before, and what it became after. Doing this reliably at the application layer is fragile — developers forget to add logging to new code paths. A trigger guarantees it happens regardless of how the data change occurs.
The standard pattern uses the INSERTED and DELETED virtual tables in SQL Server, or the OLD and NEW row references in PostgreSQL and MySQL. These give you the pre- and post-change values inside the trigger body. Store them in a generic audit table with a JSONB column (PostgreSQL) or a structured audit table with before/after value columns.
-- After UPDATE employees SET salary = 95000 WHERE employee_id = 42:
audit_log row:
action: UPDATE
record_id: 42
changed_by: 'jane.admin'
old_values: {"salary": 85000}
new_values: {"salary": 95000}
changed_at: 2024-03-15 14:22:31+00
💡Pass Application User Context with SET LOCAL
Audit logs need to capture who made the change. Database connections don't automatically carry application user identity. Use SET LOCAL app.current_user_id = '42' at the start of each request transaction, then read it inside the trigger with current_setting('app.current_user_id', true). This threads user identity from the application into the database trigger context.
📊 Production Insight
Audit trigger audit_log tables grow fast — add a composite index on (table_name, record_id) and a partitioned index on changed_at from day one.
The trigger function is reusable across tables (change the table name and record_id column) — write it once, attach to multiple tables.
For compliance requirements (GDPR, SOX, HIPAA), trigger-based audit logs are often required because they cannot be bypassed by application code.
🎯 Key Takeaway
Audit triggers are the canonical trigger use case — they guarantee logging regardless of which code path makes the change.
Pass application user context via SET LOCAL + current_setting() to capture who changed what.
Audit tables need indexes on (table_name, record_id) and changed_at from the start — they will grow to millions of rows.
thecodeforge.io
Sql Triggers
Cascading Triggers and When NOT to Use Triggers
A cascading trigger occurs when Trigger A fires a DML operation that activates Trigger B, which in turn fires Trigger C, and so on. Most databases allow cascading triggers up to a configured depth (SQL Server defaults to 32 levels). Cascades make debugging a nightmare: a single INSERT can cause dozens of side effects that are invisible to the calling application and to monitoring tools that track queries.
The worst cascading scenario is a trigger cycle: Trigger A on table T1 updates table T2, Trigger B on T2 updates T1, which fires Trigger A again — an infinite loop that runs until the recursion depth limit throws an error. This type of bug is introduced gradually as triggers are added over time without a dependency map.
Business logic does not belong in triggers. Logic like 'when an order is placed, send a welcome email' or 'recalculate the customer's tier based on total spend' has external dependencies, needs to be tested independently, and needs to be deployed with the rest of the application code. Putting it in a trigger makes it invisible, hard to test, and impossible to roll back independently of the database schema.
trigger_safety_patterns.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
-- DANGEROUS: Trigger on orders that modifies customer_stats-- which has another trigger that modifies orders -- circular!CREATETRIGGER trg_orders_update_stats
AFTERINSERTON orders
FOREACHROWEXECUTEFUNCTIONupdate_customer_stats(); -- updates customer_stats table-- If customer_stats has a trigger that updates orders...-- circular cascade -- do not do this-- SAFE PATTERN: Use a flag to prevent recursive firing (SQL Server)CREATETRIGGER trg_orders_safe
AFTERINSERTON orders
FOREACHROWASBEGINIF TRIGGER_NESTLEVEL() > 1-- already inside a trigger call chainRETURN;
-- ... safe to proceedEND;
-- PostgreSQL equivalent: check trigger nesting levelCREATEORREPLACEFUNCTIONsafe_trigger_function()
RETURNSTRIGGERAS $$
BEGINIFpg_trigger_depth() > 1THENRETURNNEW; -- skip to prevent cascadeENDIF;
-- ... proceed with side effectRETURNNEW;
END;
$$ LANGUAGE plpgsql;
-- List all triggers and their dependencies (PostgreSQL)SELECT
event_object_table AS table_name,
trigger_name,
event_manipulation AS event,
action_timing AS timing
FROM information_schema.triggers
WHERE trigger_schema = 'public'ORDERBY event_object_table, action_timing;
Output
table_name | trigger_name | event | timing
employees | trg_audit_employees | INSERT | AFTER
employees | trg_audit_employees | UPDATE | AFTER
employees | trg_audit_employees | DELETE | AFTER
orders | trg_normalise_order_before | INSERT | BEFORE
users | trg_normalise_user_before_insert| INSERT | BEFORE
⚠ Never Put Business Logic in Triggers
Triggers are invisible to the application layer. Business logic in triggers cannot be unit tested without a database, cannot be rolled back independently of schema changes, cannot be traced in application logs, and cannot be disabled during a hotfix. Logic that has external dependencies (email sending, API calls) is impossible to put in a trigger safely. Keep triggers for database-layer concerns only: data validation, normalisation, and audit logging.
📊 Production Insight
Always map your trigger dependency graph before any migration — ALTER TABLE t DISABLE TRIGGER ALL is the emergency lever for migrations in trigger-heavy databases.
pg_trigger_depth() (PostgreSQL) and TRIGGER_NESTLEVEL() (SQL Server) let you guard against recursive trigger chains.
Document every trigger with a comment block: what it fires on, what it does, and what other triggers it might activate.
🎯 Key Takeaway
Cascading triggers are the most dangerous pattern in production SQL — map the dependency graph before any migration.
Business logic does not belong in triggers — visibility, testability, and deployability all suffer.
Audit logging and data normalisation are the appropriate trigger use cases — everything else is usually better in application code.
Row-Level vs Statement-Level Triggers — The Hidden Performance Killer
Most developers don't know there are two distinct execution modes for triggers. Row-level triggers fire once per affected row. Statement-level triggers fire once per SQL statement regardless of how many rows changed. The difference will destroy your production database if you get it wrong. Imagine an UPDATE that touches 100,000 rows. A row-level trigger runs 100,000 times. A statement-level trigger runs once. That's the difference between a 50ms operation and a 5-minute outage. MySQL only supports row-level triggers. PostgreSQL and Oracle give you both. SQL Server defaults to statement-level. Always check your dialect's default. Batch operations with row-level triggers are the #1 cause of unexpected performance degradation in trigger-heavy systems. Test with realistic dataset sizes, not your 12-row development table. Profile execution plans. That row-level audit trigger you wrote yesterday? It's now the bottleneck for your nightly bulk update job.
trigger_benchmark.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge
-- PostgreSQL: Statement-level trigger for batch operations-- Note: No FOR EACH ROW clause makes this statement-levelCREATEORREPLACEFUNCTIONlog_bulk_update()
RETURNSTRIGGERAS $$
BEGININSERTINTOaudit_log (event_type, affected_rows, timestamp)
VALUES ('BULK_UPDATE', TG_NARG, NOW());
RETURNNULL;
END;
$$ LANGUAGE plpgsql;
CREATETRIGGER trg_bulk_update_audit
AFTERUPDATEON orders
-- No FOR EACH ROW → fires once per statementEXECUTEFUNCTIONlog_bulk_update();
Output
INSERT 0 1
-- Single audit row regardless of 50,000 updated orders
⚠ Production Trap:
Row-level triggers on tables with >500,000 rows and batch updates will turn your ETL pipeline into a crawl. Always ask: do I need per-row granularity or just need to know the operation happened?
🎯 Key Takeaway
Statement-level triggers for batch operations. Row-level triggers for per-row validation. Mixing them up causes outages.
Error Handling and Transaction Safety — Don't Let a Trigger Take Down a Billion-Dollar Transaction
Triggers execute inside the calling transaction. A trigger failure rolls back the entire transaction. That sounds safe, but here's the reality: a poorly handled validation trigger on a customers table can cancel a payments batch, a user registration, and a subscription renewal in one atomic failure. Your trigger's error handling must be bulletproof. Never catch exceptions silently. Always use SIGNAL or RAISE with descriptive messages. Store trigger-specific errors in a separate error log table — not the main application log. Design for idempotency. Triggers that run multiple times (replays, retries) should produce the same result. Test with concurrent writes. Race conditions in triggers cause deadlocks. Put a timeout on trigger execution. Fifteen seconds is the absolute max for customer-facing operations. Document every trigger with its transaction boundary implications. Your team needs to know: 'This trigger on the orders table can take down the entire checkout flow if it fails.'
-- Error logged in trigger_error_log for investigation
🔥Architecture Decision:
Use BEFORE triggers for validation (fail fast, cheap). Use AFTER triggers for logging (non-critical, can degrade to best-effort). Never do expensive computation in AFTER triggers.
🎯 Key Takeaway
Your trigger is part of a transaction. A failed trigger kills the whole operation. Log errors externally, fail explicitly.
● Production incidentPOST-MORTEMseverity: high
A Cascading Trigger Chain Caused a 45-Minute Deadlock During a Schema Migration
Symptom
A schema migration that should have run in 2 minutes locked for 45 minutes. The DBA team had to manually identify and kill the blocking sessions. Several application endpoints returned 500 errors during the window.
Assumption
The team knew the database had triggers but assumed they were simple audit loggers. No one had mapped the trigger dependency chain before running the migration.
Root cause
Trigger A on the orders table fired an UPDATE on the order_summary table. Trigger B on order_summary fired an INSERT into the notifications table. Trigger C on notifications fired an UPDATE back on the orders table — circular dependency. The migration held a lock on orders while executing, trigger C tried to UPDATE orders again, creating a deadlock.
Fix
Disabled non-essential triggers for the duration of the migration using ALTER TABLE orders DISABLE TRIGGER ALL. Audited and documented the full trigger dependency graph. Rewrote the notification trigger as application-level logic. Added a trigger dependency comment header to each remaining trigger.
Key lesson
Map your trigger dependency graph before any schema migration — unknown cascades cause unknown deadlocks
Never write triggers that can fire other triggers without explicit documentation of the chain depth
For migrations in trigger-heavy databases, disable triggers temporarily and re-enable after: ALTER TABLE t DISABLE TRIGGER ALL
Production debug guideDiagnosing unexpected behaviour caused by triggers4 entries
Symptom · 01
DML operation is slower than expected with no obvious query explanation
→
Fix
List all triggers on the table: SELECT trigger_name, event_manipulation FROM information_schema.triggers WHERE event_object_table = 'your_table'. Check trigger body for expensive operations — JOINs, aggregates, external table writes. A slow trigger is invisible to application-level profiling.
Symptom · 02
Data changes that no application code appears to be making
→
Fix
Triggers are the first suspect. List all triggers on affected tables. Add a temporary logging statement inside the trigger: INSERT INTO trigger_debug_log (event, ts) VALUES ('trigger_name fired', NOW()). Check trigger_debug_log to confirm the trigger is firing.
Symptom · 03
Trigger is firing but the expected side effect is not happening
→
Fix
Check if the trigger is inside a transaction that is being rolled back by the caller. Trigger DML is part of the triggering transaction — if the outer transaction rolls back, all trigger side effects roll back too. Check INSTEAD OF triggers on views — they replace, not supplement, the original DML.
Symptom · 04
UPDATE trigger fires but OLD and NEW values appear incorrect
→
Fix
In PostgreSQL: use OLD.column and NEW.column inside the trigger function. In MySQL: use OLD.column and NEW.column directly in the trigger body. In SQL Server: use the INSERTED and DELETED virtual tables — INSERTED has new values, DELETED has old values for UPDATE.
Trigger Type
Fires When
Can Modify Row?
Best For
BEFORE INSERT/UPDATE
Before the DML executes
Yes — modify NEW values
Input validation, data normalisation
AFTER INSERT/UPDATE/DELETE
After DML succeeds
No — row is already written
Audit logging, cascading updates to related tables
INSTEAD OF (SQL Server)
Replaces the DML
Yes — must implement DML explicitly
Making views updatable
STATEMENT-level
Once per DML statement
No row-level access
Table-level statistics, bulk operation logging
⚙ Quick Reference
5 commands from this guide
File
Command / Code
Purpose
before_after_triggers.sql
CREATE OR REPLACE FUNCTION normalise_user_before_insert()
BEFORE vs AFTER Triggers
audit_log_trigger.sql
CREATE TABLE audit_log (
Audit Logging with Triggers
trigger_safety_patterns.sql
CREATE TRIGGER trg_orders_update_stats
Cascading Triggers and When NOT to Use Triggers
trigger_benchmark.sql
CREATE OR REPLACE FUNCTION log_bulk_update()
Row-Level vs Statement-Level Triggers
safe_trigger.sql
CREATE TABLE trigger_error_log (
Error Handling and Transaction Safety
Key takeaways
1
Triggers execute invisibly to the caller
audit logging and data normalisation are their appropriate use cases, not business logic.
2
BEFORE triggers validate and transform incoming data; AFTER triggers log and cascade after successful writes.
A trigger bug rolls back the triggering transaction
test failure paths explicitly, especially in audit triggers.
Common mistakes to avoid
3 patterns
×
Creating triggers that fire other triggers without documenting the dependency chain
Symptom
Schema migrations cause unexpected deadlocks or infinite recursion errors; data changes produce side effects that no application code appears to be responsible for
Fix
Map all trigger dependencies before any migration. Use pg_trigger_depth() (PostgreSQL) or TRIGGER_NESTLEVEL() (SQL Server) to guard against unintended recursion. Document each trigger with a comment block listing what it activates.
×
Putting business logic with external dependencies in triggers
Symptom
Trigger fails silently or rolls back the triggering transaction when an external dependency is unavailable; logic cannot be unit tested or deployed independently
Fix
Move external-dependency logic to application code or a message queue. Triggers should only contain database-layer operations (INSERT, UPDATE on related tables, raising exceptions) — never network calls, file writes, or complex business rules.
×
Not accounting for the trigger in performance profiling
Symptom
INSERT or UPDATE operations are slower than the query plan explains — the overhead is invisible in the application's query profiler
Fix
List all triggers on the table with information_schema.triggers. Profile the trigger function separately. Complex AFTER triggers with JOINs or aggregates are the most common hidden performance sink in production databases.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01JUNIOR
What is the difference between a BEFORE and AFTER trigger, and when woul...
Q02SENIOR
What are the risks of cascading triggers and how do you guard against th...
Q03JUNIOR
When would you use a trigger for audit logging versus handling it in app...
Q01 of 03JUNIOR
What is the difference between a BEFORE and AFTER trigger, and when would you use each?
ANSWER
A BEFORE trigger fires before the DML operation executes. It has access to the incoming row values (NEW) and can modify them or raise an exception to reject the operation. Use BEFORE triggers for input validation (rejecting negative prices) and data normalisation (lowercasing emails). An AFTER trigger fires after the DML has succeeded — the row is already in the table. AFTER triggers cannot modify the triggering row. Use them for audit logging (the log should only exist if the change succeeded), cascading updates to related tables, and any side effect that should be conditional on the primary operation succeeding. In SQL Server, INSTEAD OF triggers replace the DML operation entirely and are primarily used to make views updatable.
Q02 of 03SENIOR
What are the risks of cascading triggers and how do you guard against them?
ANSWER
Cascading triggers occur when a trigger fires a DML operation that activates another trigger, potentially creating a chain reaction or circular dependency. Risks: deadlocks when a circular chain tries to lock a row already held by the triggering transaction; performance degradation from compounded DML operations per user operation; debugging complexity because the side effects are invisible to the calling application. Guards: use pg_trigger_depth() (PostgreSQL) or TRIGGER_NESTLEVEL() (SQL Server) at the top of trigger functions to detect and exit if already inside a trigger chain. Before any schema migration on trigger-heavy tables, list all triggers and map the dependency graph. Use ALTER TABLE t DISABLE TRIGGER ALL for migrations where trigger execution would cause conflicts, then re-enable after.
Q03 of 03JUNIOR
When would you use a trigger for audit logging versus handling it in application code?
ANSWER
Triggers are superior for audit logging when: (1) multiple application code paths can modify the table and you cannot trust all of them to implement logging consistently — a trigger guarantees logging regardless of how the change happens, including direct SQL updates by DBAs; (2) compliance requirements mandate that the audit log cannot be bypassed by application code; (3) the log needs to capture the exact before and after values at the database level rather than at the application level. Application-level logging is preferable when: the log requires business context that is not available at the database layer (session user role, feature flag state, request ID); the logging operation has external dependencies (writing to Elasticsearch, sending to a message queue); or when you need to test the logging logic independently of the database.
01
What is the difference between a BEFORE and AFTER trigger, and when would you use each?
JUNIOR
02
What are the risks of cascading triggers and how do you guard against them?
SENIOR
03
When would you use a trigger for audit logging versus handling it in application code?
JUNIOR
FAQ · 2 QUESTIONS
Frequently Asked Questions
01
Can a trigger see the values before and after an UPDATE?
Yes. In PostgreSQL and MySQL, inside an UPDATE trigger you have access to OLD.column (the previous value) and NEW.column (the new value). In SQL Server, you access DELETED (old values) and INSERTED (new values) virtual tables. These are essential for audit logging that records what changed.
Was this helpful?
02
Do triggers fire when data is imported with BULK INSERT or COPY?
It depends on the database and the import method. In SQL Server, BULK INSERT with the FIRE_TRIGGERS option fires triggers; without it, triggers are bypassed. In PostgreSQL, COPY fires triggers by default. Always verify trigger behaviour for your specific bulk import method — silently skipped audit triggers during bulk imports are a common compliance gap.