By: Aaron Bertrand || Related Tips:More > Database Administration
ProblemIn a previous tip, SQL Server DDL Triggers to Track All Database Changes , and in a couple of follow-ups, I explained how to capture all DDL changes on a SQL Server instance and store them in an auditing table. I left it as an exercise to the reader to determine how (and how often) they would use that table to notify their team about changes that had taken place. Some people just added an e-mail call within the trigger, and others set up jobs that polled the table periodically. I wanted to post a follow-up tip to describe how I would do it, along with a couple of corrections to the way I set things up in those earlier tips.
SolutionLet’s get some housekeeping out of the way first. There were two important things I wanted to correct that were brought up by readers of the previous tips:
If a DDL event occurs under snapshot isolation, and the auditing database does not support snapshot isolation, you will get this error:Msg 3952, Level 16, State 1, Procedure DDLTrigger_Sample
To avoid this error, you’ll want to issue this statement against your auditing database:ALTER DATABASE AuditDB SET ALLOW_SNAPSHOT_ISOLATION ON;
If you implemented anything to provide regular users with additional permissions to access the IP address of the user through sys.dm_exec_connections, such as VIEW SERVER STATE or IMPERSONATE, you should change the following pieces of code:DECLARE @ip VARCHAR(32) =
(
SELECT client_net_address
FROM sys.dm_exec_connections
WHERE session_id = @@SPID
);
To this:DECLARE @ip varchar(48) = CONVERT(varchar(48), CONNECTIONPROPERTY('client_net_address'));
Now you can remove any additional permissions you granted, since any user can access their own information from the DMV.
Okay, with those out of the way, let’s move on to the notification system.
You may want to be notified about every single change, but most people would want to avoid the noise that would create and focus on objects that changed in any given time frame. For this purpose we’ll assume that you already have Database Mail up and running (reviewthese tips if you need to set it up or it isn’t working correctly).
Notify on Every SQL Server DDL ChangeIf you want to be notified about every single change, the solution is simple: you can modify the DDL trigger to call msdb.dbo.sp_db_sendmail every time a DDL change takes place.
CREATE TRIGGER DDLTrigger_SampleON DATABASE
FOR CREATE_PROCEDURE, ALTER_PROCEDURE, DROP_PROCEDURE,
ALTER_SCHEMA, RENAME, CREATE_VIEW, ALTER_VIEW,
CREATE_FUNCTION, ALTER_FUNCTION, ALTER_TABLE --, ... other events
AS
BEGIN
SET NOCOUNT ON;
DECLARE @EventData xml = EVENTDATA(),
@ip varchar(48) = CONVERT(varchar(48), CONNECTIONPROPERTY('client_net_address'));
<strong>DECLARE</strong>
<strong> @subject nvarchar(max) = N'',</strong>
<strong> @body nvarchar(max) = N'',</strong>
<strong> @db sysname = DB_NAME(),</strong>
<strong> @schema sysname = @EventData.value(N'(/EVENT_INSTANCE/SchemaName)[1]', N'nvarchar(255)'),</strong>
<strong> @object sysname = @EventData.value(N'(/EVENT_INSTANCE/ObjectName)[1]', N'nvarchar(255)'),</strong>
<strong> @event sysname = @EventData.value(N'(/EVENT_INSTANCE/EventType)[1]', N'nvarchar(100)');</strong>
<strong>BEGIN TRY</strong> -- if e-mail errors, still want audit table updated
<strong> SET @subject = @@SERVERNAME + N' : ' + @event + N' : ' + @object;</strong>
<strong> SET @body = CONVERT(nvarchar(max), @EventData);</strong>
-- you may want to add additional details to body, such as username, hostname, etc.
<strong> EXEC msdb.dbo.sp_send_dbmail</strong>
<strong> @profile_name = N'profile name',</strong>
<strong> @recipients = N'DBA team alias',</strong>
<strong> @subject = @subject,</strong>
<strong> @body = @body;</strong>
<strong>END TRY</strong>
<strong> BEGIN CATCH</strong>
<strong>PRINT 'error';</strong> -- do real error handling here, like log exception somewhere
<strong> END CATCH</strong>
INSERT AuditDB.dbo.DDLEvents
(
EventType,
EventDDL,
EventXML,
DatabaseName,
SchemaName,
ObjectName,
HostName,
IPAddress,
ProgramName,
LoginName
)
SELECT
@event,
@EventData.value(N'(/EVENT_INSTANCE/TSQLCommand)[1]', N'nvarchar(max)'),
@EventData,
DB_NAME(),
@schema,
@object,
HOST_NAME(),
@ip,
APP_NAME(),
SUSER_SNAME(); -- or ORIGINAL_LOGIN() or CURRENT_USER or ...
END
GO
Most people are not crazy about that idea, though, because then they’re getting e-mails at unpredictable times and they may get notified about a flurry of similar or even identical changes to the same object.
Notify on a Schedule for SQL Server DDL ChangesSetup here is a little more complex, but it is quite easy to configure things so that you are only notified once per time period about a change (or change type) for any given object.
First, remove any of those notify-on-every-change edits you made to your DDL trigger above.
Next, we’ll need to add two columns to our auditing table; one to track whether an event has already been included in a notification, and another to signify when the notification was sent out:
USE AuditDB;GO
ALTER TABLE dbo.DDLEvents ADD NotifyStatus tinyint NOT NULL DEFAULT(0);
GO
UPDATE dbo.DDLEvents SET NotifyStatus = 2; -- already notified
GO
ALTER TABLE dbo.DDLEvents ADD NotifyDateTime datetime2(0);
GO
UPDATE dbo.DDLEvents SET NotifyDateTime = SYSUTCDATETIME();
GO
This means that new events will be eligible for the next notification, but everything that is already in the table will be considered “already notified” this ensures that when you run this procedure the first time, you aren’t inundated with a big “all of time” e-mail.
Next, we’ll need a stored procedure that will mark all the new events since the last notification as “in process,” and then aggregate those and build a message with one line per object (and/or event type). The reason to mark them first is to carve out the set of events you’re going to notify on, and not have that set disrupted by any new events that come in while processing is happening.
CREATE PROCEDURE dbo.DDLEvents_NotifyAS
BEGIN
SET NOCOUNT ON;
-- probably want transaction handling here, though once
-- an e-mail is queued, ROLLBACK can’t exactly undo it!
UPDATE dbo.DDLEvents
SET NotifyStatus = 1 -- in process
WHERE NotifyStatus = 0; -- new
IF @@ROWCOUNT > 0
BEGIN
DECLARE @body nvarchar(max) = N'',
@subject nvarchar(max) = @@SERVERNAME + N' : $x$ total DDL changes since $d$';
-- if you want a row per object + event type combination, uncomment EventT