How to cope with IDENTITY_INSERT for an INSTEAD OF trigger on a table with an IDENTITY column? - sql-server

I have a table with an IDENTITY column, and I have an INSTEAD OF trigger on this table. If IDENTITY_INSERT got turned on, I would want to insert the value being manually specified for the IDENTITY column.
Is there any way to properly cope with this scenario, such as detecting the value of IDENTITY_INSERT? From reading, it looks like detecting the current value of IDENTITY_INSERT for a specific table has been impossible in the past. I don't know if it's possible with newer versions of SQL Server.
Table creation SQL:
CREATE TABLE [TestTable]
(
[Id] INTEGER IDENTITY NOT NULL PRIMARY KEY,
[ExampleField] BIT NOT NULL DEFAULT(1)
)
This is what I've currently tried, but it seems rather wasteful as IDENTITY_INSERT is likely to be off for most of the time, meaning that it's always going to be failing on the first insert attempt, which seems wasteful, performance-wise.
CREATE TRIGGER [dbo].[trTestTable_ioi] ON [dbo].[TestTable] INSTEAD OF INSERT
AS
BEGIN
BEGIN TRY
INSERT INTO [TestTable]([Id],[ExampleField])
SELECT [Id], [ExampleField]
FROM [inserted]
END TRY
BEGIN CATCH
INSERT INTO [TestTable]([ExampleField])
SELECT [ExampleField]
FROM [inserted]
END CATCH
END

If your IDENTITY seed and increment is such that the generated value will always be non-zero (positive seed and increment or negative seed and increment), you can check for non-zero values in the virtual inserted table and use that value when present. This relies on my observation that the identity value is zero with an INSTEAD OF trigger and IDENTITY_INSERT OFF. However, I could not find this behavior specifically documented so you should vet in your environment and use at your own risk.
Example:
CREATE TRIGGER [dbo].[trTestTable_ioi] ON [dbo].[TestTable] INSTEAD OF INSERT
AS
SET NOCOUNT ON;
IF EXISTS(SELECT * FROM inserted WHERE ID <> 0)
BEGIN
--IDENTITY_INSERT is ON
INSERT INTO [TestTable]([Id],[ExampleField])
SELECT [Id], [ExampleField]
FROM [inserted];
END
ELSE
BEGIN
--IDENTITY_INSERT is OFF
INSERT INTO [TestTable]([ExampleField])
SELECT [ExampleField]
FROM [inserted];
END;
GO
SET IDENTITY_INSERT dbo.TestTable OFF;
GO
--identity value auto-assigned
INSERT INTO TestTable VALUES(1);
GO
SET IDENTITY_INSERT dbo.TestTable ON;
GO
--explict identity value specified
INSERT INTO TestTable(ID, ExampleField) VALUES(11, 1);
GO
SET IDENTITY_INSERT dbo.TestTable OFF;
GO
--fails as expected because identity value cannot be specified with IDENTITY_INSERT OFF
INSERT INTO TestTable(ID, ExampleField) VALUES(11, 1);
GO
SET IDENTITY_INSERT dbo.TestTable ON;
GO
--fails as expected because identity value must be specified with IDENTITY_INSERT ON
INSERT INTO TestTable VALUES(1);
GO

The documentation for set identity_insert states:
At any time, only one table in a session can have the IDENTITY_INSERT
property set to ON. If a table already has this property set to ON,
and a SET IDENTITY_INSERT ON statement is issued for another table,
SQL Server returns an error message that states SET IDENTITY_INSERT is
already ON and reports the table it is set ON for.
A terrible idea is to create a temporary table and try setting identity_insert on for it. If there is an error, catch the message and extract the table name.
NB: Any time you are catching and parsing an error message you are fiddling with some fragile code. You should run, not walk, away now.
create table #Placeholder ( Id Int Identity );
begin try
set identity_insert #Placeholder on;
end try
begin catch
-- Sample error: IDENTITY_INSERT is already ON for table 'Scratch.dbo.Foo'. Cannot perform SET operation for table '#Placeholder'.
declare #Message as NVarChar(2048) = Error_Message();
print #Message;
declare #Prefix as NVarChar(2048) = 'IDENTITY_INSERT is already ON for table ''';
declare #Suffix as NVarChar(2048) = '''. Cannot perform SET operation for table ''#Placeholder''.';
declare #TableName as NVarChar(2048) = NULL;
if ( Left( #Message, Len( #Prefix ) ) = #Prefix and Right( #Message, Len( #Suffix ) ) = #Suffix )
set #TableName = Substring( #Message, Len( #Prefix ) + 1, Len( #Message ) - Len( #Prefix ) - Len( #Suffix ) );
else
print 'Unexpected error!';
print #TableName;
end catch
drop table #Placeholder;
Then there are the performance implications of creating/dropping a temporary table in a trigger.

Related

Inserting to table having issue - Explicit value must be specified for identity column in table

I'm getting ready to release a stored procedure that gets info from other tables, does a pre-check, then inserts the good data into a (new) table. I'm not used to working with keys and new tables as much, and my insert into this new table I'm creating is having this error message having to do with the insert/key:
Msg 545, Level 16, State 1, Line 131
Explicit value must be specified for identity column in table 'T_1321_PNAnnotationCommitReport' either when IDENTITY_INSERT is set to ON or when a replication user is inserting into a NOT FOR REPLICATION identity column.
BEGIN
...
BEGIN
IF NOT EXISTS (SELECT * FROM sys.tables where name = N'T_1321_PNAnnotationCommitReport')
BEGIN
CREATE TABLE T_1321_PNAnnotationCommitReport (
[id] [INT] IDENTITY(1,1) PRIMARY KEY NOT NULL, --key
[progressnote_id] [INT] NOT NULL,
[form_id] [INT] NOT NULL,
[question_id] [INT],
[question_value] [VARCHAR](max),
[associatedconcept_id] [INT],
[crte_date] [DATETIME] DEFAULT CURRENT_TIMESTAMP,
[create_date] [DATETIME] --SCHED_RPT_DATE
);
print 'test';
END
END --if not exists main table
SET IDENTITY_INSERT T_1321_PNAnnotationCommitReport ON;
...
INSERT INTO dbo.T_1321_PNAnnotationCommitReport--(progressnote_id,form_id,question_id,question_value,associatedconcept_id,crte_date, create_date) **I tried with and without this commented out part and it's the same.
SELECT progressnote_id,
a.form_id,
question_id,
questionvalue,
fq.concept_id,
getdate(),
a.create_date
FROM (
SELECT form_id,
progressnote_id,
R.Q.value('#id', 'varchar(max)') AS questionid,
R.Q.value('#value', 'varchar(max)') AS questionvalue,
create_date
FROM
#tableNotes t
OUTER APPLY t.form_questions.nodes('/RESULT/QUESTIONS/QUESTION') AS R(Q)
WHERE ISNUMERIC(R.Q.value('#id', 'varchar(max)')) <> 0
) a
INNER JOIN [CKOLTP_DEV]..FORM_QUESTION fq ON
fq.form_id = a.form_id AND
fq.question_id = a.questionid
--select * from T_1321_PNAnnotationCommitReport
SET IDENTITY_INSERT T_1321_PNAnnotationCommitReport OFF;
END
Any ideas?
I looked at some comparable inserts we do at work, insert into select and error message, and insert key auto-incremented, and I think I'm doing what they do. Does anyone else see my mistake? Thanks a lot.
To repeat my comment under the question:
The error is literally telling you the problem. You turn change the IDENTITY_INSERT property to ON for the table T_1321_PNAnnotationCommitReport and then omit the column id in your INSERT. If you have enabled IDENTITY_INSERT you need to supply a value to that IDENTITY, just like the error says.
We can easily replicate this problem with the following batches:
CREATE TABLE dbo.MyTable (ID int IDENTITY(1,1),
SomeValue varchar(20));
GO
SET IDENTITY_INSERT dbo.MyTable ON;
--fails
INSERT INTO dbo.MyTable (SomeValue)
VALUES('abc');
GO
If you want the IDENTITY value to be autogenerated, then leave IDENTITY_INSERT set to OFF and omit the column from the INSERT (like above):
SET IDENTITY_INSERT dbo.MyTable OFF; --Shouldn't be needed normally, but we manually changed it before
--works, as IDENTITY_INSERT IS OFF
INSERT INTO dbo.MyTable (SomeValue)
VALUES('abc');
If you do specifically want to define the value for the IDENTITY, then you need to both set IDENTITY_INSERT to ON and provide a value in the INSERT statement:
SET IDENTITY_INSERT dbo.MyTable ON;
--works
INSERT INTO dbo.MyTable (ID,SomeValue)
VALUES(10,'def');
GO
SELECT *
FROM dbo.MyTable;
IDENTITY_INSERT doesn't mean "Get the RDBMS to 'insert' the value" it means that you want to want to tell the RDBMS what value to INSERT. This is covered in the opening sentence of the documentation SET IDENTITY_INSERT (Transact-SQL):
Allows explicit values to be inserted into the identity column of a table.
(Emphasis mine)

T-SQL stored procedure did not do anything

I am new to SQL stored procedures and am having difficulty getting one I grandfathered [from am existing SP that does work, written by another author] to execute properly. The following is intended to replace existing records in the 'Data' table with updated ones, and to append new records (both from the 'UpdateTemp' table, when updated).
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[ProcessNewData]
AS
DECLARE #MaxDate varchar(10);
DECLARE #MinDate varchar(10);
SELECT #MaxDate = Max([Date]) FROM [dbo].[UpdateTemp];
SELECT #MinDate = Min([Date]) FROM [dbo].[UpdateTemp];
--Remove months from existing data that have updated data
DELETE
FROM [Data]
WHERE
([Type] = 'A' AND
([Date] = #MaxDate OR [Date] = #MinDate))
OR
([Type] = 'B' AND
([Date] = #MaxDate OR [Date] = #MinDate))
--Insert new data
EXEC('INSERT INTO [Data] SELECT * FROM [UpdateTemp];')
--Delete UpdateTemp table
EXEC('DROP TABLE [dbo].[UpdateTemp];')
There is another very similar SP written by someone else that did execute successfully. Which is below:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[process_data]
AS
DECLARE #max_month varchar(10);
DECLARE #min_month varchar(10);
DECLARE #newTableName varchar(21);
SELECT #max_month = Max([month]) FROM [dbo].[Update_Temp];
SELECT #min_month = Min([month]) FROM [dbo].[Update_Temp];
SET #newTableName = 'Update_' + #max_month
--Drop table if exists
EXEC('DROP TABLE IF EXISTS [dbo].['+#newTableName+'];')
--Rename the database
EXEC sp_rename 'Update_Temp', #newTableName
--Remove 2 month from outcome
DELETE
FROM [Outcome]
WHERE
[month] = #max_month OR [month] = #min_month
--Insert new data
EXEC('INSERT INTO [Outcome] SELECT * FROM ['+#newTableName+'];')
The 'UpdateTemp' table was loaded successfully. I've tested the code by itself and it works. However it appears the SP did not run at all, or didn't do anything when it did.
Does anyone know what the issue may be?
You can run the commend without the EXEC( ).
Just do the insert and drop.
The exec was for the dynamic code, where the table name is a variable. A different use case.
I recommend you use CREATE OR ALTER PROCEDURE.
So when you implement to prod you have the correct code.
For the same reason DROP TABLE IF EXISTS.
Start using NOCOUNT ON.
Finish up with GO.
I love to test the proc after it is created - it enters the proc's code if no GO is in there.
There are no parameters - just run the code as is.
If it works out of the SP it will inside it.
When the SP receives parameters, you replace the PROCEDURE statement with a DECLARE and do the same.
CREATE OR ALTER PROCEDURE [dbo].[ProcessNewData]
AS
BEGIN
SET NOCOUNT ON;
DECLARE #MaxDate varchar(10),
#MinDate varchar(10);
SELECT #MaxDate = Max([Date]), #MinDate = Min([Date])
FROM [dbo].[UpdateTemp];
--Remove months from existing data that have updated data
DELETE
FROM [Data]
WHERE
([Type] = 'A' OR [Type] = 'B') AND
([Date] = #MaxDate OR [Date] = #MinDate))
--Insert new data
INSERT INTO [Data]
SELECT * FROM [UpdateTemp];
--Delete UpdateTemp table
DROP TABLE IF EXISTS [dbo].[UpdateTemp];
END;
GO

Best way to do batch insert ignore and return id in SQL Server?

I need to deduplicate various data when I am importing them from different sources (Json files, other databases and REST APIs), first I load them into a single table, which defines the type for them and store the data as Json, so later when I run the batch processing, I can look up the type and insert the data to the suitable tables. The number of imported rows are different (each type goes to different table/tables), but always more than 1 million (all together ~10G of data if I place them in Json format in a single table using VARCHAR(MAX)).
As I mentioned, I need to handle duplicates, so I try to define unique indexes for the target tables and enable Ignore Duplicate Keys, which 'only' raises a warning when I insert existing data. The problem is, this only works in few cases. Most of the time, I need to work with 5+ varchar(255) fields, and I cannot add them to a unique index, because of the limit (900 byte, src).
Another thing I am struggling with, is during batch inserting, I need to insert relational data , meaning one table will have foreign keys to another. So first I need to handle the dependencies, and after I got their inserted Ids, using those I can insert the data. Like a product has a manufacturer, so first I insert all the manufacturer names in the current batch, then using those Ids I can insert the products.
The need for returning Ids and doing deduplication results in a query I would like to achieve:
Will run concurrently, by 8-16 threads
Should return the inserted Id
Should only insert data If it is not inserted by another thread before (or not inserted before at all)
First, I tried to handle this, by making stored procedures like this:
Try to select the data, If found, return the Id
If not found, start a transaction
Check again, if it already got inserted by another thread.
If not, insert and return the new Id.
Code example for this.:
CREATE PROCEDURE [dbo].usp_insert_pdproductdetails
#GDDataSourceVersionId INT,
#ManufacturerNameId BIGINT,
#ManufacturerReference NVARCHAR(255),
#PropertiesJson NVARCHAR(MAX),
#OriginalContentPage NVARCHAR(MAX),
#NewId BIGINT OUT
AS
BEGIN
SET NOCOUNT ON;
SELECT #NewId = [Id] FROM PDProductDetails
WHERE GDDataSourceVersionId = #GDDataSourceVersionId AND
ManufacturerId = #ManufacturerNameId AND
ManufacturerReference = #ManufacturerReference;
IF #NewId IS NULL
BEGIN
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
SELECT #NewId = [Id] FROM PDProductDetails
WHERE GDDataSourceVersionId = #GDDataSourceVersionId AND
ManufacturerId = #ManufacturerNameId AND
ManufacturerReference = #ManufacturerReference;
IF #NewId IS NULL
BEGIN
INSERT INTO PDProductDetails (GDDataSourceVersionId, ManufacturerId, ManufacturerReference, PropertiesJson, OriginalContentPage)
VALUES(#GDDataSourceVersionId, #ManufacturerNameId, #ManufacturerReference, #PropertiesJson, #OriginalContentPage);
SELECT #NewId = SCOPE_IDENTITY();
END
COMMIT TRANSACTION
END
SELECT #NewId;
END
GO
Multiple threads would call this and insert the Product details. However, using this I got deadlocked really fast. I changed to a different approach, using Merge:
CREATE PROCEDURE [dbo].usp_insert_pdproductdetails
#GDDataSourceVersionId INT,
#ManufacturerNameId BIGINT,
#ManufacturerReference NVARCHAR(255),
#PropertiesJson NVARCHAR(MAX),
#OriginalContentPage NVARCHAR(MAX),
#NewId BIGINT OUT
AS
BEGIN
SET NOCOUNT ON;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
MERGE
INTO [dbo].[PDProductDetails] T
USING (SELECT #GDDataSourceVersionId, #ManufacturerNameId, #ManufacturerReference, #PropertiesJson, #OriginalContentPage)
AS Source (GDDataSourceVersionId, ManufacturerNameId, ManufacturerReference, PropertiesJson, OriginalContentPage)
ON T.GDDataSourceVersionId = Source.GDDataSourceVersionId AND
T.ManufacturerId = Source.ManufacturerNameId AND
T.ManufacturerReference = Source.ManufacturerReference
WHEN NOT MATCHED THEN
INSERT (GDDataSourceVersionId, ManufacturerId, ManufacturerReference, PropertiesJson, OriginalContentPage)
VALUES(Source.GDDataSourceVersionId, Source.ManufacturerNameId,
Source.ManufacturerReference, Source.PropertiesJson, Source.OriginalContentPage);
COMMIT TRANSACTION;
SELECT #NewId = [Id] FROM PDProductDetails (NOLOCK)
WHERE GDDataSourceVersionId = #GDDataSourceVersionId AND
ManufacturerId = #ManufacturerNameId AND
ManufacturerReference = #ManufacturerReference;
SELECT #NewId;
END
GO
This always merges the row and selects later. It still deadlocks tough, not as fast as the other one, but still.
How can I achieve an insert ignore and return inserted id functionality, which won't deadlock in concurrent environment?
After #ta.speot.is mentioned you could do OUTPUT with merge, I searched for how to assign it to a variable and an answer mentioned it.
I used this stored procedure.:
CREATE PROCEDURE [dbo].usp_insert_pdproductdetails
#GDDataSourceVersionId INT,
#ManufacturerNameId BIGINT,
#ManufacturerReference NVARCHAR(255),
#PropertiesJson NVARCHAR(MAX),
#OriginalContentPage NVARCHAR(MAX),
#NewId BIGINT OUT
AS
BEGIN
SET NOCOUNT ON;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
MERGE
INTO [dbo].[PDProductDetails] T
USING (SELECT #GDDataSourceVersionId, #ManufacturerNameId, #ManufacturerReference, #PropertiesJson, #OriginalContentPage)
AS Source (GDDataSourceVersionId, ManufacturerNameId, ManufacturerReference, PropertiesJson, OriginalContentPage)
ON T.GDDataSourceVersionId = Source.GDDataSourceVersionId AND
T.ManufacturerId = Source.ManufacturerNameId AND
T.ManufacturerReference = Source.ManufacturerReference
WHEN MATCHED THEN
UPDATE SET #NewId = T.Id
WHEN NOT MATCHED THEN
INSERT (GDDataSourceVersionId, ManufacturerId, ManufacturerReference, PropertiesJson, OriginalContentPage)
VALUES(Source.GDDataSourceVersionId, Source.ManufacturerNameId,
Source.ManufacturerReference, Source.PropertiesJson, Source.OriginalContentPage);
SET #NewId = ISNULL(#NewId, SCOPE_IDENTITY());
COMMIT TRANSACTION;
SELECT #NewId;
END
GO
Edit.: As #ta.speot.is mentioned, it would have been better, to make batch request using table-valued parameters, using the same approach (MERGE would use the table input as Source).

Null values INSERTED in trigger

I want to copy content of one table to another table in the same database.
For this I wrote trigger on source table which triggered on AFTER INSERT UPDATE, there are 2 uniqueidentifier fields in the table which generates values based on newid() as default binding. Based on this uniqueidentifier I am checking whether the record is present on the destination table or not if present then it will update and if not present then insert dataset into the table.
Problem is when i insert a new record the INSERTED in trigger give me NULL values for the uniqueidentifier fields.
In may case only one row is either update or insert so cursor is not used.
Below is my code, I am getting null values in #OriginalTable_MoveDataUID and #OriginalTable_ProcedureUID. Both the MoveDataUID and ProcedureUID are uniqueidentifier fileds.
Please share your thoughts or any alternative for this.
ALTER TRIGGER [dbo].[spec_ref_movedata_procedures_ToUpdate]
ON [dbo].[spec_ref_movedata_procedures]
AFTER INSERT, UPDATE
AS
BEGIN
SET XACT_ABORT ON
BEGIN DISTRIBUTED TRANSACTION
DECLARE #OriginalTable_MoveDataUID NVarchar (100)
DECLARE #OriginalTable_ProcedureUID NVarchar (100)
DECLARE #PresentInHistoryYesNo int
SELECT #OriginalTable_MoveDataUID= MoveDataUID,#OriginalTable_ProcedureUID=ProcedureUID FROM INSERTED
-- inserted for checking purpose
INSERT INTO ERP_Test_NK_spec_ref_movedata_procedures_history_2 (MovedataUID,ProcedureUID) VALUES
(#OriginalTable_MoveDataUID,#OriginalTable_ProcedureUID)
SELECT #PresentInHistoryYesNo = count(*) from spec_ref_movedata_procedures_history WHERE MoveDataUID=#OriginalTable_MoveDataUID AND ProcedureUID=#OriginalTable_ProcedureUID
IF #PresentInHistoryYesNo = 0
BEGIN
-- insert opertions
print 'insert record'
END
ELSE IF #PresentInHistoryYesNo = 1
BEGIN
-- update opertions
print 'update record'
END
COMMIT TRANSACTION
SET XACT_ABORT OFF
END
Instead of using variables, you could do this:
INSERT INTO ERP_Test_NK_spec_ref_movedata_procedures_history_2 (MovedataUID,ProcedureUID)
SELECT MoveDataUID,ProcedureUID FROM INSERTED

SQL Server TRIGGER that will INSERT or UPDATE another table

I have a SQL Server table called Prices, which contains tens of thousands of rows of data. This table is used heavily by legacy applications and unfortunately cannot be modified (no columns can be added, removed, or modified).
My requirement is to keep track of when the table is modified (INSERT, UPDATE, or DELETE). However, the Prices table does not have a LastUpdated column, and I am not able to add such column. Additionally, my trigger must be compatible with SQL Server 2005.
I can however create an additional table, PricesHistory which will store the PriceID, UpdateType and LastUpdated columns.
I want to attach a SQL TRIGGER to the Prices table that will either INSERT or UPDATE a row in the PricesHistory table which will keep track of when the prices were last updated and what operation triggered it.
Here is what I have so far, which will detect which operation caused the trigger to fire. However, I'm stumped on how to SELECT from either inserted or deleted tables and do a proper INSERT/UPDATE to the PricesHistory table.
Basically, all operations should check if the PriceID already exists in the PriceHistory table, and UPDATE the UpdateType and LastUpdated columns. If the PriceID does not exist yet, it should INSERT it along with the UpdateType and LastUpdated values.
EDIT: It has been brought to my attention by a co-worker that the inserted and deleted items are rows not tables. Meaning that I could do a simple IF EXISTS ... UPDATE ELSE INSERT INTO clause. Is this true? I was under the impression it would be a table of the rows, not individual rows.
CREATE TRIGGER PricesUpdateTrigger
ON Prices
AFTER INSERT, UPDATE, DELETE
AS
DECLARE #UpdateType nvarchar(1)
DECLARE #UpdatedDT datetime
SELECT #UpdatedDT = CURRENT_TIMESTAMP
IF EXISTS (SELECT * FROM inserted)
IF EXISTS (SELECT * FROM deleted)
SELECT #UpdateType = 'U' -- Update Trigger
ELSE
SELECT #UpdateType = 'I' -- Insert Trigger
ELSE
IF EXISTS (SELECT * FROM deleted)
SELECT #UpdateType = 'D' -- Delete Trigger
ELSE
SELECT #UpdateType = NULL; -- Unknown Operation
IF #UpdateType = 'I'
BEGIN
-- Log an insertion record
END
IF #UpdateType = 'U'
BEGIN
-- Log an update record
END
IF #UpdateType = 'D'
BEGIN
-- Log a deletion record
END
GO
Why not a generic audit table? See my presentation "How to prevent and audit changes?"
http://craftydba.com/?page_id=880
Here is a table to save the data being changed.
--
-- 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
Here is a trigger to capture INS, UPD, DEL statements.
--
-- 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
If you are having a-lot of changes to the table, then either purge data on a cycle or just record the modified date in another table like you stated. However, key information will be lost.
Nice thing about my solution is that it tells you when and who did the change. The actual data is save in XML format that can be restored if need be.
the code i am giving from there you can get the view how to write single trigger for handling database insert & update and as well as how to do audit trail by trigger. hope this help.
CREATE TRIGGER TRG_HourSheet ON EditedHourSheet
FOR INSERT, UPDATE
AS
DECLARE #v_xml XML,
#PKValue INT,
#type CHAR(1),
#v_slno INT
BEGIN
SET NOCOUNT ON
IF EXISTS(SELECT * FROM INSERTED)
BEGIN
IF EXISTS(SELECT * FROM DELETED)
BEGIN
SET #type ='U';
END
ELSE
BEGIN
SET #type ='I';
END
END
IF #type = 'U'
BEGIN
DECLARE DB_CURSOR CURSOR FOR
SELECT ID FROM DELETED ORDER BY ModDate DESC
OPEN DB_CURSOR
FETCH NEXT FROM DB_CURSOR INTO #PKValue
WHILE ##FETCH_STATUS = 0
BEGIN
SET #v_xml =(SELECT * FROM DELETED Where ID=#PKValue
FOR xml AUTO, root('Record'),elements XSINIL)
SELECT #v_slno = IsNull(Max(RowID),0)+1 FROM EditedHourSheetLog
Where HourSheetID=#PKValue
INSERT INTO EditedHourSheetLog(HourSheetID,XMLData,Action,RowID)
values (#PKValue,#v_xml,#type,#v_slno)
FETCH NEXT FROM DB_CURSOR INTO #PKValue
END
CLOSE DB_CURSOR
DEALLOCATE DB_CURSOR
--END
END
ELSE IF #type = 'I'
BEGIN
DECLARE DB_CURSOR CURSOR FOR
SELECT ID FROM INSERTED ORDER BY ModDate DESC
OPEN DB_CURSOR
FETCH NEXT FROM DB_CURSOR INTO #PKValue
WHILE ##FETCH_STATUS = 0
BEGIN
SET #v_xml =(SELECT * FROM INSERTED Where ID=#PKValue
FOR xml AUTO, root('Record'),elements XSINIL)
SELECT #v_slno = IsNull(Max(RowID),0)+1 FROM EditedHourSheetLog
Where HourSheetID=#PKValue
INSERT INTO EditedHourSheetLog(HourSheetID,XMLData,Action,RowID)
values (#PKValue,#v_xml,#type,#v_slno)
FETCH NEXT FROM DB_CURSOR INTO #PKValue
END
CLOSE DB_CURSOR
DEALLOCATE DB_CURSOR
--END
END
END

Resources