Zurück

Table Auditing with SQL Server 2000


You can download all Procedures and Triggers here

Auditing

Often a table will need to be audited, particularly if your client has hired a number data entry people who are prone to make mistakes. Auditing a table allows you to see who made changes, when the changes were made, what changed, and the history of all states the data have been in (so the data are never lost).

Auditing a table consists of:

  • Adding a few columns to the table to record the time of update and the identity of the person making the update.
  • Creating a separate audit table that contains all old versions of the data in the table.
  • Creating a trigger which automatically stuffs a row into the audit table whenever the original table is modified.

 In the following example we show how the first point can be implemented.

Audit INSERTs and UPDATEs

Add the following columns to each table you want to audit:

credat

Date of first INSERT

creusr

Creator of first INSERT

mutdat

String containing Date of each UPDATE, e.g:
2003-09-10 12:34:10/2003-12-10 09:10:12/2003-12-02 13:10:21

mutusr

String containing Username of each UPDATE, e.g:
zahn/mueller/meier

The columns creadat and creuser contains the date and creator-name of the initial row in the table. This columns are never updated later, so you can see when and who has created the first row.

The columns mutdat and mutusr are strings containing the last 5 (or more) modifications. If there are more than 5 modifications the strings will be shift from right to left, so the oldest entry on the most left side of the modification string will be thrown away.

Example

Create the following Table which you want to audit:

IF EXISTS (SELECT * FROM sysobjects
            WHERE id = OBJECT_ID(N'[dbo].[test_audit]')
              AND OBJECTPROPERTY(id, N'IsTable') = 1)
DROP TABLE [dbo].[test_audit]
GO

CREATE TABLE test_audit (
    id         INTEGER        NOT NULL,
    credat     SMALLDATETIME  NOT NULL,
    creusr     VARCHAR(8)     NOT NULL,
    mutdat     VARCHAR(500),
    mutusr     VARCHAR(45)

)
GO

Create the following helper function to get the login name. If Windows-Authentication is used,
the domain-part is truncated (e.g. \\SERVER\Zahn ==> Zahn)

IF EXISTS (SELECT * FROM sysobjects
            WHERE id = OBJECT_ID(N'[dbo].[get_login_name]')
              AND OBJECTPROPERTY(id, N'IsScalarFunction') = 1)
DROP FUNCTION [dbo].[get_login_name]
GO

/*
* ==============================================================================
* Function:  get_login_name
*
* Returns:   Name of the logged-in user. If Windows-Authentication is used,
*            the domain-part is truncated (e.g. \\SERVER\Zahn ==> Zahn)
* ==============================================================================
*/

CREATE FUNCTION get_login_name()
RETURNS VARCHAR(8)
AS
BEGIN
    RETURN RIGHT(SYSTEM_USER,(LEN(SYSTEM_USER)-CHARINDEX('\',SYSTEM_USER)))
END
GO

Add the following Defaults for the columns credat and creusr.

ALTER TABLE test_audit ADD
    CONSTRAINT default_credat DEFAULT (getdate()) FOR credat,
    CONSTRAINT default_erfusr DEFAULT (dbo.get_login_name()) FOR creusr
GO

Create the following helper function, to count the number of substrings in a string.

IF EXISTS (SELECT * FROM sysobjects
            WHERE id = OBJECT_ID(N'[dbo].[cnt_strings]')
              AND OBJECTPROPERTY(id, N'IsScalarFunction') = 1)
DROP FUNCTION [dbo].[cnt_strings]
GO

/*
* ==============================================================================
* Function:  cnt_strings
*
* Arguments: @substring: String to be count
*            @string:    String in which to count
*
* Returns:   The number of Occurrences of Substring in String.
* ==============================================================================
*/

CREATE FUNCTION cnt_strings (@substring VARCHAR(5), @string VARCHAR(300))
  RETURNS SMALLINT AS
BEGIN

   
-- Position in String
    DECLARE @pos SMALLINT

    -- Count the Occurrences of Substring in String
    DECLARE @count SMALLINT

   
-- Length of Substring and String
    DECLARE @lenString SMALLINT
    DECLARE @lenSubString SMALLINT

   
-- Initializing
    set @pos = -1
    set @count = 0
    set @lenString = LEN(@string)
    set @lenSubString = LEN(@substring)

   
-- Loop through the String and count Occurences of Substring
    WHILE (@pos <> 0)
    BEGIN
        SET @pos = CHARINDEX(@substring,@string,@pos)
        IF (@pos = 0) BREAK

       
-- Substring was found
        SET @pos = @pos + @lenSubstring
        SET @count = @count +1
    END

    RETURN @count
END
GO

Create the main audit function, which manipulates the update-string:

/*
* ==============================================================================
* Function:  log_audit
*
* Arguments: @old_value: string containing @separator separated substrings
*            @separator: string separator
*            @new_value: value to be appended
*
* Returns:   Table Audtiting - The Columns mutdat and mutusr reflects any
*            updates on the Table. These columns contains a ";" separated
*            String with the Mutations-User / Mutations-Date. The last update
*            is on the most right side, max num_entries Updates are seen.
*            If there are more than 10 Updates the most left entry is deleted
*            from the string.
* ==============================================================================
*/
CREATE  FUNCTION log_audit (@old_value VARCHAR(500), @separator VARCHAR(5), @new_value VARCHAR(20))
  RETURNS VARCHAR(500) AS
BEGIN
  DECLARE @first_occurrence SMALLINT
  DECLARE @num_entries SMALLINT
  SET @num_entries = 10
  -- Return new_value if multi occurrence field is empty IF (@old_value IS NULL)
  BEGIN
    RETURN @new_value
  END
  -- Remove first occurrence (most left) if max number of entries are reached IF (dbo.cnt_strings (@separator, @old_value) >= (@num_entries - 1))
  BEGIN
    -- @first_occurrence contains position of first semicolon.
    -- The position is counted from the right side,
    -- as needed in the right function.   SET @first_occurrence = LEN(@old_value) - CHARINDEX(@separator, @old_value)
    SET @old_value = RIGHT(@old_value, @first_occurrence)
  END
  RETURN @old_value+@separator+@new_value
END
GO

Finally create the following UPDATE Trigger. Triggers make use of two special tables called inserted and deleted. The inserted table contains the data referenced in an INSERT before it is actually committed to the database. The deleted table contains the data in the underlying table referenced in a DELETE before it is actually removed from the database. When an UPDATE is issued both tables are used. More specifically, the new data referenced in the UPDATE statement is contained in inserted and the data that is being updated is contained in deleted.

IF EXISTS (SELECT * FROM sysobjects
            WHERE id = OBJECT_ID(N'[dbo].[update_log_test_audit]')
              AND OBJECTPROPERTY(id, N'IsTrigger') = 1)
DROP TRIGGER [dbo].[update_log_test_audit]
GO

CREATE TRIGGER update_log_test_audit
  ON dbo.test_audit FOR UPDATE
  NOT FOR REPLICATION AS
UPDATE test_audit SET
  test_audit.mutdat =
  dbo.log_audit(deleted.mutdat,'|',CONVERT(VARCHAR(20),GETDATE(),120)),
  test_audit.mutusr = dbo.log_audit(deleted.mutusr,'|',dbo.get_login_name())
 FROM inserted, deleted
WHERE test_audit.id = inserted.id
GO

Test the Auditing

Insert some values into the test table, note that we set @num_entries = 3 for this test:

INSERT INTO test_audit (id) VALUES (1)
INSERT INTO test_audit (id) VALUES (2)
INSERT INTO test_audit (id) VALUES (3)
INSERT INTO test_audit (id) VALUES (4)
INSERT INTO test_audit (id) VALUES (5)
GO

SELECT * FROM test_audit
GO

The initial values for credat and creusr are inserted by the Defaults. Now update row 2:

UPDATE test_audit SET id = 6 WHERE id = 2
GO

Update the test table as follows:

UPDATE test_audit SET id = 2 WHERE id = 6
GO

Now, two entries are included in mutdat and mutusr.

UPDATE test_audit SET id = 6 WHERE id = 2
GO

SELECT id,mutdat,mutusr FROM test_audit
GO

UPDATE test_audit SET id = 2 WHERE id = 6
GO

SELECT id,mutdat,mutusr FROM test_audit
GO

In the last screendump, you can see, that the first entry "2003-10-02 11:30:51" is disappered from the string, it was shifted out of the string.