Search code examples
sql-server-2012change-tracking

Script to check changes on SQL Server Instance?


Is there any SQL script to check the changes on SQL Server instance made by other DB admin and i get email alert of that changes. if yes, then please provide me the script and all the steps for applying it. I am using SQL Server 2012.


Solution

  • One solution is to use a DDL trigger to catch all schema changes (procedures, functions, table definition etc.). This can work for all non-encrypted objects and of course, other admins must not disable it.

    More details about how to write such a trigger and persist changes can be found here.

    [Later edit]

    I remembered I have created such an audit on the development environment and I can provide a custom version based on the indicated article. Besides usual information, I have also included a "distance" between old object text and new object text to have a basic idea about change magnitude.

    1) Table definition:

    IF OBJECT_ID('dbo.DDLEvents', 'U') IS NULL
    BEGIN
        CREATE TABLE dbo.DDLEvents
        (
            EventId           INT IDENTITY(1, 1) NOT NULL,
            EventDate         DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            EventType         NVARCHAR(64),
            EventXML          XML,
            DatabaseName      NVARCHAR(255),
            SchemaName        NVARCHAR(255),
            ObjectName        NVARCHAR(255),
            HostName          VARCHAR(128),
            IPAddress         VARCHAR(32),
            ProgramName       NVARCHAR(255),
            LoginName         NVARCHAR(255),
            ObjectDefinition  NVARCHAR(MAX),
            LastObjDefinition NVARCHAR(MAX),
            Diff              INT                                   -- edit distance between last and current object version (gives an idea of how much was changed in the object)
        );
    
        create index IDX_DDLEvents_Object ON DDLEvents (SchemaName, ObjectName)
    END
    go
    

    2) Initial text for existing objects:

    IF NOT EXISTS(SELECT * FROM dbo.DDLEvents)
    BEGIN
        INSERT INTO dbo.DDLEvents
        (EventType, DatabaseName, SchemaName, ObjectName, LoginName, ObjectDefinition)
        SELECT 'CREATE_PROCEDURE', DB_NAME(), OBJECT_SCHEMA_NAME([object_id]), OBJECT_NAME([object_id]), 'SYSTEM', OBJECT_DEFINITION([object_id])
        FROM sys.procedures;
    
        INSERT INTO dbo.DDLEvents
        (EventType, DatabaseName, SchemaName, ObjectName, LoginName, ObjectDefinition)
        SELECT 'CREATE_VIEW', DB_NAME(), OBJECT_SCHEMA_NAME([object_id]), OBJECT_NAME([object_id]), 'SYSTEM', OBJECT_DEFINITION([object_id])
        FROM sys.views;
    
        INSERT INTO dbo.DDLEvents
        (EventType, DatabaseName, SchemaName, ObjectName, LoginName, ObjectDefinition)
        SELECT 'CREATE_FUNCTION', DB_NAME(), OBJECT_SCHEMA_NAME([object_id]), OBJECT_NAME([object_id]), 'SYSTEM', OBJECT_DEFINITION([object_id])
        FROM sys.objects
        -- scalar, inline table-valued, table-valued
        WHERE type IN ('FN', 'IF', 'TF')  
    END
    go
    

    3) Distance function (CLR):

    [Microsoft.SqlServer.Server.SqlFunction(IsDeterministic = true, IsPrecise = false)]
    public static int Levenshtein(SqlString S1, SqlString S2)
    {
        if (S1.IsNull)
            S1 = new SqlString("");
    
        if (S2.IsNull)
            S2 = new SqlString("");
    
        int maxLen = 4096;
    
        // keeping only the first part of the string (performance reasons)
        String SC1 = S1.Value.ToUpper();
        String SC2 = S2.Value.ToUpper();
    
        if (SC1.Length > maxLen)
            SC1 = SC1.Remove(maxLen);
        if (SC2.Length > maxLen)
            SC2 = SC2.Remove(maxLen);
    
        int n = SC1.Length;
        int m = SC2.Length;
    
        short[,] d = new short[n + 1, m + 1];
        int cost = 0;
    
        if (n + m == 0)
        {
            return 0;
        }
        else if (n == 0)
        {
            return 0;
        }
        else if (m == 0)
        {
            return 0;
        }
    
        for (short i = 0; i <= n; i++)
            d[i, 0] = i;
    
        for (short j = 0; j <= m; j++)
            d[0, j] = j;
    
        for (int i = 1; i <= n; i++)
        {
            for (int j = 1; j <= m; j++)
            {
                if (SC1[i - 1] == SC2[j - 1])
                    cost = 0;
                else
                    cost = 1;
    
                d[i, j] = (short) System.Math.Min(System.Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost);
            }
        }
    
        // double percentage = System.Math.Round((1.0 - ((double)d[n, m] / (double)System.Math.Max(n, m))) * 100.0, 2);
        // return percentage;
        return d[n, m];
    }
    

    4) The DDL trigger definition

    if not exists (select * from sys.triggers where name = 'DDL_Audit_Trigger')
        exec ('create trigger DDL_Audit_Trigger ON DATABASE FOR CREATE_PROCEDURE AS BEGIN PRINT 1; END')
    GO
    
    ALTER TRIGGER [DDL_Audit_Trigger]
    ON DATABASE
    FOR 
        CREATE_ASSEMBLY, ALTER_ASSEMBLY, DROP_ASSEMBLY,
        CREATE_PROCEDURE, ALTER_PROCEDURE, DROP_PROCEDURE, 
        CREATE_FUNCTION, ALTER_FUNCTION, DROP_FUNCTION,
        CREATE_INDEX, ALTER_INDEX, DROP_INDEX,
        CREATE_VIEW, ALTER_VIEW, DROP_VIEW, 
        CREATE_ROLE, ALTER_ROLE, DROP_ROLE,
        CREATE_SCHEMA, ALTER_SCHEMA, DROP_SCHEMA,
        CREATE_TABLE, ALTER_TABLE, DROP_TABLE,
        CREATE_TYPE, DROP_TYPE, 
        CREATE_USER, ALTER_USER, DROP_USER,
        CREATE_TRIGGER, ALTER_TRIGGER, DROP_TRIGGER,
        RENAME
    AS
    BEGIN
        SET NOCOUNT ON;
    
        BEGIN TRY
            DECLARE @EventData XML = EVENTDATA();
            DECLARE @ip VARCHAR(32) = ( SELECT client_net_address FROM sys.dm_exec_connections WHERE session_id = @@SPID);
            DECLARE @ObjectSchema NVARCHAR(255) = @EventData.value('(/EVENT_INSTANCE/SchemaName)[1]',  'NVARCHAR(255)')
            DECLARE @ObjectName NVARCHAR(255) = @EventData.value('(/EVENT_INSTANCE/ObjectName)[1]',  'NVARCHAR(255)')
            -- DECLARE @ObjectFullName NVARCHAR(255) = @ObjectSchema + '.' + @ObjectName
            DECLARE @CommandText NVARCHAR(MAX) = @EventData.value('(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]', 'NVARCHAR(MAX)')
    
            DECLARE @LastObjectChange DATETIME = (SELECT TOP 1 EventDate FROM dbo.DDLEvents where SchemaName = @ObjectSchema and ObjectName = @ObjectName ORDER BY EventDate DESC)
            DECLARE @LastObjectDefinition NVARCHAR(MAX) = (SELECT TOP 1 ObjectDefinition FROM dbo.DDLEvents where SchemaName = @ObjectSchema and ObjectName = @ObjectName and EventDate = @LastObjectChange ORDER BY EventDate DESC)
    
            INSERT INTO dbo.DDLEvents
            (EventType, 
                EventXML, DatabaseName,
                SchemaName, ObjectName,
                HostName, IPAddress, ProgramName, LoginName,
                ObjectDefinition, LastObjDefinition, Diff
            )
            SELECT @EventData.value('(/EVENT_INSTANCE/EventType)[1]', 'NVARCHAR(100)'), 
                @EventData, DB_NAME(),
                @ObjectSchema, @ObjectName,
                HOST_NAME(), @ip, PROGRAM_NAME(), SUSER_SNAME(),
                @CommandText, @LastObjectDefinition, dbo.Levenshtein(@CommandText, @LastObjectDefinition);
        END TRY
        BEGIN CATCH
            INSERT INTO dbo.DDLEventsLog (Error) 
            SELECT ERROR_MESSAGE()
        END CATCH
    END
    GO