Technology Introduced Events Tracked Data Available PerformanceDML Triggers — fires T-SQL code on table events Beginning of time Instead Of or After Insert, Update, Delete, Merge Inserte
Trang 1Technology Introduced Events Tracked Data Available Performance
DML Triggers — fires T-SQL
code on table events
Beginning
of time
Instead Of or After Insert, Update, Delete, Merge
Inserted and deleted tables, all columns, all changes with user context
Synchronous within transaction; depending on width of table and amount of code, may have
a significant impact PerfMon/SysMon — Windows
utilities
Beginning
of time
Listens for hundreds of Windows/SQL Server events
Counters for nearly any event, no actual data
Virtually no impact
SQL Trace and Profiler — SQL
Server internal monitoring and UI
6.5 Monitors 179 SQL
Server internal events
Event data, no actual data
Profiler may have an impact, trace alone has little impact Wait States — tracks system
waits
7 Every time a process
waits for a resource
Aggregate count of waits
by wait type
No impact C2 Auditing — combines SQL
Trace and security settings for
compliance
2000 All security events All security-related data Minor impact
Common Criteria
Compliance — combines SQL
Trace and security settings for
compliance
2005 All security events All security-related data Minor impact
DMVs — dynamic management
views, catalog views
metadata and current status
No impact
Management Studio
Reports — easy way to view
DMV data
DMVs
Some impact depending on report
Performance Dashboard —
downloadable add-on with extra
reports
DMVs
Some impact depending on report
DDL Triggers — responds to
server and database DDL events
2005 All server and database
DDL events and logins
EventDate()
provides all info in XML format
Rarely called, so no real impact
Change Tracking —
synchronously records which
rows have been updated
2008 Row inserts, updates,
and deletes
PK with net change, columns changed, very limited user context
Little impact
Change Data Capture —
asynchronously reads T-Log and
records data changes
2008 Insert, Update, Delete,
Merge
All or net data changed, all column values, no user context
Reads from the log, so no impact during the transaction, but impact of the overall work Extended Events — lightweight
windows monitoring technology
2008 Nearly any server or
database event, similar
to SQL Trace
Event detail – similar to SQL Trace/Profiler
Very lightweight
SQL Auditing — SQL Server
interface for extended events
2008 Any extended event Detail dependent on
extended events
Very lightweight Management Data
Warehouse — collects
performance data
2008 Nearly continuous
recording of performance data
Aggregate historical performance data
Lightweight, but requires server
to collect data Policy-Based Management —
enforces and reports on
2008 Any defined
configuration policy
Can view health level of any object
No impact
Trang 2Data Audit Triggers
IN THIS CHAPTER
Creating a robust data-audit trail
Rolling back changes Undeleting rows Thinking through auditing complications
My consulting firm once developed a legal compliance/best-practices
document-management system for a Fortune 100 company whose law
firm was populating the database with regulatory laws The law firm fell
behind on its schedule and claimed that it was unable to enter data for two weeks
because of software problems When a list of the more than 70,000 column-level
data changes made during those two weeks was provided from the data-audit
trail, the claim vanished
Data auditing is just a plain good idea
The section ‘‘Data Architecture’’ in Chapter 2 lists data auditing as a key
contribu-tor toward the security and integrity database objectives
Although Microsoft has added several auditing technologies, there’s still a place
for the old trigger/audit table solution It’s still the best way to build a complete
audit trail of every value change ever made to a row since the inception of
the database
A trigger based data-audit trail can provide very detailed history of the data,
including the following:
■ All data changes to a row since it was inserted
■ All data changes made by a specific user last week
■ All data changes from a certain workstation during lunch
■ All data changes made from an application other than the standard
front-end application
I’ve seen published methods of auditing data that add a few columns to the table,
or duplicate the table, to store the last change Neither of these methods is
worth-while A partial audit, or a last-value audit, is of no real value A robust audit trail
should track every change, from every table, in a generic audit table
Trang 3Audit triggers that write every DML change to a generic audit table come in two basic flavors — fixed
and dynamic:
■ Fixed audit triggers: These are custom written for each table and include hard-coded code for each column There are two problems inherent with fixed audit triggers First, they are a pain to write: tedious, back-breaking, repetitive, time intensive, and error prone code
Second, the only task worse than coding a fixed audit trigger is maintaining it The audit triggers must be kept in perfect sync with everyALTER TABLE; otherwise, either the trigger generates an error, or the data audit trail is incomplete
■ Dynamic audit triggers: These investigate the metadata and either pass the data to the CLR
or use dynamic SQL to insert into the audit table There are a couple dynamic audit trigger
solutions on the Internet, and I published a dynamic SQL audit trigger in the SQL Server 2000
Bible These solutions are easy to implement but perform as well as a slug on a lazy day Plus,
the use of dynamic SQL requires the end-users to have more privileges in the database than is typically desired in most database applications
AutoAudit
The hybrid solution I’m proposing in this chapter is to dynamically code-gen perfect fixed audit trail
triggers using the techniques covered in Chapter 29, ‘‘Dynamic SQL and Code Generation.’’
The result is the best performance possible from a trigger, but no manual coding When the schema
changes, just rerun the code-gen procedure to recreate the audit trigger
As I’m writing, version 1.09a of AutoAudit is available on www.codeplex.com/AutoAudit.
It’s a work in progress, so check for updates.
Besides recording every column value change to an audit table, I like to record some generic audit data
right in the row:
■ Createdcolumn to record the date the row was inserted
■ Modifiedcolumn to timestamp the last update
■ Versioncolumn to count the number of times the row has been updated AutoAudit adds these three columns to any table, and code-gens the trigger code to keep themodified
andversioncolumns up-to-date
This version is limited to tables with single-column primary keys (That’s not likely to change anytime
soon, because modifying the audit table on the fly to handle composite primary keys adds too much
complexity and only a few have requested the feature.) It’s written to be compatible with SQL Server
2005 and SQL Server 2008
Installing AutoAudit
The first time the AutoAudit script is run in a database it creates thedbo.Audittable and four stored
procedures:
Trang 4■ dbo.pAutoAudit: Code-gens the changes required for a single table
■ dbo.pAutoAuditAll: Executes AutoAudit on every table in the database
■ dbo.pAutoAuditDrop: Backs out the changes made by AutoAudit for a single table
■ dbo.pAutoAuditDropAll: Backs out the changes made by AutoAudit for every table in the
database
The audit table
TheAudittable’s purpose is to provide a single location in which to record data changes for the
database The following audit-trail table can store all non-BLOB changes to any table TheOperation
column stores anI,U, orD, depending on the DML statement This is the table created by executing
the AutoAudit script:
CREATE TABLE dbo.Audit (
AuditID BIGINT NOT NULL IDENTITY PRIMARY KEY CLUSTERED,
AuditDate DATETIME NOT NULL,
HostName sysname NOT NULL,
SysUser VARCHAR(50) NOT NULL,
Application VARCHAR(50) NOT NULL,
TableName SYSNAME NOT NULL,
Operation CHAR(1) NOT NULL, i,u,d
PrimaryKey INT NOT NULL, edit to suite
RowDescription VARCHAR(50) NULL, Optional
SecondaryRow VARCHAR(50) NULL, Optional
ColumnName SYSNAME NULL, required for i,u not for D
OldValue VARCHAR(50) NULL, edit to suite - varchar(MAX)?
NewValue VARCHAR(50) NULL
Version INT NULL
);
ThePrimaryKeycolumn stores the pointer to the row that was modified, and theRowDescription
column records a readable description of the row These two columns enable the audit trail to be joined
with the original table or viewed directly ThePrimaryKeycolumn is important because it can quickly
find all changes to a single row regardless of how the description has changed over time
Running AutoAudit
Once the AutoAudit script is run in the current database,pAutoAuditmay be executed for any table
The parameters are self-explanatory:
EXEC pAutoAudit @Schema, @Table;
The stored procedure makes several changes to the table:
1 Adds thecreated,modified, andversioncolumns to the table
2 Code-gens and creates the_modifiedupdate trigger, which updates themodifiedand
rowversioncolumns
Trang 53 Code-gens and creates the_updatetrigger, which writes any updated column values to the audittable
4 Code-gens and creates the_deletetrigger, which writes any deleted column values to the audittable
The stored procedure then creates two new objects in the database:
5 Code-gens and creates the_deletedview, which reassembles the deleted rows from the audittable
6 Code-gens and creates the_RowHistoryuser-defined function, which presents a historical view of the data for any row
The following code executespAutoAuditin theAdventureWorks2008database on the
Departmenttable:
EXEC pAutoAudit ‘HumanResources’, ‘Department’
ExecutingpAutoAuditAllwill runpAutoAuditagainst every table in the current database
_Modified trigger
The generatedProduct_ Modifiedtrigger isn’t very complicated The trigger simply updates the
modifiedcolumn to the current date time and increments therowversioncolumn As with any
trigger, joining theinsertedtable with the base table limits the rows affected to those rows being
updated:
CREATE TRIGGER [HumanResources].[Department_Modified] ON [HumanResources].[Department]
AFTER Update NOT FOR REPLICATION AS SET NoCount On
generated by AutoAudit on Apr 28 2009 3:38PM created by Paul Nielsen
www.SQLServerBible.com Begin Try
If Trigger_NestLevel (object_ID(N‘[HumanResources].[Department_Modified]’)) > 1 Return;
If (Update(Created) or Update(Modified)) AND Trigger_NestLevel() = 1 Begin; Raiserror(‘Update failed.’, 16, 1); Rollback; Return; End;
Update the Modified date UPDATE [HumanResources].[Department]
SET Modified = getdate(), [Version] = [Department].[Version] + 1 FROM [HumanResources].[Department]
JOIN Inserted
ON [Department].[DepartmentID] = Inserted.[DepartmentID] End Try Begin Catch
Raiserror(‘error in [HumanResources].[Department_modified] trigger’,
Trang 616, 1 ) with log
End Catch
GO
Auditing changes
The real workhorse of AutoAudit is the_Updatetrigger, which writes every update, column by
column, to theaudittable The trigger is a bit repetitive, but here’s a sample of the code it creates for
each column (slightly formatted for readability):
IF UPDATE([Name])
INSERT dbo.Audit
(AuditDate, SysUser, Application, HostName, TableName,
Operation, PrimaryKey, RowDescription, SecondaryRow,
ColumnName, OldValue, NewValue, RowVersion)
SELECT @AuditTime, suser_sname(), APP_NAME(), Host_Name(),
‘HumanResources.Department’, ‘u’, Convert(VARCHAR(50),
Inserted.[DepartmentID]),
NULL, Row Description (e.g Order Number)
NULL, Secondary Row Value
(e.g Order Number for an Order Detail Line)
‘[Name]’,
Convert(VARCHAR(50), Deleted.[Name]),
Convert(VARCHAR(50), Inserted.[Name]),
DELETED.Rowversion + 1
FROM Inserted
JOIN Deleted
ON Inserted.[DepartmentID] = Deleted.[DepartmentID]
AND isnull(Inserted.[Name],‘’) <> isnull(Deleted.[Name],‘’) Here’s how this works TheINSERTstatement joins theInsertedandDeletedtables on the primary
key to correctly handle multiple-row inserts and updates The join is also restricted with anot-equals
<>join condition so that when a multiple-row update only affects some of the rows for a given column,
only those rows that are actually changed are recorded to the audit trail
The_Inserttrigger audits all inserts In AutoAudit version 1.09b, inserts are minimally logged to save
space The only row written to theAudittable is the insert event with the primary key value The idea
is that between the base table, the insert event, and the fully audited updates, the complete history can
be reconstructed A popular request is the verbose insert, which would audit every value on insert — I’ll
probably add that to the next version
With AutoAudit’s code-generated ‘‘fixed audit trigger’’ installed, the following batch exercises it by
insert-ing and updatinsert-ing work order data:
INSERT Production.WorkOrder
(ProductID, OrderQty, ScrappedQty,
StartDate, EndDate, DueDate, ScrapReasonID)
VALUES (757, 25, 3,
‘2008-9-20’, ‘2008-9-23’, ‘2008-9-24’, 2)
Trang 7UPDATE Production.WorkOrder SET DueDate = ‘2008-10-12’
WHERE WorkOrderID = 72592 With these two changes made, AutoAudit recorded these audits:
SELECT AuditDate, SysUser, Operation, PrimaryKey as Key, ColumnName, OldValue as Old, NewValue as New, Version
FROM dbo.Audit Result:
AuditDate SysUser Op Key ColumnName Old New Version - - - - - - -2009-04-28 20:10:50.567 Maui\Pn i 72592 [WorkOrderID] NULL 72592 1
2009-04-28 20:10:53.137 Maui\Pn u 72592 [ScrappedQty] 3 5 2 The trigger is the right place to implement an audit trail because it will catch all the changes, even those
made directly to the table with DML commands
Viewing and undeleting deleted rows
The_Deletetrigger writes all the final values for every column to theaudittable From this
infor-mation, the final state of the row can be easily recreated using a case expression-style crosstab query like
this one for theAdventureWorks2008products table (abbreviated for space):
SELECT Max(Case ColumnName WHEN ‘[ProductID]’ THEN OldValue ELSE ‘’ END) AS [ProductID], Max(Case ColumnName
WHEN ‘[Name]’ THEN OldValue ELSE ‘’ END) AS [Name], Max(Case ColumnName WHEN ‘[ProductNumber]’ THEN OldValue ELSE ‘’ END) AS [ProductNumber],
Max(Case ColumnName WHEN ‘[ModifiedDate]’ THEN OldValue ELSE ‘’ END) AS [ModifiedDate], Max(Case ColumnName
WHEN ‘[Created]’ THEN OldValue ELSE ‘’ END) AS [Created], Max(Case ColumnName
WHEN ‘[Modified]’ THEN OldValue ELSE ‘’ END) AS [Modified], Max(Case ColumnName
WHEN ‘[RowVersion]’ THEN OldValue ELSE ‘’ END) AS [RowVersion],
Trang 8MAX(AuditDate) AS ‘Deleted’
FROM Audit
Where TableName = ‘SalesLT.Product’ AND Operation = ‘d’
GROUP BY PrimaryKey;
For every table audited by AutoAudit, it creates a view, namedvtablename_Deleted, that contains
the code-genned crosstab query Selecting from this view displays all the rows deleted from the table
To undelete rows, it’s trivial toinsert .selectfrom the_deletedview
Viewing row history
A feature I’ve recently added and am still enhancing is the row history user-defined function Its purpose
is to provide a visual history The function is code-genned when AutoAudit is applied to a table After
four updates to theDB Auditrow in theHumanResources.Departmenttable, the_RowHistory()
function returns the following story:
SELECT AuditDate, Version AS Ver, Name, GroupName
FROM HumanResources.Department_RowHistory(22)
Result:
- -
-2009-04-29 10:46:23 1 DB Audit Compliance Dept
2009-04-29 10:46:52 2 Data Audit Compliance Dept
2009-04-29 10:47:13 3 Data Auditing Compliance Dept
2009-04-29 10:47:26 4 Data Auditing Compliance and Audit Dept
2009-04-29 10:48:04 5 Data Forensics Data Audit
Note that the_RowHistory()function requires the primary key as a parameter To return multiple
histories, use theCROSS APPLYcommand
Backing out AutoAudit
AutoAudit makes several changes to the database schema, modifying tables and creating objects It
would be irresponsible to not provide an automated way to roll back these changes The following
command removes all changes made bypAutoAudit, with one key exception: It does not drop the
audittable, so any saved audit values are retained:
dbo.pAutoAuditDrop @Schema, @Table
Auditing Complications
While AutoAudit provides a rather complete audit trail solution, there are other factors that you should
consider when implementing it, modifying it, or creating your own custom solution
Trang 9Best Practice
Develop the entire database and prove that the data schema is correct prior to implementing a data-audit
trail Changes to the data schema are more complex once audit-trail triggers are in place
Auditing related data
The most significant complication involves auditing related data such as secondary rows For example,
a change to anOrderDetailrow is actually a change to the order A user will want to see the data
history of the order and all changes to any of the data related to the order Therefore, a change to the
OrderDetailtable should be recorded as a change to the[Order]table, and the line number of
the order detail item that was changed is recorded in theSecondaryRowcolumn
Recording foreign key changes is another difficult aspect of a full audit trail A user does not want to
see the new GUID or identity value for a foreign-key update If the order-ship-method foreign key is
changed from ‘‘Slow Boat’’ to ‘‘Speedy Express,’’ the audit-trail viewing procedure might want to look up
the meaningful value and display that instead of the surrogate key value
Auditing select statements
Data-audit triggers are limited to auditingINSERT,UPDATE, andDELETEDML statements To audit
data reads, implement the read audit in theFETCHstored procedure Use SQL Server security to limit
access to the table so that all reads must go through a stored procedure or a function
In my software product I record everyobjectfetchoperation with theobjectID,username, and
datetimein a separate table This table serves as a select audit and feeds the user’s recently viewed
objects feature
Data auditing and security
Another concern for those creating a full data-audit history is the security of the data-audit trail
Any-one who has read rights to the audit table will be able to effectively see all the data from every audited
table If users will be able to see the data history for a given row, use a stored procedure to fetch the
audit data so that security can be preserved
Data auditing and performance
A full data-audit trail will add some level of overhead to the system A single row insert to a
20-column-wide table will add 20 inserts to the audit table To reduce the performance impact of the audit trail, do
the following:
■ Limit the indexes on the audit table
■ Locate the audit table on its own filegroup and disk subsystem A separate filegroup will make backups easier as well
Trang 10■ Consider archiving older audits and then using distributed partitioned views to see all the
audit data
■ Edit the fixed audit trigger to limit the auditing to those columns that require such a high level
of data integrity
One question when creating an audit trail is whether or not to write the insert DML values to the audit
trail Including the inserts completes the audit trail, and the rows may theoretically be completely
recre-ated from the audit trail alone The problem, of course, with writing inserts is that the audit trail grows
larger than the original base tables
Most OLTP databases see significantly more inserts than updates Most rows are inserted and never
updated Therefore, if the original tables are used to store the original data, and the audit trail records
only insert events and changes, then theaudittable is significantly smaller than if every insert were
fully audited This improves performance by eliminating the audit during inserts and reduces the size of
the audit table
AutoAudit only records update values, but the commented out code to code-gen an insert audit
trigger is in the AutoAudit stored procedure
Summary
With all the new compliance regulations and requirements, auditing is more important than ever But
auditing is a pain I’ve wasted way too many hours keeping a fixed audit trail in sync with schema
changes, and the best dynamic audit triggers are slow
I’ve created AutoAudit to help you solve some of your auditing problems If after using it you have any
suggestions, e-mail them to me, or add a request in CodePlex and vote for your idea
Key points from this chapter include the following:
■ A single audit table is easier to query for changes across the whole database than an individual
audit-table-per-base-table approach
■ AutoAudit will code-gen triggers, views, and user-defined functions for your auditing solution
■ Deleted rows can be viewed or undeleted using thevtable_deletedview
■ Row history can be viewed using the well-named_RowHistoryuser-defined function
■ AutoAudit requires a single-column primary key
■ Suggest improvements and vote for enhancements onwww.CodePlex.com/AutoAudit
The next chapter continues the thread of auditing with triggers, but it moves from auditing data to
auditing the schema