Log trigger


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.

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 on the table in another, history table, defined as following:

CREATE TABLE HistoryTable, or in other words, it stores how the data were in the period of time between the StartDate and EndDate.
For each entity 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. 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 and new values are exposed to the trigger :
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
VALUES

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

SELECT
FROM INSERTED

Compatibility notes

  • 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 do not support that the same trigger can be attached to more than one operation. 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, 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 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.

    [Data warehousing]

According with the slowly changing dimension management methodologies, The log trigger falls into the following:

[IBM DB2]"Database Fundamentals" by Nareej Sharma et al. (First Edition, Copyright IBM Corp. 2010)

  • A trigger cannot be attached to more than one operation, 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
VALUES ;
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
VALUES ;
END;

[Microsoft SQL Server]"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
SELECT ColumnID, Column2,..., Columnn, @NOW, NULL
FROM INSERTED

[MySQL]

  • A trigger cannot be attached to more than one operation, 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
VALUES ;
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
VALUES ;
END;

Oracle">Oracle Database">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, 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
VALUES ;
END IF;
END;

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 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.
  • It is automatic, once it is created, it works with no further human intervention.
  • It is not required to have good knowledge about the tables of the database, or the data model.
  • Changes in current programming are not required.
  • Changes in the current tables are not required, because log data of any table is stored in a different one.
  • It works for both programmed and ad hoc statements.
  • Only changes are registered, so the growing rate of the history tables are proportional to the changes.
  • It is not necessary to apply the trigger to all the tables on database, it can be applied to certain tables, or certain columns of a table.

    Disadvantages

  • It does not automatically store information about the user producing the changes. This information might be provided explicitly. It could be enforced in information systems, but not in ad hoc queries.

    Examples of use

Getting the current version of a table


SELECT Column1, Column2,..., Columnn
FROM HistoryTable
WHERE EndDate IS NULL

It should return the same resultset of the whole original table.

Getting the version of a table in a certain point of time

Suppose the @DATE variable contains the point or time of interest.

SELECT Column1, Column2,..., Columnn
FROM HistoryTable
WHERE @Date >= StartDate
AND

Getting the information of an entity in a certain point of time

Suppose the @DATE variable contains the point or time of interest, and the @KEY variable contains the primary key of the entity of interest.

SELECT Column1, Column2,..., Columnn
FROM HistoryTable
WHERE Column1 = @Key
AND @Date >= StartDate
AND

Getting the history of an entity

Suppose the @KEY variable contains the primary key of the entity of interest.

SELECT Column1, Column2,..., Columnn, StartDate, EndDate
FROM HistoryTable
WHERE Column1 = @Key
ORDER BY StartDate

Getting when and how an entity was created

Suppose the @KEY variable contains the primary key of the entity of interest.

SELECT H2.Column1, H2.Column2,..., H2.Columnn, H2.StartDate
FROM HistoryTable AS H2 LEFT OUTER JOIN HistoryTable AS H1
ON H2.Column1 = H1.Column1
AND H2.Column1 = @Key
AND H2.StartDate = H1.EndDate
WHERE H2.EndDate IS NULL

Immutability of [primary key]s

Since the trigger requires that primary key being the same throughout time, it is desirable to either ensure or maximize its immutability, if a primary key changed its value, the entity it represents would break its own history.
There are several options to achieve or maximize the primary key immutability:
  • Use of a surrogate key as a primary key. Since there is no reason to change a value with no meaning other than identity and uniqueness, it would never change.
  • Use of an immutable natural key as a primary key. In a good database design, a natural key which can change should not be considered as a "real" primary key.
  • Use of a mutable natural key as a primary key where changes are propagated in every place where it is a foreign key. In such a case, the history table should be also affected.

    Alternatives

Sometimes the Slowly changing dimension is used as a method, this diagram is an example: