Triggers are used to instruct the PostgreSQL engine to execute a piece of code when a specific event occurs. They act like a catalyst for changes — a trigger that initiates a chain of events.
A trigger must be associated with a specific table, view (pseudo-table), or foreign table. It activates only when operations are performed on that entity — INSERT
, UPDATE
, DELETE
, or TRUNCATE
. Depending on your needs, the trigger can run before, after, or instead of the event/operation.
PotgreSQL triggers are divided into two types depending on the level at which they operate:
FOR EACH ROW
: The function is called for each individual row affected by the event. For example, an UPDATE
affecting 100 rows will trigger the function 100 times — once for each row.
FOR EACH STATEMENT
: The function is called just once per SQL statement, regardless of how many rows are affected.
Triggers are powerful tools with many use cases. Some examples include:
Tracking Changes: You can use triggers to log transaction details when changes occur in a table.
Data Validation: Triggers can enforce constraints before applying changes to the database.
Auto-Filling Fields: Automatically populate fields based on new transaction data.
Performance Optimization: For example, on a server like Hostman, you might log timestamps in a table and want to aggregate that data every 6 hours (four intervals a day). Scanning and aggregating all rows repeatedly is inefficient on large datasets.
Instead of recalculating everything each time, you can use Materialized Views, which cache results. However, these are recalculated entirely on each refresh, which is still problematic on large datasets.
Triggers can solve this by acting like "smart" materialized views: updating only the affected row, not the entire dataset.
Let’s look at how to create a trigger in PostgreSQL.
Syntax:
CREATE [ OR REPLACE ] [ CONSTRAINT ] TRIGGER name
{ BEFORE | AFTER | INSTEAD OF } { event [ OR ... ] }
ON table_name
[ FROM referenced_table_name ]
[ NOT DEFERRABLE | [ DEFERRABLE ] [ INITIALLY IMMEDIATE | INITIALLY DEFERRED ] ]
[ REFERENCING { { OLD | NEW } TABLE [ AS ] transition_relation_name } [ ... ] ]
[ FOR [ EACH ] { ROW | STATEMENT } ]
[ WHEN ( condition ) ]
EXECUTE { FUNCTION | PROCEDURE } function_name ( arguments );
Where the event can be one or more of the following:
INSERT
UPDATE [ OF column_name [, ...] ]
DELETE
TRUNCATE
Key notes:
CREATE
) or replace (REPLACE
) an existing trigger.INSTEAD OF
must be marked with FOR EACH ROW
and are only allowed on views.BEFORE
or AFTER
on views must be FOR EACH STATEMENT
.Refer to PostgreSQL documentation for a summary table that outlines these rules.
CREATE TRIGGER check_update
BEFORE UPDATE ON accounts
FOR EACH ROW
EXECUTE FUNCTION check_account_update();
CREATE OR REPLACE TRIGGER check_update
BEFORE UPDATE OF balance ON accounts
FOR EACH ROW
EXECUTE FUNCTION check_account_update();
CREATE TRIGGER log_update
AFTER UPDATE ON accounts
FOR EACH ROW
WHEN (OLD.* IS DISTINCT FROM NEW.*)
EXECUTE FUNCTION log_account_update();
CREATE TRIGGER view_insert
INSTEAD OF INSERT ON my_view
FOR EACH ROW
EXECUTE FUNCTION view_insert_row();
CREATE TRIGGER example_delete_trigger
AFTER DELETE ON my_view
FOR EACH ROW
EXECUTE PROCEDURE aft_delete();
Let’s create a PostgreSQL trigger that adds information about a new employee to a second table when data is inserted into the primary table.
Step 1: Create both tables:
CREATE TABLE "Employee" (
"EmployeeId" INT NOT NULL,
"LastName" VARCHAR(20) NOT NULL,
"FirstName" VARCHAR(20) NOT NULL,
"Title" VARCHAR(30),
"ReportsTo" INT,
"BirthDate" TIMESTAMP,
"HireDate" TIMESTAMP,
"Address" VARCHAR(70),
"City" VARCHAR(40),
"State" VARCHAR(40),
"Country" VARCHAR(40),
"PostalCode" VARCHAR(10),
"Phone" VARCHAR(24),
"Fax" VARCHAR(24),
"Email" VARCHAR(60),
CONSTRAINT "PK_Employee" PRIMARY KEY ("EmployeeId")
);
CREATE TABLE "Employee_Audit" (
"EmployeeId" INT NOT NULL,
"LastName" VARCHAR(20) NOT NULL,
"FirstName" VARCHAR(20) NOT NULL,
"UserName" VARCHAR(20) NOT NULL,
"EmpAdditionTime" VARCHAR(20) NOT NULL
);
Step 2: Create the trigger function:
CREATE OR REPLACE FUNCTION employee_insert_trigger_fnc()
RETURNS trigger AS
$$
BEGIN
INSERT INTO "Employee_Audit" ("EmployeeId", "LastName", "FirstName", "UserName", "EmpAdditionTime")
VALUES (NEW."EmployeeId", NEW."LastName", NEW."FirstName", current_user, current_date);
RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';
Step 3: Create the trigger:
CREATE TRIGGER employee_insert_trigger
AFTER INSERT
ON "Employee"
FOR EACH ROW
EXECUTE PROCEDURE employee_insert_trigger_fnc();
Test:
Insert a record:
INSERT INTO "Employee"
VALUES(12, 'Smith', 'Jeff', 'Editor', 1, '1992-05-28 00:00:00', '2022-01-15 00:00:00',
'Paseo de Gracia', 'Barcelona', 'Catalonia', 'Spain', '128 665', '+15 52-469-2573',
'+15 52-469-2573', 'mail@mail.com');
Check that the data is there:
SELECT * FROM "Employee" WHERE "EmployeeId" = 12;
Check the audit log:
SELECT * FROM "Employee_Audit";
You should see something like:
EmployeeId | 12
LastName | Smith
FirstName | Jeff
UserName | postgres
EmpAdditionTime | 2025-04-10
Perfect! It works as expected.
To change a trigger's properties, use CREATE OR REPLACE TRIGGER
and specify the same trigger function and table. You can update the remaining properties as needed.
To rename a trigger:
ALTER TRIGGER name ON table_name RENAME TO new_name;
Check documentation for more details.
To delete a trigger:
DROP TRIGGER [ IF EXISTS ] name ON table_name [ CASCADE | RESTRICT ];
Example:
DROP TRIGGER some_example_of_trigger ON "Example";
Note:
IF EXISTS
to avoid errors if the trigger doesn’t exist.CASCADE
to remove all dependent objects.RESTRICT
to prevent deletion if dependencies exist (default).Check the documentation for more details.
TRIGGER
privilege on the table and EXECUTE
privilege on the trigger function.pg_trigger
to view existing triggers.