top of page
Writer's pictureSheba Alice Prathab

A Comprehensive Guide to SQL Triggers: DML Triggers - PART I


In SQL, triggers are special database objects that automatically perform specified actions when particular database events occur. These events include data manipulation (like INSERT, UPDATE, or DELETE) or schema modifications. Triggers are widely used to maintain data integrity, enforce business rules, log changes, or automate tasks that would otherwise require manual coding in application logic.


Why Use SQL Triggers?


Triggers are especially useful for:


  • Enforcing validation rules: Ensuring data integrity by validating data before it is committed.

  • Automating workflows: Automatically updating related tables or performing complex actions without additional application logic.

  • Auditing changes to sensitive data: Keeping track of modifications made to crucial data points.

  • Implementing complex business logic: Ensuring that the database enforces rules consistently.


Understanding SQL Triggers: Structure and Hierarchy


In this blog, we'll explore the structure and hierarchy of SQL triggers, looking at their types, execution levels, and timing. We'll also see detailed examples of each type of trigger to help you understand how to apply them in real-world scenarios.


SQL Trigger Structure


  1. Trigger Types:


    • DML (Data Manipulation Language) Triggers: Fired by INSERT, UPDATE, or DELETE operations on tables; the most commonly used type.

    • DDL (Data Definition Language) Triggers: Fired by schema-altering commands like CREATE, ALTER, or DROP; helps monitor and log schema changes.

    • Logon Triggers: Executed in response to user logins; used to enforce access policies or log session details.


  2. Trigger Levels:


    • Row-Level Triggers: Fired once for each row affected by the triggering SQL statement, allowing for granular control.

    • Statement-Level Triggers: Fired once for the entire SQL statement, regardless of how many rows are affected; often used for auditing.


  3. Trigger Timings:


    • Before Triggers: Executed before the operation, allowing for data validation or modifications.

    • After Triggers: Executed after the operation, typically for logging or performing actions post-modification.

    • Instead Of Triggers: Primarily used with views; allow you to specify custom actions instead of the default behavior.


Hierarchy of SQL Triggers


To visualize the structure, let’s map out the hierarchical order:


  • Step 1: Trigger Types


    • DML Triggers

    • DDL Triggers

    • Logon Triggers


  • Step 2: Levels of Triggers


    • DML Triggers: Can have Row-Level and Statement-Level Triggers.

    • DDL Triggers: Usually Statement-Level (affecting the schema).

    • Logon Triggers: Operate like Statement-Level Triggers.


  • Step 3: Timing for Each Level


    • Before, After, Instead Of (mainly for views in DML triggers).



1. DML Trigger for INSERT :


Row-Level Trigger


Before Trigger : Validates data or generate logs before insertion.


Step 1: Create a Function

You need to define a trigger function that will contain the logic to execute when the trigger is fired. For example:


CREATE OR REPLACE FUNCTION log_before_insert_film() 
RETURNS TRIGGER AS $$ 
BEGIN      
RAISE NOTICE 'Inserting film with ID: %', NEW.film_id; 
RETURN NEW; -- Allow insertion to proceed 
END; 
$$ LANGUAGE plpgsql;

Step 2: Delete Any Old Trigger if Existing


Before creating a new trigger, it’s a good practice to drop any existing trigger of the same name to avoid conflicts:


DROP TRIGGER IF EXISTS before_insert_film_trigger ON film;

Step 3: Create New Trigger


Now, you create a new trigger that will call the function you just defined whenever an insert operation occurs on the film table:


CREATE TRIGGER before_insert_film_trigger 
BEFORE INSERT ON film 
FOR EACH ROW  
EXECUTE FUNCTION log_before_insert_film();

Step 4: Try to Insert Values


You can now insert a new row into the film table to see the trigger in action:


INSERT INTO film VALUES (61081, 'Hello Barak', 'Battle Between Two Kings', 2010, 1, 3, 4.99, 150, 15.23, 'R', CURRENT_TIMESTAMP, '{Trailor}', 'chamber:1');

Step 5: See the RAISE NOTICE Messages Before the Insert


After executing the insert statement, you should see the raised notice message in your console, confirming the ID of the film being inserted:


NOTICE:  Inserting film with ID: 61081


Row-Level Trigger


After Trigger : Validates data or logs completion after insertion.


Step 1: Create a Function


First, you define a trigger function that will log a message with the film ID whenever a row is inserted into the film table. This function is called for each row being inserted:


CREATE OR REPLACE FUNCTION log_after_insert_film() 
RETURNS TRIGGER AS $$ 
BEGIN      
RAISE NOTICE 'Inserted film with ID: %', NEW.film_id; 
RETURN NEW; -- Allow insertion to proceed 
END; $$ LANGUAGE plpgsql;

Step 2: Delete Any Old Trigger if Existing


Before creating a new trigger, it's important to remove any existing trigger with the same name to prevent conflicts:


DROP TRIGGER IF EXISTS after_insert_film_trigger ON film;

Step 3: Create New Trigger


Now, you create a new trigger that will call the function you just defined every time a new row is inserted into the film table:

CREATE TRIGGER after_insert_film_trigger 
AFTER INSERT ON film 
FOR EACH ROW  
EXECUTE FUNCTION log_after_insert_film();

Step 4: Try to Insert Values


You can now insert a new row into the film table to see the trigger in action:


INSERT INTO film VALUES  (61081, 'Hello Barak', 'Battle Between Two Kings', 2010, 1, 3, 4.99, 150, 15.23, 'R', CURRENT_TIMESTAMP, '{Trailor}', 'chamber:1');

Step 5: See the RAISE NOTICE Messages After the Insert


After executing the insert statement, you should see the raised notice message in your console, confirming the ID of the film that was just inserted:


NOTICE:  Inserted film with ID: 61081


Statement-Level Trigger


Before Trigger: Validates or Logs an action to the entire insert statement before insertion.


Step 1: Create a Function


First, you define a trigger function that will log a message whenever an insert statement is executed on the film table. This function is executed once for the entire statement, not for each row:


CREATE OR REPLACE FUNCTION before_insert_film_statement_trigger() RETURNS TRIGGER AS $$ 
BEGIN      
RAISE NOTICE 'A new batch of films is being inserted into the film table'; RETURN NEW; -- Proceed with insertion 
END; $$ LANGUAGE plpgsql;

Step 2: Create the Trigger


Next, you create a trigger (drop the trigger if exists already) that will call the above function before any insert operation on the film table. This trigger is defined to fire before the entire insert statement:


DROP TRIGGER IF EXISTS before_insert_film_statement ON film;

CREATE TRIGGER before_insert_film_statement 
BEFORE INSERT ON film 
FOR EACH STATEMENT EXECUTE FUNCTION before_insert_film_statement_trigger();

Step 3: Insert Values


You can now perform an insert operation that includes multiple rows at once. The trigger will activate, logging the action for the entire insert statement:


INSERT INTO film VALUES  
(61094, 'Movie Title 1', 'Description 1', 2021, 1, 3, 4.99, 120, 7.99, 'PG-13', CURRENT_TIMESTAMP, '{Trailers}', 'Sample Text 1'), 
(61095, 'Movie Title 2', 'Description 2', 2022, 1, 5, 5.99, 130, 5.89, 'PG', CURRENT_TIMESTAMP, '{Trailers}', 'Sample Text 2');

Step 4: See the RAISE NOTICE Message


After executing the insert statement, you should see the raised notice message in your console, indicating that a batch of films is being inserted:

NOTICE:  A new batch of films is being inserted into the film table


Statement-Level Trigger


After Trigger: Validates or Logs an action to the entire insert statement after insertion.


Step 1: Create a Function


First, you define a trigger function that will log a message after an insert statement is executed on the film table. This function is executed once for the entire statement, not for each row:


CREATE OR REPLACE FUNCTION after_insert_film_statement_trigger() RETURNS TRIGGER AS $$ 
BEGIN 
RAISE NOTICE 'A batch of films has been inserted into the film table'; RETURN NEW; -- Proceed with the statement 
END; $$ LANGUAGE plpgsql;

Step 2: Create the Trigger


Next, create a trigger (drop the trigger if it exists already) that will call the above function after any insert operation on the film table. This trigger is defined to fire after the entire insert statement:


DROP TRIGGER IF EXISTS after_insert_film_statement ON film;

CREATE TRIGGER after_insert_film_statement 
AFTER INSERT ON film 
FOR EACH STATEMENT 
EXECUTE FUNCTION after_insert_film_statement_trigger();

Step 3: Insert Values


You can now perform an insert operation that includes multiple rows at once. The trigger will activate, logging the action for the entire insert statement:


INSERT INTO film VALUES  
(61094, 'Movie Title 1', 'Description 1', 2021, 1, 3, 4.99, 120, 7.99, 'PG-13', CURRENT_TIMESTAMP, '{Trailers}', 'Sample Text 1'), 
(61095, 'Movie Title 2', 'Description 2', 2022, 1, 5, 5.99, 130, 5.89, 'PG', CURRENT_TIMESTAMP, '{Trailers}', 'Sample Text 2');

Step 4: See the RAISE NOTICE Message


After executing the insert statement, you should see the raised notice message in your console, indicating that a batch of films has been inserted:


NOTICE: A batch of films has been inserted into the film table


Additional DML Trigger for INSERT with Validation


Before Trigger: Validates the release year of the film.

CREATE FUNCTION

CREATE OR REPLACE FUNCTION validate_film_year() 
RETURNS TRIGGER AS $$ 
BEGIN
 
IF NEW.release_year > EXTRACT(YEAR FROM CURRENT_DATE) 
THEN RAISE EXCEPTION 'Release year cannot be in the future'; 
END IF; 

RETURN NEW; -- Proceed with the insert 

END; $$ LANGUAGE plpgsql; 

CREATE TRIGGER

CREATE TRIGGER film_year_check 
BEFORE INSERT ON film 
FOR EACH ROW 
EXECUTE FUNCTION validate_film_year();  

INSERT VALUES -- THIS WILL RAISE AN EXCEPTION

INSERT INTO film VALUES (61099, 'Movie Title 1', 'Description 1', 2026, 1,3, 4.99, 120, 7.99, 'PG-13', CURRENT_TIMESTAMP, '{Trailers}', 'Sample Text 1');


2. DML Trigger on UPDATE


Row-Level Trigger


Before Trigger : Validates data or generate logs before updating.


Step 1: Create a Function


First, you define a trigger function that updates the last_update timestamp whenever a row in the actor table is updated:


CREATE OR REPLACE FUNCTION update_actor_last_updated() RETURNS TRIGGER AS $$ 
BEGIN 
NEW.last_update := CURRENT_TIMESTAMP; 
RETURN NEW; -- Allow update to proceed 
END; $$ LANGUAGE plpgsql;

Step 2: Drop Any Old Trigger if Existing


Before creating a new trigger, it’s a good practice to drop any existing trigger of the same name to avoid conflicts:


DROP TRIGGER IF EXISTS update_last_updated_row ON actor;

Step 3: Create the Trigger


Now, create a new trigger that will call the above function before any update operation on the actor table:


CREATE TRIGGER update_last_updated_row 
BEFORE UPDATE ON actor 
FOR EACH ROW  EXECUTE FUNCTION update_actor_last_updated();

Step 4: Update Values


You can now perform an update operation on the actor table. This will trigger the function to update the last_update column:


UPDATE actor SET first_name = 'Alice' WHERE first_name = 'Sheba';

Step 5: Verify the Update


After executing the update statement, you can select the updated rows to verify the change and check the last_update timestamp:


SELECT * FROM actor WHERE first_name = 'Alice';

Row-Level Trigger


After Trigger : Validates data or logs completion after updating.


Step 1: Create the Audit Log Table


First, create a table to store the audit logs for updates made to the actor table:


CREATE TABLE audit_log (
log_id SERIAL PRIMARY KEY, 
updated_at TIMESTAMP, 
total_updated_rows INTEGER );

Step 2: Create a Function


Next, define a trigger function that logs the update details into the audit_log table whenever a row in the actor table is updated:


CREATE OR REPLACE FUNCTION log_actor_updates() 
RETURNS TRIGGER AS $$ 

DECLARE      
row_count INTEGER; 

BEGIN      

SELECT COUNT(*) INTO row_count 
FROM actor 
WHERE first_name = NEW.first_name; 

INSERT INTO audit_log (updated_at, total_updated_rows) VALUES (CURRENT_TIMESTAMP, row_count); 

RETURN NEW; -- Proceed with the update operation 

END; $$ LANGUAGE plpgsql;

Step 3: Drop Any Old Trigger if Existing


Before creating a new trigger, it’s a good practice to drop any existing trigger of the same name to avoid conflicts:


DROP TRIGGER IF EXISTS log_actor_update_statement ON actor;

Step 4: Create the Trigger


Now, create a new trigger that will call the above function after any update operation on the actor table:


CREATE TRIGGER log_actor_update_statement 
AFTER UPDATE ON actor 
FOR EACH ROW  
EXECUTE FUNCTION log_actor_updates();

Step 5: Update Values


You can now perform an update operation on the actor table. This will trigger the function to log the update details:


UPDATE actor SET first_name = 'Sheba' WHERE first_name = 'Alice';

Step 6: Verify the Audit Log


After executing the update statement, you can check the audit_log table to see the entries that were logged:


SELECT * FROM audit_log;

Step 7: Verify the Updated Values


Finally, you can check the updated rows in the actor table to confirm the changes:


SELECT * FROM actor WHERE first_name = 'Sheba';


Statement-Level Trigger


Before Trigger: Validates or Logs an action to the update statement before updating.


Step 1: Create a Function


First, you define a trigger function that logs a message before any update operation on the department table:


CREATE OR REPLACE FUNCTION log_before_update_department() RETURNS TRIGGER AS $$ 
BEGIN 
RAISE NOTICE 'About to update rows in the department table'; 
RETURN NULL; -- Statement-level trigger, no row-level processing 
END; $$ LANGUAGE plpgsql;

Step 2: Drop Any Old Trigger if Existing


It's always a good practice to drop any existing trigger of the same name to avoid conflicts:


DROP TRIGGER IF EXISTS before_update_department_statement ON department;

Step 3: Create the Trigger


Now, create a new before update trigger that will fire before any update operation on the department table:


CREATE TRIGGER before_update_department_statement 
BEFORE UPDATE ON department 
FOR EACH STATEMENT 
EXECUTE FUNCTION log_before_update_department();

Step 4: Update Values


Perform an update operation on the department table:


UPDATE department SET budget = budget * 1.05;

Step 5: Verify the Trigger Logs


Upon performing the update, you'll see the following output from the trigger:


NOTICE: About to update rows in the department table


Statement-Level Trigger


After Trigger: Validates or Logs an action to the update statement after updating.


Step 1: Create a Function


Now, define a trigger function that logs a message after any update operation on the department table:


CREATE OR REPLACE FUNCTION log_after_update_department() RETURNS TRIGGER AS $$ 
BEGIN 
RAISE NOTICE 'Update complete on the department table'; 
RETURN NULL; -- Statement-level trigger, no row-level processing 
END; $$ LANGUAGE plpgsql;

Step 2: Drop Any Old Trigger if Existing


Drop any old trigger of the same name:


DROP TRIGGER IF EXISTS after_update_department_statement ON department;

Step 3: Create the Trigger


Now, create a new after update trigger that will fire after any update operation on the department table:

CREATE TRIGGER after_update_department_statement 
AFTER UPDATE ON department 
FOR EACH STATEMENT 
EXECUTE FUNCTION log_after_update_department();

Step 4: Update Values


Again, perform an update operation:


UPDATE department SET budget = budget * 1.05;

Step 5: Verify the Trigger Logs


After performing the update, you should see the following output:


NOTICE: Update complete on the department table


3. DML Trigger on DELETE


Row-Level Trigger


Before Trigger : Validates data or generate logs before deletion.


Step 1: Create a Function


First, you define a trigger function that prevents the deletion of rows where the first_name is 'Sheba':


CREATE OR REPLACE FUNCTION prevent_delete_actor() 
RETURNS TRIGGER AS $$ 
BEGIN 
IF OLD.first_name = 'Sheba' THEN RAISE EXCEPTION 'Deletion of Sheba is not allowed!'; 
END IF; 
RETURN OLD; -- Allow the deletion to proceed for other rows 
END; $$ LANGUAGE plpgsql;

Step 2: Drop Any Old Trigger if Existing


Drop any old trigger to avoid conflicts:


DROP TRIGGER IF EXISTS before_actor_delete_trigger ON actor;

Step 3: Create the Trigger


Now, create a before delete trigger that will fire before any row is deleted from the actor table:


CREATE TRIGGER before_actor_delete_trigger 
BEFORE DELETE ON actor 
FOR EACH ROW  
EXECUTE FUNCTION prevent_delete_actor();

Step 4: Delete Values


Now, attempt to delete a row where the first_name is 'Sheba' (this will trigger the exception):


DELETE FROM actor WHERE first_name = 'Sheba';

Step 5: Verify the Trigger Logs


When attempting to delete, you will see the following error message:


ERROR:  Deletion of Sheba is not allowed!

Rows with other first names will still be deletable.



Row-Level Trigger


After Trigger : Validates data or logs completion after deletion.


Step 1: Create an Audit Table


First, you need an audit table to store the details of deleted rows:


CREATE TABLE actor_audit ( audit_id SERIAL PRIMARY KEY, actor_id INTEGER, first_name VARCHAR(50), last_name VARCHAR(50), deleted_at TIMESTAMP );

Step 2: Create a Function


Now, define a trigger function that logs the details of each deleted row into the audit table:


CREATE OR REPLACE FUNCTION log_deleted_actor() 
RETURNS TRIGGER AS $$ 
BEGIN -- Insert the deleted row details into the audit table INSERT INTO actor_audit (actor_id, first_name, last_name, deleted_at) VALUES (OLD.actor_id, OLD.first_name, OLD.last_name, CURRENT_TIMESTAMP); RETURN OLD; -- Return the deleted row 
END; $$ LANGUAGE plpgsql;

Step 3: Drop Any Old Trigger if Existing


Drop any old trigger to avoid conflicts:


DROP TRIGGER IF EXISTS after_actor_delete_trigger ON actor;

Step 4: Create the Trigger


Now, create an after delete trigger that fires after each row is deleted from the actor table:


CREATE TRIGGER after_actor_delete_trigger 
AFTER DELETE ON actor 
FOR EACH ROW  
EXECUTE FUNCTION log_deleted_actor();

Step 5: Delete Values


Now, attempt to delete rows from the actor table. This will trigger the function to log the deleted rows:


DELETE FROM actor WHERE first_name = 'Alice';

Step 6: Verify the Audit Log


After executing the delete statement, check the actor_audit table to verify the logs of the deleted rows:


SELECT * FROM actor_audit;

You should see an entry for each deleted row, with details like actor_id, first_name, last_name, and the deleted_at timestamp.




Statement-Level Trigger


Before Trigger: Validates or Logs an action to the delete statement before deletion.


Step 1: Create the Function


This function will log a message indicating that a delete operation is going to be performed.


CREATE OR REPLACE FUNCTION log_before_delete() 
RETURNS TRIGGER AS $$ 
BEGIN -- Log a message indicating that a delete is going to be performed RAISE NOTICE 'Delete operation is going to be performed on the actor table.'; 
RETURN NULL; -- Allow the deletion to proceed 
END; $$ LANGUAGE plpgsql;

Step 2: Create the Trigger


Now, create the trigger that will call the above function before any delete operation on the actor table.


DROP TRIGGER IF EXISTS before_actor_delete ON actor;

CREATE TRIGGER before_actor_delete 
BEFORE DELETE ON actor 
FOR EACH STATEMENT 
EXECUTE FUNCTION log_before_delete();

Step 3: Testing the Trigger


Now, let's delete some rows from the actor table to see how the trigger behaves.


DELETE FROM actor WHERE first_name = 'Alice' OR first_name = 'Bob';

After executing the delete statement, you should see the following message printed in the console:


NOTICE:  Delete operation is going to be performed on the actor table.


Statement-Level Trigger


After Trigger: Validates or Logs an action to the delete statement after deletion.


Step 1: Create the Function


The function will log the number of rows remaining in the actor table after rows are deleted.


CREATE OR REPLACE FUNCTION log_after_delete_actor() 
RETURNS TRIGGER AS $$ 
DECLARE 
remaining_actors INTEGER; 
BEGIN -- Count the remaining rows in the actor table 
SELECT COUNT(*) INTO remaining_actors FROM actor; -- Log a message with the number of remaining actors 
RAISE NOTICE 'Rows deleted. Remaining actors: %', remaining_actors; RETURN NULL; -- No need to return anything since the deletion is already done 
END; $$ LANGUAGE plpgsql;

Step 2: Drop the Old Trigger (if it exists)


Before creating a new trigger, drop any existing one to avoid conflicts.


DROP TRIGGER IF EXISTS after_delete_actor_trigger ON actor;

Step 3: Create the Trigger


Now, create the after delete trigger that will call the log_after_delete_actor() function after any delete operation occurs on the actor table.


CREATE TRIGGER after_delete_actor_trigger 
AFTER DELETE ON actor 
FOR EACH STATEMENT 
EXECUTE FUNCTION log_after_delete_actor();

Step 4: Delete Rows to Test the After Delete Trigger


Let's delete rows from the actor table to trigger the after_delete_actor_trigger.


DELETE FROM actor WHERE first_name = 'Alice';

Step 5: Expected Output of the After Delete Trigger


After executing the delete statement, the trigger will log how many rows remain in the actor table. You should see the following notice in your console:


NOTICE: Rows deleted. Remaining actors: [remaining row count]


DML Trigger Combinations Table


DML

Trigger Level

Trigger Type

Logging

Validation (Before)

Validation (After)

Pre-Analysis & Backup (Before)

Result Analysis Tracking (After)

Insert

Row

Before

✔️

✔️


✔️


Insert

Row

After

✔️


✔️


✔️

Insert

Statement

Before

✔️

✔️


✔️


Insert

Statement

After

✔️


✔️


✔️

Update

Row

Before

✔️

✔️


✔️


Update

Row

After

✔️


✔️


✔️

Update

Statement

Before

✔️

✔️


✔️


Update

Statement

After

✔️


✔️


✔️

Delete

Row

Before

✔️

✔️


✔️


Delete

Row

After

✔️


✔️


✔️

Delete

Statement

Before

✔️

✔️


✔️


Delete

Statement

After

✔️


✔️


✔️

Column Definitions


  • Logging: If you need to record actions taken by the trigger for auditing or monitoring, implement logging in these trigger (marked ✔️) . Use logging triggers to maintain an audit trail of DML operations.


  • Validation (Before): Use these trigger (marked ✔️) if you need to perform validation checks before executing the DML operation. This helps ensure data integrity and business rules are followed before any changes are made to the database.


  • Validation (After): Implement these trigger (marked ✔️) when you require validation after the DML operation to confirm that the operation was successful or to verify that the data meets certain conditions post-execution.


  • Pre-Analysis & Backup (Before): If you need to analyze data or create backups before executing the DML operation, use these trigger (marked ✔️) . This allows you to assess the state of the data or safeguard it against potential loss during the operation.


  • Result Analysis Tracking: Utilize these trigger (marked ✔️) if you want to track and analyze the outcome of the operation after execution. This is essential for capturing the effects of the DML command, which can be useful for reporting or further analysis.



Best Practices


  • Use triggers sparingly: Overusing triggers can make debugging difficult and degrade performance.

  • Document your triggers: Always document what each trigger does and why it exists to help future developers understand its purpose.

  • Test your triggers: Ensure you thoroughly test triggers in a development environment before deploying to production to avoid unintended consequences.


SQL triggers are powerful tools that can greatly enhance the functionality of your database. Understanding the types of triggers, their execution levels, and timings allows you to enforce data integrity, automate workflows, and log changes effectively. Whether you're a beginner or an advanced user, mastering triggers will empower you to write cleaner, more efficient SQL code and help maintain a robust database environment.


By following these guidelines and understanding the hierarchical structure of SQL triggers, you can leverage their capabilities to ensure your database operates smoothly and efficiently.

46 views

Recent Posts

See All
bottom of page