Create Data Audit in SQL Server - sql

I've recently been given the task of creating an Audit on a database table so that any changes made to any columns can be tracked.
Lets say I have the following table:
[TableA]
------
ID
ColumnA
ColumnB
ColumnC
For Auditing I've created a table such as:
[TableA.Audit]
------
ID
TableAID
UserID
Date (default value = getdate())
ColumnA
ColumnB
ColumnC
I've then wrote a script like:
DECLARE #currentColumnA int
,#currentColumnB int
,#currentColumnC int
SELECT TOP 1 #currentColumnA=ColumnA
,#currentColumnB=ColumnB
,#currentColumnC=ColumnC
FROM [TableA]
WHERE ID=#TableAID
UPDATE [TableA]
SET ColumnA=#ColumnA
,ColumnB=#ColumnB
,ColumnC=#ColumnC
WHERE ID=#TableAID
INSERT INTO [TableA.Audit] (TableAID, UserID, ColumnA, ColumnB, ColumnC)
VALUES (#TableAID, #UserID, NULLIF(#ColumnA, #currentColumnA), NULLIF(#ColumnB, #currentColumnB), NULLIF(#ColumnC, #currentColumnC))
The problem with this, is that if I was to add a ColumnD field to TableA I'm going to have to edit my TableA.Audit table as well as the above script.
Therefore is there a better way of doing this?

You are better off writing triggers for the table for AFTER INSERT, AFTER DELETE, and AFTER UPDATE. This way, any time ANYTHING (application, Management Studio, etc.) that inserts, updates, or deletes data in the table will get logged. You'll have to add a field for the audit action, and in your trigger insert the literal for the action (e.g. 'I' or 'INSERT'). I structure my audit tables in this way:
audit_id: INT IDENTITY
audit_date: DATETIME GETDATE()
audit_action: VARCHAR(16) ... or you can use CHAR(1)
audit_user: VARCHAR(128) SUSER_SNAME()
(the fields from the table being audited)
Since our apps use Active Directory, I can default audit_user to SUSER_SNAME().

We use triggers (the only way to go and make sure you write them to handle mulitple record inserts/updates/deletes) and our structure is a bit differnt.
First we have a table that stores the information about the action, the person/aplicationthat did it the date thenumber of affected records. Then we havea table that stores the details. This table has an identifier column, column_name, old value, new value. (we use nvarchar (max) for the columns in the audit table) This way if the table gets new columns we don't have to worry about changing the audit tables. We have one set of audit tables for each table we audit.
Newer versions of Sql server have change tracking but we don't find it has enough detail for the auditing we need and it deletes the data too quickly unless you move it to another permanent table.

The problem with this, is that if I was to add a ColumnD field to
TableA I'm going to have to edit my TableA.Audit table as well as the
above script.
Therefore is there a better way of doing this?
Not really. You can make the implementation better via trigger's as HardCode mentions but you still have to modify the audit and related scirpts.
I've witnessed attempts to make this "better" where you don't have to update a trigger or audit table. This always results in trading the minor problem (hey a column got added and I've got to do some stuff) for much larger ones. Usually performance, correctness and reliability issues.

Related

How to retrieve the name of the active SAVEPOINT in Informix

Is there a way in Informix (v12 or higher) to retrieve the name of the current SAVEPOINT?
In Oracle there is something similar: You can name the transaction using SET TRANSACTION NAME and then select the transaction name from v$transaction:
SELECT name
FROM v$transaction
WHERE xidusn
|| '.'
|| xidslot
|| '.'
|| xidsqn = DBMS_TRANSACTION.LOCAL_TRANSACTION_ID;
That is not very straightforward, but it does the trick. Effectively we can use that to have a transaction scoped variable (yes, that is ugly, but it works for years now).
We have a mechanism based on this and would like to port that to Informix. Is there a way to do that?
Of course, if there is a different mechanism providing transaction scoped variables (so DEFINE GLOBAL is not what we are looking for), that would be helpful, too, but I doubt, there is one.
Thank you all for your comments so far.
Let me show the solution I have come up with. It is just a work in progress idea, but I hope it will lead somewhere:
I will need a "audit_lock" table which always contains a record for the current transaction carrying information about the current transaction, especially a username and a unique transaction_id (UUID or similar). That row will be inserted on starting the transaction and deleted before committing it.
Then I have a generic audit_trail table containing the audited information.
All audited tables fill the generic audit trail table using triggers, serializing each audited column into a separate record of the generic audit trail table.
The audit_lock and the audit_trail table need to use row locking. Also to avoid read locks on the audit_lock table we need to set the isolation level to COMMITTED READ LAST COMMITTED. If your use case does not support that, the suggested pattern does not work.
Here's the DDL:
CREATE TABLE audit_lock
(
transaction_id varchar(40) primary key,
username varchar(40)
);
alter table audit_lock
lock mode(ROW);
CREATE TABLE audit_trail
(
id serial primary key,
tablename varchar(255) NOT NULL,
record_id numeric(10) NOT NULL,
username varchar(40) NOT NULL,
transaction_id varchar(40) NOT NULL,
changed_column_name varchar(40),
old_value varchar(40),
new_value varchar(40),
operation varchar(40) NOT NULL,
operation_date datetime year to second NOT NULL
);
alter table audit_trail
lock mode(ROW);
Now we need to have the audited table:
CREATE TABLE audited_table
(
id serial,
somecolumn varchar(40)
);
And the table has an insert trigger writing into the audit_trail:
CREATE PROCEDURE proc_trigger_audit_audited_table ()
REFERENCING OLD AS o NEW AS n FOR audited_table;
INSERT INTO audit_trail
(
tablename,
record_id,
username,
transaction_id,
changed_column_name,
old_value,
new_value,
operation,
operation_date
)
VALUES
(
'audited_table',
n.id,
(SELECT username FROM audit_lock),
(SELECT transaction_id FROM audit_lock),
'somecolumn',
'',
n.somecolumn,
'INSERT',
sysdate
);
END PROCEDURE;
CREATE TRIGGER audit_insert_audited_table INSERT ON audited_table REFERENCING NEW AS post
FOR EACH ROW(EXECUTE PROCEDURE proc_trigger_audit_audited_table() WITH TRIGGER REFERENCES);
Now let's use that: First the caller of the transaction needs to generate a transaction_id for himself, maybe using a UUID generation mechanism. In the example below the transaction_id is simply '4711'.
BEGIN WORK;
SET ISOLATION TO COMMITTED READ LAST COMMITTED; --should be set globally
-- Issue the generation of the audit_lock entry at the beginnig of each transaction
insert into audit_lock (transaction_id, username) values ('4711', 'userA');
-- Is it there?
select * from audit_lock;
-- do data manipulation stuff
insert into audited_table (somecolumn) values ('valueA');
-- Issue that at the end of each transaction
delete from audit_lock
where transaction_id = '4711';
commit;
In a quick test, all of this worked even in simultaneaous transactions. Of course, that still needs a lot of work and testing, but I currently hope that path is feasible.
Let me also add a little bit more info on the other approach we are using in Oracle:
In Oracle we are (ab)using the transaction name, to store exactly the information that in the suggestion above is stored in the audit_lock table.
The rest is the same as above. The triggers work perfectly in that specific application, even though there are of course a lot of scenarios for other applications, where putting insert, delete and update triggers on each table generating records for each changed column in the table would be nuts. In our application it works perfectly for ten years now and it has no mentionable performance impact on the way the application is used.
In the java application server all code blocks, that are changing data, start with setting the transaction name first and then do loads of changes to various tables, that might be issuing all these triggers. All of these are running in the same transaction and since that has a transaction name which contains the application user, the triggers can write that information to the audit trail table.
I know there are other approaches to the problem, and you could even do that with hibernate features only, but our approach allows us to enforce some consistency through the database (NOT NULL constraint in the audit trail table on the username). Since everything is done via triggers, we can let those fail, if the transaction name is not containing the user (by requiring it to be in a specific format). If there any other portions of the application, other applications or ignorant administrators trying to issue updates to the audited tables without respecting to set the transaction name to the specific format, those updates will fail. This makes updates to the audited tables, that do not generate the required audit table entries harder (certainly not impossible, a ill willing admin can do anything, of course).
So all of you that are cringing now, let me quote Luis: Might seem like a terrible idea, but I have my use case ;)
The idea of #Luís to creating a specific table in each transaction to store the information causes a locking issue in systables. Let's call that "transaction info table". That idea did not cross my mind, since DDL causes commits in Oracle. So I tried that in Informix but if I try to create a table called "tblX" in two simultaneaous transactions, the second transaction get's a locking exception:
Cannot update system catalog (systables). [SQL State=IX000, DB Errorcode=-312]
Next: ISAM error: key value locked [SQL State=IX000, DB Errorcode=-144]
But letting all transactions use the same table as above works, as far as I tested it right now.

Auto-Increment editable Column

I am looking for the best solution, of a problem I've met.
I have several databases, which I align quite often to have the same data in it. In some referential tables there are Auto-Increment ID columns, and in other tables there are columns that match these ID values. Unfortunately, on the source DB IDs after changes might be like: (10,15,20), while, after inserting it into destination table it is (1,2,3). I hope my mock-ups clarify the situation:
Source DB RefTable:
ID_________Name
10_________a
15_________b
20_________c
Destination DB RefTable:
ID_________Name
1__________a
2__________b
3__________c
Source FactTable:
RefTableID___OtherColumns
10__________a
10__________b
15__________c
15__________d
The problem is that after migrating FactTable to the destination DB it does not match records from the RefTable. I had an idea to add another column that will be editable in contrast to ID column. The issue is that the end-user should not see it (it will not mean anything for him), so it should be also auto-increment. What should I use in this situation:
sequence?
-trigger?
-post add/save stored procedure?
Or maybe someone has better idea how to solve this problem?
In SQL Server, an identity columns can't be updated once the row was inserted into a table.
However, it's value can be inserted manually by using set identity_insert for the specified table.
In each session, you can have only a single table with identity_insert set to on at the same time, so always set it back to off once insert is complete.
The code might look something like this:
set identity_insert RefTable on;
insert into RefTable (id, name)
select id, name
from sourceDb.sourceSchema.RefTable s
where not exists (
select 1
from RefTable t
where t.Id = s.Id
);
set identity_insert RefTable off;

Adding Row in existing table (SQL Server 2005)

I want to add another row in my existing table and I'm a bit hesitant if I'm doing the right thing because it might skew the database. I have my script below and would like to hear your thoughts about it.
I want to add another row for 'Jane' in the table, which will be 'SKATING" in the ACT column.
Table: [Emp_table].[ACT].[LIST_EMP]
My script is:
INSERT INTO [Emp_table].[ACT].[LIST_EMP]
([ENTITY],[TYPE],[EMP_COD],[DATE],[LINE_NO],[ACT],[NAME])
VALUES
('REG','EMP','45233','2016-06-20 00:00:00:00','2','SKATING','JANE')
Will this do the trick?
Your statement looks ok. If the database has a problem with it (for example, due to a foreign key constraint violation), it will reject the statement.
If any of the fields in your table are numeric (and not varchar or char), just remove the quotes around the corresponding field. For example, if emp_cod and line_no are int, insert the following values instead:
('REG','EMP',45233,'2016-06-20 00:00:00:00',2,'SKATING','JANE')
Inserting records into a database has always been the most common reason why I've lost a lot of my hairs on my head!
SQL is great when it comes to SELECT or even UPDATEs but when it comes to INSERTs it's like someone from another planet came into the SQL standards commitee and managed to get their way of doing it implemented into the final SQL standard!
If your table does not have an automatic primary key that automatically gets generated on every insert, then you have to code it yourself to manage avoiding duplicates.
Start by writing a normal SELECT to see if the record(s) you're going to add don't already exist. But as Robert implied, your table may not have a primary key because it looks like a LOG table to me. So insert away!
If it does require to have a unique record everytime, then I strongly suggest you create a primary key for the table, either an auto generated one or a combination of your existing columns.
Assuming the first five combined columns make a unique key, this select will determine if your data you're inserting does not already exist...
SELECT COUNT(*) AS FoundRec FROM [Emp_table].[ACT].[LIST_EMP]
WHERE [ENTITY] = wsEntity AND [TYPE] = wsType AND [EMP_COD] = wsEmpCod AND [DATE] = wsDate AND [LINE_NO] = wsLineno
The wsXXX declarations, you will have to replace them with direct values or have them DECLAREd earlier in your script.
If you ran this alone and recieved a value of 1 or more, then the data exists already in your table, at least those 5 first columns. A true duplicate test will require you to test EVERY column in your table, but it should give you an idea.
In the INSERT, to do it all as one statement, you can do this ...
INSERT INTO [Emp_table].[ACT].[LIST_EMP]
([ENTITY],[TYPE],[EMP_COD],[DATE],[LINE_NO],[ACT],[NAME])
VALUES
('REG','EMP','45233','2016-06-20 00:00:00:00','2','SKATING','JANE')
WHERE (SELECT COUNT(*) AS FoundRec FROM [Emp_table].[ACT].[LIST_EMP]
WHERE [ENTITY] = wsEntity AND [TYPE] = wsType AND
[EMP_COD] = wsEmpCod AND [DATE] = wsDate AND
[LINE_NO] = wsLineno) = 0
Just replace the wsXXX variables with the values you want to insert.
I hope that made sense.

How to store historical records in a history table in SQL Server

I have 2 tables, Table-A and Table-A-History.
Table-A contains current data rows.
Table-A-History contains historical data
I would like to have the most current row of my data in Table-A, and Table-A-History containing historical rows.
I can think of 2 ways to accomplish this:
whenever a new data row is available, move the current row from Table-A to Table-A-History and update the Table-A row with the latest data (via insert into select or select into table)
or
whenever a new data row is available, update Table-A's row and insert a new row into Table-A-History.
In regards to performance is method 1 or 2 better? Is there a better different way to accomplish this?
Basically you are looking to track/audit changes to a table while keeping the primary table small in size.
There are several ways to solve this issue. The cons and pros of each way is discussed below.
1 - Auditing of the table with triggers.
If you are looking to audit the table (inserts, updates, deletes), look at my how to revent unwanted transactions - SQL Saturday slide deck w/code - http://craftydba.com/?page_id=880. The trigger that fills the audit table can hold information from multiple tables, if you choose, since the data is saved as XML. Therefore, you can un-delete an action if necessary by parsing the XML. It tracks who and what made the change.
Optionally, you can have the audit table on it's own file group.
Description:
Table Triggers For (Insert, Update, Delete)
Active table has current records.
Audit (history) table for non-active records.
Pros:
Active table has smaller # of records.
Index in active table is small.
Change is quickly reported in audit table.
Tells you what change was made (ins, del, upd)
Cons:
Have to join two tables to do historical reporting.
Does not track schema changes.
2 - Effective dating the records
If you are never going to purge the data from the audit table, why not mark the row as deleted but keep it for ever? Many systems like people soft use effective dating to show if a record is no longer active. In the BI world this is called a type 2 dimensional table (slowly changing dimensions). See the data warehouse institute article. http://www.bidw.org/datawarehousing/scd-type-2/ Each record has a begin and end date.
All active records have a end date of null.
Description:
Table Triggers For (Insert, Update, Delete)
Main table has both active and historical records.
Pros:
Historical reporting is easy.
Change is quickly shown in main table.
Cons:
Main table has a large # of records.
Index of main table is large.
Both active & history records in same filegroup.
Does not tell you what change was made (ins, del, upd)
Does not track schema changes.
3 - Change Data Capture (Enterprise Feature).
Micorsoft SQL Server 2008 introduced the change data capture feature. While this tracks data change (CDC) using a LOG reader after the fact,
it lacks things like who and what made the change. MSDN Details - http://technet.microsoft.com/en-us/library/bb522489(v=sql.105).aspx
This solution is dependent upon the CDC jobs running. Any issues with sql agent will cause delays in data showing up.
See change data capture tables.
http://technet.microsoft.com/en-us/library/bb500353(v=sql.105).aspx
Description:
Enable change data capture
Pros:
Do not need to add triggers or tables to capture data.
Tells you what change was made (ins, del, upd) the _$operation field in
<user_defined_table_CT>
Tracks schema changes.
Cons:
Only available in enterprise version.
Since it reads the log after the fact, time delay in data showing up.
The CDC tables do not track who or what made the change.
Disabling CDC removes the tables (not nice)!
Need to decode and use the _$update_mask to figure out what columns changed.
4 - Change Tracking Feature (All Versions).
Micorsoft SQL Server 2008 introduced the change tracking feature. Unlike CDC, it comes with all versions; However, it comes with a bunch of TSQL functions that you have to call to figure out what happened.
It was designed for the purpose of synchronization one data source with SQL server via an application. There is a whole synchronization frame work on TechNet.
http://msdn.microsoft.com/en-us/library/bb933874.aspx
http://msdn.microsoft.com/en-us/library/bb933994.aspx
http://technet.microsoft.com/en-us/library/bb934145(v=sql.105).aspx
Unlike CDC, you specify how long changes last in the database before being purged. Also, inserts and deletes do not record data. Updates only record what field changed.
Since you are synchronizing the SQL server source to another target, this works fine.
It is not good for auditing unless you write a periodic job to figure out changes.
You will still have to store that information somewhere.
Description:
Enable change tracking
Cons:
Not a good auditing solution
The first three solutions will work for your auditing. I like the first solution since I use it extensively in my environment.
Sincerely
John
Code Snippet From Presentation (Autos Database)
--
-- 7 - Auditing data changes (table for DML trigger)
--
-- Delete existing table
IF OBJECT_ID('[AUDIT].[LOG_TABLE_CHANGES]') IS NOT NULL
DROP TABLE [AUDIT].[LOG_TABLE_CHANGES]
GO
-- Add the table
CREATE TABLE [AUDIT].[LOG_TABLE_CHANGES]
(
[CHG_ID] [numeric](18, 0) IDENTITY(1,1) NOT NULL,
[CHG_DATE] [datetime] NOT NULL,
[CHG_TYPE] [varchar](20) NOT NULL,
[CHG_BY] [nvarchar](256) NOT NULL,
[APP_NAME] [nvarchar](128) NOT NULL,
[HOST_NAME] [nvarchar](128) NOT NULL,
[SCHEMA_NAME] [sysname] NOT NULL,
[OBJECT_NAME] [sysname] NOT NULL,
[XML_RECSET] [xml] NULL,
CONSTRAINT [PK_LTC_CHG_ID] PRIMARY KEY CLUSTERED ([CHG_ID] ASC)
) ON [PRIMARY]
GO
-- Add defaults for key information
ALTER TABLE [AUDIT].[LOG_TABLE_CHANGES] ADD CONSTRAINT [DF_LTC_CHG_DATE] DEFAULT (getdate()) FOR [CHG_DATE];
ALTER TABLE [AUDIT].[LOG_TABLE_CHANGES] ADD CONSTRAINT [DF_LTC_CHG_TYPE] DEFAULT ('') FOR [CHG_TYPE];
ALTER TABLE [AUDIT].[LOG_TABLE_CHANGES] ADD CONSTRAINT [DF_LTC_CHG_BY] DEFAULT (coalesce(suser_sname(),'?')) FOR [CHG_BY];
ALTER TABLE [AUDIT].[LOG_TABLE_CHANGES] ADD CONSTRAINT [DF_LTC_APP_NAME] DEFAULT (coalesce(app_name(),'?')) FOR [APP_NAME];
ALTER TABLE [AUDIT].[LOG_TABLE_CHANGES] ADD CONSTRAINT [DF_LTC_HOST_NAME] DEFAULT (coalesce(host_name(),'?')) FOR [HOST_NAME];
GO
--
-- 8 - Make DML trigger to capture changes
--
-- Delete existing trigger
IF OBJECT_ID('[ACTIVE].[TRG_FLUID_DATA]') IS NOT NULL
DROP TRIGGER [ACTIVE].[TRG_FLUID_DATA]
GO
-- Add trigger to log all changes
CREATE TRIGGER [ACTIVE].[TRG_FLUID_DATA] ON [ACTIVE].[CARS_BY_COUNTRY]
FOR INSERT, UPDATE, DELETE AS
BEGIN
-- Detect inserts
IF EXISTS (select * from inserted) AND NOT EXISTS (select * from deleted)
BEGIN
INSERT [AUDIT].[LOG_TABLE_CHANGES] ([CHG_TYPE], [SCHEMA_NAME], [OBJECT_NAME], [XML_RECSET])
SELECT 'INSERT', '[ACTIVE]', '[CARS_BY_COUNTRY]', (SELECT * FROM inserted as Record for xml auto, elements , root('RecordSet'), type)
RETURN;
END
-- Detect deletes
IF EXISTS (select * from deleted) AND NOT EXISTS (select * from inserted)
BEGIN
INSERT [AUDIT].[LOG_TABLE_CHANGES] ([CHG_TYPE], [SCHEMA_NAME], [OBJECT_NAME], [XML_RECSET])
SELECT 'DELETE', '[ACTIVE]', '[CARS_BY_COUNTRY]', (SELECT * FROM deleted as Record for xml auto, elements , root('RecordSet'), type)
RETURN;
END
-- Update inserts
IF EXISTS (select * from inserted) AND EXISTS (select * from deleted)
BEGIN
INSERT [AUDIT].[LOG_TABLE_CHANGES] ([CHG_TYPE], [SCHEMA_NAME], [OBJECT_NAME], [XML_RECSET])
SELECT 'UPDATE', '[ACTIVE]', '[CARS_BY_COUNTRY]', (SELECT * FROM deleted as Record for xml auto, elements , root('RecordSet'), type)
RETURN;
END
END;
GO
--
-- 9 - Test DML trigger by updating, deleting and inserting data
--
-- Execute an update
UPDATE [ACTIVE].[CARS_BY_COUNTRY]
SET COUNTRY_NAME = 'Czech Republic'
WHERE COUNTRY_ID = 8
GO
-- Remove all data
DELETE FROM [ACTIVE].[CARS_BY_COUNTRY];
GO
-- Execute the load
EXECUTE [ACTIVE].[USP_LOAD_CARS_BY_COUNTRY];
GO
-- Show the audit trail
SELECT * FROM [AUDIT].[LOG_TABLE_CHANGES]
GO
-- Disable the trigger
ALTER TABLE [ACTIVE].[CARS_BY_COUNTRY] DISABLE TRIGGER [TRG_FLUID_DATA];
** Look & Feel of audit table **
The recent versions of SQL server (2016+ and Azure) have temporal tables which provide the exact functionality requested, as a first class feature.
https://learn.microsoft.com/en-us/sql/relational-databases/tables/temporal-tables
Somebody at Microsoft probably read this page. :)
Logging changes is something I've generally done using triggers on a base table to record changes in a log table. The log table has additional columns to record the database user, action and date/time.
create trigger Table-A_LogDelete on dbo.Table-A
for delete
as
declare #Now as DateTime = GetDate()
set nocount on
insert into Table-A-History
select SUser_SName(), 'delete-deleted', #Now, *
from deleted
go
exec sp_settriggerorder #triggername = 'Table-A_LogDelete', #order = 'last', #stmttype = 'delete'
go
create trigger Table-A_LogInsert on dbo.Table-A
for insert
as
declare #Now as DateTime = GetDate()
set nocount on
insert into Table-A-History
select SUser_SName(), 'insert-inserted', #Now, *
from inserted
go
exec sp_settriggerorder #triggername = 'Table-A_LogInsert', #order = 'last', #stmttype = 'insert'
go
create trigger Table-A_LogUpdate on dbo.Table-A
for update
as
declare #Now as DateTime = GetDate()
set nocount on
insert into Table-A-History
select SUser_SName(), 'update-deleted', #Now, *
from deleted
insert into Table-A-History
select SUser_SName(), 'update-inserted', #Now, *
from inserted
go
exec sp_settriggerorder #triggername = 'Table-A_LogUpdate', #order = 'last', #stmttype = 'update'
Logging triggers should always be set to fire last. Otherwise, a subsequent trigger may rollback the original transaction, but the log table will have already been updated. This is a confusing state of affairs.
How about method 3: Make Table-A a view against Table-A-History. Insert into Table-A-History and let appropriate filtering logic generate Table-A. That way you're only inserting into one table.
Even though it consumes more space, having the history table containing the most recent record as well will save you pain on writing reports and seeing how changes occurred and when. Something worth thinking about in my opinion.
As far as performance, I would expect them to be identical. But, you certainly wouldn't want to delete the record (option 1's "move") from the non-hist table because you are using referential integrity between the two tables, right?
I would prefer method 1
In addition, I will have also maintain the current record in the history table too
it depends on the need.
Option 1 is OK.
But you have method 4 too :)
Insert new record to your table,
Move old record to archive table on regular base using mysql scheduler. You can schedule data archivation at the time of minimal load, for example at night hours.
You can simply create procedure or job to overcome this issue like this:
create procedure [dbo].[sp_LoadNewData]
AS
INSERT INTO [dbo].[Table-A-History]
(
[1.Column Name], [2.Column Name], [3.Column Name], [4.Column Name]
)
SELECT [1.Column Name], [2.Column Name], [3.Column Name], [4.Column Name]
FROM dbo.[Table-A] S
WHERE NOT EXISTS
(
SELECT * FROM [dbo].[Table-A-History] D WHERE D.[1.Column Name] =S.[1.Column Name]
)
Note: [1.Column Name] is common column for the tables.

Most efficient way to sustain a maximum number of entries in SQL Server database table

I was wondering, what is the most efficient way of doing the following?
I'm trying to implement some sort of an auditing system where each logon to my page will be stored in a database. I use SQL Server 2005 database. The table that stores the auditing data obviously cannot grow without an upper limit. So, say, it should have a maximum of 1000 entries and then any older entries must be deleted when new ones are inserted. The question is how do you do this in a most efficient way -- do I need to add any special columns, like, say an ordinal entry number for easier clean-up?
EDIT:
Say, if the structure of my table is (pseudo code):
`id` BIGINT autoincrement
`date` DATETIME
`data1` NVARCHAR(256)
`data2` INT
How would you write this cleanup procedure?
As Tony mentioned, use dates to identify the inserts. In addition, use a clustered index on the date field, so that inserts are always at the end of the table and it is easy and efficient to scan through and delete the old rows.
If you use a number, something like this should work:
DELETE FROM myTable WHERE someField < (SELECT MAX(someField) - 1000 FROM myTable)
For a date, deleting everything older than one day would be something like:
DELETE FROM myTable WHERE someField < DateAdd('d', -1, getdate())
Do it by date not number. Have a look at your stats, see how many days 1000 is / will be. Delete anything older than that. Auditing is never particularly efficient, but if you have loads of data that doesn't help you that's very inefficient....
If I understand your needs, this should work. I've tested it on SQL 2008R2, but I can not see any reason why it would not work on SQL Server 2005.
Use a logon trigger to insert a row into your audit table.
Create an AFTER INSERT trigger on your audit table that deletes the row with MIN(ID).
Here's some code to play with:
/* Create audit table */
CREATE TABLE ServerLogonHistory
(SystemUser VARCHAR(512),
ID BIGINT,
DBUser VARCHAR(512),
SPID INT,
LogonTime DATETIME)
GO
/* Create Logon Trigger */
CREATE TRIGGER Tr_ServerLogon
ON ALL SERVER FOR LOGON
AS
BEGIN
INSERT INTO TestDB.dbo.ServerLogonHistory
SELECT SYSTEM_USER, MAX(ID)+1 , USER,##SPID,GETDATE()
FROM TestDB.dbo.ServerLogonHistory;
END
GO
/* Create the cleanup trigger */
CREATE TRIGGER AfterLogin
ON TestDB.dbo.ServerLogonHistory
AFTER INSERT
AS
DELETE
FROM TestDB.dbo.ServerLogonHistory
WHERE ID =
(SELECT MIN(ID) FROM TestDB.dbo.ServerLogonHistory);
GO;
A word of warning. If you create an invalid logon trigger, you'll not be able to logon to the database. But don't panic! It's all part of learning. You'll be able to use 'sqlcmd' to drop the bad trigger.
I did try to delete the row with the min ID in the logon trigger, but I was not able to get that to work.
Is the "ID" column a Identity column with step 1?
after you insert one row
delete column where id<IDENTITY_CURRENT(YOUR_TABLE)-1000