What Is a Database Trigger?
A database trigger is a procedure that runs automatically on an insert, update, or delete — enforcing rules the application layer can't guarantee.
A database trigger is a procedure that the database runs automatically in response to a specific event on a table — a row being inserted, updated, or deleted — without any application code asking it to. You define the trigger once, attach it to a table and an event, and the database guarantees it fires every time that event happens, regardless of which application, script, or person caused it.
Why triggers exist
Application-level validation only catches changes that go through the application. Anything that writes directly to the database — a migration script, an analyst running a manual query, a different service sharing the same database — bypasses it entirely. A trigger runs at the database layer itself, so it fires no matter what wrote the row. That makes it the right tool for invariants that genuinely must never be violated, not just invariants the main application happens to enforce.
BEFORE, AFTER, and row vs statement
Triggers are defined by when they fire relative to the event, and how many times:
BEFOREtriggers run before the change is applied, and can modify the row or abort the operation entirely — useful for validation or normalizing data before it’s written.AFTERtriggers run once the change has already been committed to the table, and are typically used for side effects — logging, cascading updates, notifications — that depend on the change having actually happened.- Row-level triggers fire once per affected row.
- Statement-level triggers fire once per statement, regardless of how many rows it touched.
A simple example — logging every change to a salary column into an audit table:
CREATE TRIGGER log_salary_change
AFTER UPDATE OF salary ON employees
FOR EACH ROW
EXECUTE FUNCTION record_salary_audit();
Common uses
- Audit trails. Recording who changed what and when, independent of application logging.
- Enforcing constraints that go beyond a simple foreign key or unique constraint. Business rules that depend on values across multiple rows or tables — checks a plain constraint can’t express — are a common case for a
BEFOREtrigger that aborts the write if the rule is violated. - Keeping denormalized data in sync. Updating a cached count or summary column whenever the rows it’s derived from change, rather than recomputing it on every read.
- Cascading side effects. Triggering a downstream update in a related table as part of the same transaction, so both changes succeed or fail together — which only works because the trigger runs inside the same ACID transaction as the write that fired it.
The pitfalls
Triggers have a reputation for being one of the easiest ways to create logic nobody can find. A few reasons:
- Invisible control flow. A developer looking at an
UPDATEstatement has no way to see, from the query alone, that it also fires three triggers with their own side effects. Debugging “why did this row change” often means discovering a trigger exists at all. - Ordering is easy to get wrong. When multiple triggers fire on the same event, the order they run in matters and is often less obvious than it should be — and can differ across database engines.
- Performance overhead on every write. A trigger runs on every matching write, including bulk operations, so an expensive trigger can turn a fast bulk update into a slow one without that cost being visible anywhere in application code.
- Recursive triggers. A trigger that itself performs a write can accidentally fire another trigger, which fires another — a chain that’s easy to create by accident and hard to reason about.
Testing and observability around triggers
Because a trigger’s effects aren’t visible in the query that fired it, teams that rely on triggers tend to need extra discipline elsewhere to compensate. That usually means documenting every trigger attached to a table somewhere developers will actually see it before writing a migration, including trigger behavior explicitly in integration tests rather than assuming a unit test of the application code covers it, and treating a trigger the same as any other piece of production logic when reviewing a schema change — not as an implementation detail the database quietly handles. Some teams set a hard rule of keeping triggers as small and single-purpose as possible — one trigger, one clearly named responsibility — specifically to keep the invisible part of the system small enough that it stays reasoned-about.
Triggers vs application logic vs plain constraints
| Plain constraint | Application logic | Trigger | |
|---|---|---|---|
| Enforced regardless of write path | Yes | No — only via the app | Yes |
| Expressiveness | Limited (single-row checks) | Arbitrary | Arbitrary, in the DB’s procedural language |
| Visibility to developers | High — declared on the table | High — lives in the codebase | Low — easy to miss |
| Performance cost | Minimal | N/A (runs in the app) | Runs on every matching write |
A plain constraint should be the default whenever it’s expressive enough — it’s declarative, visible, and cheap. Application logic should handle anything that doesn’t need to be enforced against every possible write path. Triggers are the tool for the narrower case: rules that must hold no matter what wrote the data, and that a constraint alone can’t express. If you’re weighing a trigger against reacting to changes asynchronously instead, change data capture is often the better fit for side effects that don’t need to happen in the same transaction as the write.
The takeaway
A database trigger runs automatically when a table event happens, guaranteeing the logic fires no matter what wrote the row — which makes it the right tool for invariants and audit trails that have to hold regardless of write path. That same guarantee is what makes triggers dangerous to overuse: hidden control flow, ordering surprises, and per-write performance cost. Prefer a plain constraint when one is expressive enough, application logic when the rule only needs to apply through the app, and reach for a trigger only when the rule genuinely has to survive every possible way data gets written.
Tagged
Keep reading
The Lycoris Team · · 5 min read What Is a Stored Procedure? SQL Logic in the Database
A stored procedure is precompiled SQL saved inside the database and invoked by name, cutting network round trips and centralizing business logic.
The Lycoris Team · · 4 min read Primary Key vs Foreign Key vs Unique Constraint
Primary keys identify a row, foreign keys link one table to another, and unique constraints just prevent duplicates. How the three differ in SQL.
The Lycoris Team · · 4 min read Database Deadlocks Explained: Causes and Prevention
A database deadlock happens when two transactions each wait on a lock the other holds. Why deadlocks occur, how databases detect them, and how to avoid them.