使用触发器记录对数据库表的更改

时间:2009-05-22 20:24:05

标签: sql sql-server sql-server-2005 logging

我正在寻找一种很好的方法来记录在我的SQL Server 2005数据库中的一组特定表上发生的更改。我认为最好的方法是通过触发器来执行更新和删除操作。无论如何都要抓住正在运行的实际语句?一旦我有了声明,我就可以轻松地将其记录到其他地方(其他数据库表)。但是,我还没有找到一种简单的方法(如果可能的话)来获取正在运行的SQL语句。

12 个答案:

答案 0 :(得分:7)

如果您只想在某些数据库表中保留所有事务的日志(插入,更新和删除),则可以运行以下脚本:

IF NOT EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME= 'Audit')
  CREATE TABLE LogTable
  (
    LogID [int]IDENTITY(1,1) NOT NULL,
    Type char(1), 
    TableName varchar(128), 
    PrimaryKeyField varchar(1000), 
    PrimaryKeyValue varchar(1000), 
    FieldName varchar(128), 
    OldValue varchar(1000), 
    NewValue varchar(1000), 
    UpdateDate datetime DEFAULT (GetDate()), 
    UserName varchar(128)
  )
GO

DECLARE @sql varchar(8000), @TABLE_NAME sysname
SET NOCOUNT ON

SELECT @TABLE_NAME= MIN(TABLE_NAME)
FROM INFORMATION_SCHEMA.Tables 
WHERE 
--query for table that you want to audit
TABLE_TYPE= 'BASE TABLE' 
AND TABLE_NAME!= 'sysdiagrams'
AND TABLE_NAME!= 'LogTable'
AND TABLE_NAME!= 'one table to not record de log';

WHILE @TABLE_NAME IS NOT NULL
  BEGIN

  SELECT 'PROCESANDO ' + @TABLE_NAME;

  EXEC('IF OBJECT_ID (''' + @TABLE_NAME+ '_ChangeTracking'', ''TR'') IS NOT NULL DROP TRIGGER ' + @TABLE_NAME+ '_ChangeTracking')


  SELECT @sql = 'create trigger ' + @TABLE_NAME+ '_ChangeTracking on ' + @TABLE_NAME+ ' for insert, update, delete
    as
      declare 
        @bit int ,
        @field int ,
        @maxfield int ,
        @char int ,
        @fieldname varchar(128) ,
        @TableName varchar(128) ,
        @PKCols varchar(1000) ,
        @sql varchar(2000), 
        @UpdateDate varchar(21) ,
        @UserName varchar(128) ,
        @Type char(1) ,
        @PKFieldSelect varchar(1000),
        @PKValueSelect varchar(1000)

        select @TableName = ''' + @TABLE_NAME+ '''

        -- date and user
        select @UserName = system_user ,
        @UpdateDate = convert(varchar(8), getdate(), 112) + '' '' + convert(varchar(12), getdate(), 114)

        -- Action
        if exists (select * from inserted)
          if exists (select * from deleted)
            select @Type = ''U''
          else
            select @Type = ''I''
        else
          select @Type = ''D''

        -- get list of columns
        select * into #ins from inserted
        select * into #del from deleted

        -- Get primary key columns for full outer join
        select @PKCols = coalesce(@PKCols + '' and'', '' on'') + '' i.'' + c.COLUMN_NAME + '' = d.'' + c.COLUMN_NAME
          from INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,
          INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
          where pk.TABLE_NAME = @TableName
          and CONSTRAINT_TYPE = ''PRIMARY KEY''
          and c.TABLE_NAME = pk.TABLE_NAME
          and c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME

        -- Get primary key fields select for insert(comma deparated)           
        select @PKFieldSelect = coalesce(@PKFieldSelect+''+'','''') + '''''''' + COLUMN_NAME + '','''''' 
          from INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,
          INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
          where pk.TABLE_NAME = @TableName
          and CONSTRAINT_TYPE = ''PRIMARY KEY''
          and c.TABLE_NAME = pk.TABLE_NAME
          and c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME

        -- Get primary key values for insert(comma deparated as varchar)           
        select @PKValueSelect = coalesce(@PKValueSelect+''+'','''') + ''convert(varchar(100), coalesce(i.'' + COLUMN_NAME + '',d.'' + COLUMN_NAME + ''))'' + ''+'''','''''' 
          from INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,    
          INFORMATION_SCHEMA.KEY_COLUMN_USAGE c   
          where  pk.TABLE_NAME = @TableName   
          and CONSTRAINT_TYPE = ''PRIMARY KEY''   
          and c.TABLE_NAME = pk.TABLE_NAME   
          and c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME 

        if @PKCols is null
        begin
          raiserror(''no PK on table %s'', 16, -1, @TableName)
          return
        end

        select @sql = ''insert LogTable(Type, TableName, PrimaryKeyField, PrimaryKeyValue, UserName)''
        select @sql = @sql + '' select '''''' + @Type + ''''''''
        select @sql = @sql + '','''''' + @TableName + ''''''''
        select @sql = @sql + '','' + @PKFieldSelect
        select @sql = @sql + '','' + @PKValueSelect
        select @sql = @sql + '','''''' + @UserName + ''''''''

        select @sql = @sql + '' from #ins i full outer join #del d''
        select @sql = @sql + @PKCols        

        exec (@sql)
    ';
  SELECT @sql
  EXEC(@sql)


  SELECT @TABLE_NAME= MIN(TABLE_NAME) FROM INFORMATION_SCHEMA.Tables 
    WHERE TABLE_NAME> @TABLE_NAME
    --query for table that you want to audit
    AND TABLE_TYPE= 'BASE TABLE' 
    AND TABLE_NAME!= 'sysdiagrams'
    AND TABLE_NAME!= 'LogTable'
    AND TABLE_NAME!= 'one table to not record de log';
END

答案 1 :(得分:5)

您应该可以使用system management views完成此操作。

一个例子是这样的:

SELECT er.session_id,
  er.status,
  er.command,
  DB_NAME(database_id) AS 'DatabaseName',
  user_id,
  st.text
FROM sys.dm_exec_requests AS er
  CROSS APPLY sys.dm_exec_sql_text(er.sql_handle) AS st
WHERE er.session_id = @@SPID;

我不确定这对你有用,因为更多以数据为中心的日志记录机制可能会这样。

答案 2 :(得分:4)

不要忘记您的日志记录将成为事务的一部分,因此如果出现错误并回滚事务,您的日志也将被删除。

答案 3 :(得分:2)

MSSQL具有名为“已插入”和“已删除”的虚拟表,其中包含新插入和/或新删除和/或新更新的数据记录,您可以从触发器访问...我使用这些,知道哪些数据已经改变(而不是告诉哪些语句改变了数据)。

答案 4 :(得分:2)

有一种模式可用于创建名为日志触发器的此类触发器。这是独立于供应商的,非常简单。它在here中描述。

更改记录在另一个历史记录表中。没有办法获取确切的语句,但可以检测它是否是插入,更新或删除,因为它创建了一个“链式”记录集。插入是没有前任的记录,删除是没有后继的记录,中间记录是更新。可以检测到记录与其前任记录的比较。

在给定的时间点获取单个实体(或整个表)的快照非常容易。

作为奖励,与Oracle,DB2和MySQL相比,SQL Server的这种模式的语法恰好是最简单的。

答案 5 :(得分:1)

您是否真的需要记录运行的语句,大多数人都会记录更改的数据(触发器中的INSERTED和DELETED表)。

答案 6 :(得分:1)

触发器很糟糕,我会远离触发器。

如果您尝试对某些内容进行问题排查,请将Sql Profiler附加到具有特定条件的数据库中。这将记录每次查询运行以供检查。

另一种选择是更改为调用程序以记录其查询。这是一种非常普遍的做法。

答案 7 :(得分:1)

触发器是确保记录任何更改的好方法,因为无论更新如何执行,它们几乎总是会触发 - 例如ad-hoc连接以及应用程序连接。

正如@mwigdahl所建议的那样,系统管理视图看起来是捕获当前运行批处理的好方法。这对于登录触发器是否特别有用是另一回事。

使用触发器的一个缺点是您只能从数据库连接中识别更新源。许多应用程序没有与连接关联的任何用户信息,以便于连接池,因此您不知道哪个用户正在执行该操作。即,连接使用的登录是通用应用程序登录,而不是使用该应用程序的人。解决此问题的常用方法是使用存储过程作为所有数据库交互的接口,然后确保UserId与所有过程调用一起传递。然后,您可以通过存储过程而不是触发器执行日志记录。显然,只有当您知道人们不使用这些程序直接更新表,或者不需要记录这种情况时,这才有用。

获取当前正在执行的批处理的能力可能提供更好的机制:如果您确保所有sql批处理包含UserId,则可以从触发器中的sql中提取它。这将允许您使用触发器执行所有日志记录,这意味着您捕获所有内容,但也允许您将更改与特定用户相关联。

如果您正在按下触发路径,那么值得检查情况触发器是否被触发(可能是批量加载数据?或者如果人们有权禁用触发器)。

同时考虑@idstam指出触发器代码将在您的事务中,因此通常会记录并随之回滚。

编写触发器时要考虑的另一件事是@@IDENTITY的行为:如果你有使用@@ IDENTITY的程序,你可能会意外地改变他们的行为。

答案 8 :(得分:1)

使用Log Trigger

没有理由捕获实际的SQL,因为可以有许多不同的语句以相同的方式更改数据。

答案 9 :(得分:0)

尝试安装一些基于触发器的第三方工具,例如ApexSQL Audit,然后对它们的执行方式进行逆向工程。只需将其安装在试用模式下,看看它是如何生成触发器以捕获所有不同类型的信息的。

需要考虑的其他几个方面是:

存储规划 - 如果您有大量更新,则意味着您将获得大量审计数据。我考虑将这些数据存储在不同的数据库中。特别是如果您计划审核多个数据库。

管理数据量 - 随着时间的推移,您可能不需要保留一些非常旧的记录。计划轻松删除旧数据

架构更改 - 如果更新架构会怎样。在最坏的情况下,如果没有正确创建,您的触发器将停止工作并抛出错误。在最好的情况下,你会错过一些数据。这也是需要考虑的事情。

考虑到所有这些因素,与一些已经开发的解决方案相比,这可能是最有效的时间,而不是自己从头开始创建。

答案 10 :(得分:0)

这是根据Juan Carlos Velez的回答改编的。我对其进行了修改,以解决复合主键以及包含空格的列名的问题。另外,我在全文中都进行了注释,以便希望对其目的进行修改的人可以理解每个步骤(如果他们对代码不清楚的话)的情况。

-- This stops the message that shows the count of the number of rows affected from being returned as part of the result set.
set nocount on

-- If the Audit table doesn't exist, create it.
if not exists(select * from INFORMATION_SCHEMA.TABLES where TABLE_NAME = 'Audit')
create table Audit
(
    AuditID [int] identity(1,1) not null,
    [Type] char(1), 
    TableName nvarchar(128), 
    PKFields nvarchar(max),
    PKValues nvarchar(max),
    FieldName nvarchar(128), 
    OldValue nvarchar(max), 
    NewValue nvarchar(max), 
    UpdateDate datetime, 
    UserName nvarchar(128)
)
go

-- Variables for the dynamic SQL and table name.
declare @tr nvarchar(max),
@tableName sysname

-- Get the first table in database. Skip over views and a few specified tables.
select @tableName = min(TABLE_NAME) from INFORMATION_SCHEMA.TABLES where TABLE_TYPE = 'BASE TABLE' and TABLE_NAME <> 'sysdiagrams' and TABLE_NAME <> 'Audit'
---- If you want to specify certain tables, uncomment the next line and add your table names.
--and (TABLE_NAME = 'IGAs' or TABLE_NAME = 'IGABudgets' or TABLE_NAME = 'Resolutions' or TABLE_NAME = 'RTAProjects' or TABLE_NAME = 'RTASubProjects')

-- Loop through the tables in the database and create an audit trigger on each one.
while @tableName is not null
begin

    -- If a trigger of the same name already exists, delete it.
    exec('if OBJECT_ID (''' + @tableName + '_ChangeTracking'', ''TR'') is not null drop trigger ' + @tableName + '_ChangeTracking')

    -- Check if there is a primary key. If not, throw an error.
    if (select count(*) from INFORMATION_SCHEMA.TABLE_CONSTRAINTS c, INFORMATION_SCHEMA.KEY_COLUMN_USAGE u where c.TABLE_NAME = @tableName and c.CONSTRAINT_TYPE = 'PRIMARY KEY' and u.TABLE_NAME = c.TABLE_NAME and u.CONSTRAINT_NAME = c.CONSTRAINT_NAME) = 0
    begin
        raiserror('Error: There is no primary key on table %s', 16, -1, @tableName)
        return
    end

    -- Create the trigger.
    select @tr = 'create trigger ' + @tableName + '_ChangeTracking on ' + @tableName + ' for insert, update, delete as

    -- Misc variables.
    declare @table nvarchar(128),
    @fieldName nvarchar(128) = '''',
    @type char(1),
    @pkJoin nvarchar(max),
    @pkSelect nvarchar(max),
    @pkFields nvarchar(max),
    @pkValues nvarchar(max),
    @updateDate nvarchar(30) = convert(varchar(30), getdate(), 22),
    @user nvarchar(128) = system_user,
    @sql nvarchar(max),
    @params nvarchar(max) = N''@out nvarchar(max) output'',
    @fieldIndex int = 0,
    @maxField int,
    @bit int,
    @char int

    -- Get the table name.
    select @table = object_name(parent_id) from sys.triggers where object_id = @@PROCID

    -- Get the modification type: U = update, I = insert, D = delete
    if exists (select * from inserted)
        if exists (select * from deleted)
            select @type = ''U''
        else select @type = ''I''
    else select @type = ''D''

    -- Save the inserted and deleted values into temp tables.
    select * into #ins from inserted
    select * into #del from deleted

    -- Get the number of columns in the table.
    select @maxField = max(columnproperty(object_id(@table), COLUMN_NAME, ''ColumnID'')) from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = @table

    -- Get the primary key join relationship(s).
    select @pkJoin = coalesce(@pkJoin + '' and'', '' on'') + '' i.['' + u.COLUMN_NAME + ''] = d.['' + u.COLUMN_NAME + '']''
    from INFORMATION_SCHEMA.TABLE_CONSTRAINTS c, INFORMATION_SCHEMA.KEY_COLUMN_USAGE u
    where c.TABLE_NAME = @table
    and c.CONSTRAINT_TYPE = ''PRIMARY KEY''
    and u.TABLE_NAME = c.TABLE_NAME
    and u.CONSTRAINT_NAME = c.CONSTRAINT_NAME

    -- Get the primary key field name(s).
    select @pkFields = coalesce(@pkFields + '', '', '''') + ''['' + u.COLUMN_NAME + '']''
    from INFORMATION_SCHEMA.TABLE_CONSTRAINTS c, INFORMATION_SCHEMA.KEY_COLUMN_USAGE u
    where c.TABLE_NAME = @table
    and c.CONSTRAINT_TYPE = ''PRIMARY KEY''
    and u.TABLE_NAME = c.TABLE_NAME
    and u.CONSTRAINT_NAME = c.CONSTRAINT_NAME

    -- Get the primary key field(s) for select statement.
    select @pkSelect = coalesce(@pkSelect + '' + '''', '''' + '', '''') + ''convert(nvarchar(max), ['' + u.COLUMN_NAME + ''])''
    from INFORMATION_SCHEMA.TABLE_CONSTRAINTS c, INFORMATION_SCHEMA.KEY_COLUMN_USAGE u
    where c.TABLE_NAME = @table
    and c.CONSTRAINT_TYPE = ''PRIMARY KEY''
    and u.TABLE_NAME = c.TABLE_NAME
    and u.CONSTRAINT_NAME = c.CONSTRAINT_NAME

    -- Get the primary key field value(s).
    if (@type = ''D'')
    begin
        set @sql = ''select @out = '' + @pkSelect + '' from #del''
        exec sp_executesql @sql, @params, @out = @pkValues output
    end
    else
    begin 
        set @sql = ''select @out = '' + @pkSelect + '' from #ins''
        exec sp_executesql @sql, @params, @out = @pkValues output
    end

    -- Loop through each field in the inserted table.
    while @fieldIndex < @maxField
    begin

        -- Iterate the fieldIndex.
        select @fieldIndex = min(ORDINAL_POSITION) from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = @table and columnproperty(object_id(@table), COLUMN_NAME, ''ColumnID'') > @fieldIndex 

        -- If the column in scope has been modified, insert a record into the Audit table.
        select @bit = (@fieldIndex - 1)% 8 + 1
        select @bit = POWER(2, @bit - 1)
        select @char = ((@fieldIndex - 1) / 8) + 1
        if substring(columns_updated(), @char, 1) & @bit > 0 or @Type IN (''I'', ''D'')
        begin

            -- Get the name of the field whose ColumnID equals the current fieldIndex.
            select @fieldName = ''['' + COLUMN_NAME + '']'' from INFORMATION_SCHEMA.COLUMNS 
            where TABLE_NAME = @table and columnproperty(object_id(@table), COLUMN_NAME, ''ColumnID'') = @fieldIndex '

    -- Select statements have a length limitation. End the statement, then add the rest.
    select @tr = @tr + '

            set @sql = ''insert into Audit (Type, TableName, PKFields, PKValues, FieldName, OldValue, NewValue, UpdateDate, UserName) select '''''' + @type + '''''', '''''' + @table + '''''', '''''' + @pkFields + '''''', '''''' + @pkValues + '''''', '''''' + @fieldName + '''''', convert(nvarchar(max), d.'' + @fieldName + ''), convert(nvarchar(max), i.'' + @fieldName + ''), '''''' + @updateDate + '''''', '''''' + @user + '''''' from #ins i full outer join #del d'' + @pkJoin + '' where i.'' + @fieldName + '' <> d.'' + @fieldName + '' or (i.'' + @fieldName + '' is null and d.'' + @fieldName + '' is not null) or (i.'' + @fieldName + '' is not null and d.'' + @fieldName + '' is null)''

            --print(@sql)
            exec(@sql)

        end
    end'

    ---- This is if you want to see the statement that is generated rather than execute it.
    --select @tr

    -- Execute the trigger statement.
    exec(@tr)

    -- Iterate the table name.
    select @tableName = min(TABLE_NAME) from INFORMATION_SCHEMA.TABLES 
    where TABLE_NAME > @tableName and
    TABLE_TYPE = 'BASE TABLE' and TABLE_NAME <> 'sysdiagrams' and TABLE_NAME <> 'Audit'
    ---- If you want to specify certain tables, uncomment the next line and add your table names.
    --and (TABLE_NAME = 'IGAs' or TABLE_NAME = 'IGABudgets' or TABLE_NAME = 'Resolutions' or TABLE_NAME = 'RTAProjects' or TABLE_NAME = 'RTASubProjects')

end

答案 11 :(得分:-4)

这里要小心,因为触发器在ROW级别触发,而不是SQL STATEMENT级别。因此,如果某人“删除了BIGTABLE”,那么您的触发器将触发该表中的每一行(这特别是关于您想知道执行该操作的SQL语句的事实,因此您需要“图”说出“对于声明影响的每一行”。