In relational databases, the log trigger or history trigger is a mechanism for automatic recording of information about changes inserting or/and updating or/and deleting rows in a database table.

It is a particular technique for change data capturing, and in data warehousing for dealing with slowly changing dimensions.

Introduction

Operational databases are typically designed to capture the current state of an organization, acting as a snapshot of "now" rather than a historical archive. In this environment, updates are often destructive; when a specific data point changes, the system prioritizes efficiency by replacing the existing value with the new one. For instance, in an employee or customer directory, if an individual moves to a new location, an update operation is performed on the database that writes the new address directly over the old one. Consequently, the previous address is permanently overwritten and lost to the system, leaving the database with only the most up-to-date information and no record of the entity's history or previous status.

The Log trigger is a mechanism to automatically detect changes and to store the previous status of information.

Definition

Suppose there is a table which we want to audit. This table contains the following columns:

Column1, Column2, ..., Columnn

The column Column1 is assumed to be the primary key.

These columns are defined to have the following types:

Type1, Type2, ..., Typen

The Log Trigger works writing the changes (INSERT, UPDATE and DELETE operations) on the table in another, history table, defined as following:

CREATE TABLE HistoryTable ( Column1 Type1, Column2 Type2,

Columnn Typen,

StartDate DATETIME, EndDate DATETIME )

As shown above, this new table contains the same columns as the original table, and additionally two new columns of type DATETIME: StartDate and EndDate. This is known as tuple versioning. These two additional columns define a period of time of "validity" of the data associated with a specified entity (the entity of the primary key), or in other words, it stores how the data were in the period of time between the StartDate (included) and EndDate (not included).

For each entity (distinct primary key) on the original table, the following structure is created in the history table. Data is shown as example.

Notice that if they are shown chronologically the EndDate column of any row is exactly the StartDate of its successor (if any). It does not mean that both rows are common to that point in time, since -by definition- the value of EndDate is not included.

There are two variants of the Log trigger, depending how the old values (DELETE, UPDATE) and new values (INSERT, UPDATE) are exposed to the trigger (it is RDBMS dependent):

Old and new values as fields of a record data structure

CREATE TRIGGER HistoryTable ON OriginalTable FOR INSERT, DELETE, UPDATE AS DECLARE @Now DATETIME SET @Now = GETDATE()

/* deleting section */

UPDATE HistoryTable SET EndDate = @Now WHERE EndDate IS NULL AND Column1 = OLD.Column1

/* inserting section */

INSERT INTO HistoryTable (Column1, Column2, ...,Columnn, StartDate, EndDate) VALUES (NEW.Column1, NEW.Column2, ..., NEW.Columnn, @Now, NULL)

Old and new values as rows of virtual tables

CREATE TRIGGER HistoryTable ON OriginalTable FOR INSERT, DELETE, UPDATE AS DECLARE @Now DATETIME SET @Now = GETDATE()

/* deleting section */

UPDATE HistoryTable SET EndDate = @Now FROM HistoryTable, DELETED WHERE HistoryTable.Column1 = DELETED.Column1 AND HistoryTable.EndDate IS NULL

/* inserting section */

INSERT INTO HistoryTable (Column1, Column2, ..., Columnn, StartDate, EndDate) SELECT (Column1, Column2, ..., Columnn, @Now, NULL) FROM INSERTED

Compatibility notes

The code above is shown as a code idiom. Trigger syntax vary enormously among RDBMS, for example:

The function GetDate() is used to get the system date and time, a specific RDBMS could either use another function name, or get this information by another way. Several RDBMS (Db2, MySQL) do not support that the same trigger can be attached to more than one operation (INSERT, DELETE, UPDATE). In such a case a trigger must be created for each operation; For an INSERT operation only the inserting section must be specified, for a DELETE operation only the deleting section must be specified, and for an UPDATE operation both sections must be present, just as it is shown above (the deleting section first, then the inserting section), because an UPDATE operation is logically represented as a DELETE operation followed by an INSERT operation. In the code shown, the record data structure containing the old and new values are called OLD and NEW. On a specific RDBMS they could have different names. In the code shown, the virtual tables are called DELETED and INSERTED. On a specific RDBMS they could have different names. Another RDBMS (Db2) even let the name of these logical tables be specified. In the code shown, comments are in C/C++ style, they could not be supported by a specific RDBMS, or a different syntax should be used. Several RDBMS require that the body of the trigger is enclosed between BEGIN and END keywords.

Implementation in common RDBMS

IBM Db2

Source:"Database Fundamentals" by Nareej Sharma et al. (First Edition, Copyright IBM Corp. 2010)

A trigger cannot be attached to more than one operation (INSERT, DELETE, UPDATE), so a trigger must be created for each operation. The old and new values are exposed as fields of a record data structures. The names of these records can be defined, in this example they are named as O for old values and N for new values.

-- Trigger for INSERT CREATE TRIGGER Database.TableInsert AFTER INSERT ON Database.OriginalTable REFERENCING NEW AS N FOR EACH ROW MODE DB2SQL BEGIN DECLARE Now TIMESTAMP; SET NOW = CURRENT TIMESTAMP;

INSERT INTO Database.HistoryTable (Column1, Column2, ..., Columnn, StartDate, EndDate) VALUES (N.Column1, N.Column2, ..., N.Columnn, Now, NULL); END;

-- Trigger for DELETE CREATE TRIGGER Database.TableDelete AFTER DELETE ON Database.OriginalTable REFERENCING OLD AS O FOR EACH ROW MODE DB2SQL BEGIN DECLARE Now TIMESTAMP; SET NOW = CURRENT TIMESTAMP;

UPDATE Database.HistoryTable SET EndDate = Now WHERE Column1 = O.Column1 AND EndDate IS NULL; END;

-- Trigger for UPDATE CREATE TRIGGER Database.TableUpdate AFTER UPDATE ON Database.OriginalTable REFERENCING NEW AS N OLD AS O FOR EACH ROW MODE DB2SQL BEGIN DECLARE Now TIMESTAMP; SET NOW = CURRENT TIMESTAMP;

UPDATE Database.HistoryTable SET EndDate = Now WHERE Column1 = O.Column1 AND EndDate IS NULL;

INSERT INTO Database.HistoryTable (Column1, Column2, ..., Columnn, StartDate, EndDate) VALUES (N.Column1, N.Column2, ..., N.Columnn, Now, NULL); END;

Microsoft SQL Server

Source:"Microsoft SQL Server 2008 - Database Development" by Thobias Thernström et al. (Microsoft Press, 2009)

The same trigger can be attached to all the INSERT, DELETE, and UPDATE operations. Old and new values as rows of virtual tables named DELETED and INSERTED.

CREATE TRIGGER TableTrigger ON OriginalTable FOR DELETE, INSERT, UPDATE AS

DECLARE @NOW DATETIME SET @NOW = CURRENT_TIMESTAMP

UPDATE HistoryTable SET EndDate = @now FROM HistoryTable, DELETED WHERE HistoryTable.ColumnID = DELETED.ColumnID AND HistoryTable.EndDate IS NULL

INSERT INTO HistoryTable (ColumnID, Column2, ..., Columnn, StartDate, EndDate) SELECT ColumnID, Column2, ..., Columnn, @NOW, NULL FROM INSERTED

MySQL

A trigger cannot be attached to more than one operation (INSERT, DELETE, UPDATE), so a trigger must be created for each operation. The old and new values are exposed as fields of a record data structures called Old and New.

DELIMITER $$

/* Trigger for INSERT */ CREATE TRIGGER HistoryTableInsert AFTER INSERT ON OriginalTable FOR EACH ROW BEGIN DECLARE N DATETIME; SET N = now();

INSERT INTO HistoryTable (Column1, Column2, ..., Columnn, StartDate, EndDate) VALUES (New.Column1, New.Column2, ..., New.Columnn, N, NULL); END;

/* Trigger for DELETE */ CREATE TRIGGER HistoryTableDelete AFTER DELETE ON OriginalTable FOR EACH ROW BEGIN DECLARE N DATETIME; SET N = now();

UPDATE HistoryTable SET EndDate = N WHERE Column1 = OLD.Column1 AND EndDate IS NULL; END;

/* Trigger for UPDATE */ CREATE TRIGGER HistoryTableUpdate AFTER UPDATE ON OriginalTable FOR EACH ROW BEGIN DECLARE N DATETIME; SET N = now();

UPDATE HistoryTable SET EndDate = N WHERE Column1 = OLD.Column1 AND EndDate IS NULL;

INSERT INTO HistoryTable (Column1, Column2, ..., Columnn, StartDate, EndDate) VALUES (New.Column1, New.Column2, ..., New.Columnn, N, NULL); END;

Oracle

The same trigger can be attached to all the INSERT, DELETE, and UPDATE operations. The old and new values are exposed as fields of a record data structures called :OLD and :NEW. It is necessary to test the nullity of the fields of the :NEW record that define the primary key (when a DELETE operation is performed), in order to avoid the insertion of a new row with null values in all columns.

CREATE OR REPLACE TRIGGER TableTrigger AFTER INSERT OR UPDATE OR DELETE ON OriginalTable FOR EACH ROW DECLARE Now TIMESTAMP; BEGIN SELECT CURRENT_TIMESTAMP INTO Now FROM Dual;

UPDATE HistoryTable SET EndDate = Now WHERE EndDate IS NULL AND Column1 = :OLD.Column1;

IF :NEW.Column1 IS NOT NULL THEN INSERT INTO HistoryTable (Column1, Column2, ..., Columnn, StartDate, EndDate) VALUES (:NEW.Column1, :NEW.Column2, ..., :NEW.Columnn, Now, NULL); END IF; END;

PostgreSQL

The action associated to a trigger must be specified as a function, so a function is first defined. Old and new values are exposed as rows of virtual tables named old_table and new_table, but these names can be different. Even though a trigger can be associated to more than one operation (INSERT, DELETE, UPDATE), in this case a different trigger is associated on every operation on order to specify the names of the virtual tables, and these triggers can reference to the same function.

CREATE OR REPLACE FUNCTION process_for_table() RETURNS TRIGGER AS $$ DECLARE now TIMESTAMP := NOW(); BEGIN --- deleting section

IF (TG_OP = 'UPDATE' OR TG_OP = 'DELETE') THEN UPDATE HistoricTable SET EndDate = now FROM HistoricTable AS H INNER JOIN old_table ON H.ColumnID = old_table.ColumnID WHERE HistoricTable.ColumnID = H.ColumnID AND HistoricTable.EndDate IS NULL; END IF;

--- inserting section

IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN INSERT INTO HistoricTable SELECT ColumnID, Column2, ..., Columnn, now, NULL FROM new_table; END IF;

RETURN NULL; END; $$ LANGUAGE plpgsql

CREATE TRIGGER TriggerForTableInsert AFTER INSERT ON OriginalTable REFERENCING NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE FUNCTION process_for_table();

CREATE TRIGGER TriggerForTableUpdate AFTER UPDATE ON OriginalTable REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE FUNCTION process_for_table();

CREATE TRIGGER TriggerForTableDelete AFTER DELETE ON OriginalTable REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION process_for_table();

Historic information

Typically, database backups are used to store and retrieve historic information. A database backup is a security mechanism, more than an effective way to retrieve ready-to-use historic information.

A (full) database backup is only a snapshot of the data in specific points of time, so we could know the information of each snapshot, but we can know nothing between them. Information in database backups is discrete in time.

Using the log trigger the information we can know is not discrete but continuous, we can know the exact state of the information in any point of time, only limited to the granularity of time provided with the DATETIME data type of the RDBMS used.

Advantages

It is simple. It is not a commercial product, it works with available features in common RDBMS.