Change Tracking Delete vs Insert and Update - sql-server

I am working on a system which logs alle data manipulation actions centrally.
Currently SQL Server CHange Tracking is applied, but there is a problem in the data it tracks.
E.g. if I Insert a row in TableX, Update the same row and then Delete it, it seems like only the Delete action is being recorded. Does anyone know how to view the previous actions?
DECLARE
#synchronization_version bigint,
#last_synchronization_version bigint;;
-- Obtain the current synchronization version. This will be used next time that changes are obtained.
SET #synchronization_version = CHANGE_TRACKING_CURRENT_VERSION();
-- Obtain initial data set.
SELECT T.*
FROM TableX AS T;
SELECT DTC.commit_time, CT.*, T.*
FROM TableX AS T
RIGHT OUTER JOIN CHANGETABLE(CHANGES TableX, #last_synchronization_version) AS CT ON T.fldId = CT.fldId
JOIN sys.dm_tran_commit_table DTC ON CT.sys_change_version = DTC.commit_ts;

As pointed out by #AlwaysLearning the solution was to use Change Data Capture (CDC) (Microsoft Docs) instead of Change Tracking (CT).

Related

Update a Column When Inserting a Record in SQL Server

I have a table Conservation_Dev with these columns (amongst 30 other):
I3_IDENTITY - a BIGINT and a unique key
STATE - a 2-letter US state abbreviation
ZONE - when I want to store the time zone for this record
I also have a table TimeZoneCodes that maps US states to time zones (forget the fact that some states are in more than one time zone):
state_code - the 2-letter abbreviation for the state
time_zone - the text with the time zone (EST, CST, etc)
I have data being loaded into Conservation_Dev without the time zone data and that is something that I can't control. I want to create an after insert trigger that updates the record. Doing some research on previous threads I came up with the following:
CREATE TRIGGER [dbo].[PopulateTimeZoneBasedOnUSState]
ON [dbo].[Conservation_Dev]
AFTER INSERT
AS
BEGIN
UPDATE [dbo].[Conservation_Dev]
SET [ZONE] = (SELECT [time_zone]
FROM [dbo].[TimeZoneCodes] Z
WHERE Z.[state_code] = i.[STATE])
FROM Inserted i
WHERE [I3_IDENTITY] = i.[I3_IDENTITY]
END
I get an error, though:
Ambiguous column name 'I3_IDENTITY'
Also, is this the right way to do this? Will this be a problem if the data load is, say, 5 or 10 thousand records at a time through an SSIS import package?
Try:
CREATE TRIGGER [dbo].[PopulateTimeZoneBasedOnUSState]
ON [dbo].[Conservation_Dev]
AFTER INSERT
AS
BEGIN
UPDATE A
SET A.[ZONE] = Z.[time_zone]
FROM [dbo].[Conservation_Dev] as A
INNER JOIN Inserted as i
ON A.[I3_IDENTITY] = i.[I3_IDENTITY]
INNER JOIN [dbo].[TimeZoneCodes] as Z
ON Z.[state_code] = i.[STATE]
END
My vote would be to move this update into the SSIS package so that the insert of data is all in one place. I may then move to a stored procedure that runs after the data is loaded. A trigger is going to happen on every insert. I think table based queries/updates would have a better performance. Triggers can be hard to find from a troubleshooting standpoint, but if they are well documented then it may not be much of an issue. The trigger will allow the Zone to be populated once a record is inserted.
The problem with the trigger you are creating is because SQL Server doesn't know which I3_IDENTITY you are referring to in the first part of the WHERE clause. This should fix it:
CREATE TRIGGER [dbo].[PopulateTimeZoneBasedOnUSState]
ON [dbo].[Conservation_Dev]
AFTER INSERT
AS
BEGIN
UPDATE [dbo].[Conservation_Dev]
SET [ZONE] = (SELECT TOP 1 [time_zone] FROM [dbo].[TimeZoneCodes] Z WHERE Z.[state_code] = i.[STATE])
FROM Inserted i
WHERE [dbo].[Conservation_Dev].[I3_IDENTITY] = i.[I3_IDENTITY]
END
This would be a table level update that will update all time_zones in one sweep. I would use something like this in a stored procedure after the initial inserts were complete.
CREATE PROCEDURE dbo.UpdateAllZones
AS
Update dbo.Conservation_Dev
SET Zone = Time_Zone
From dbo.Conservation_Dev c INNER JOIN dbo.TimeZoneCodes z ON c.state_code = z.state_code
Go
Executed as
EXEC dbo.UpdateAllZones

How to check in UPDATE Trigger if record has really changed

I have a SQL Server 2012 UPDATE Trigger which occurs when data is updated within a table in the database (Microsoft Project Server Database) :)
I am using the following statement to get the updated element from the table within the Trigger
SELECT #dataId = INSERTED.Id FROM INSERTED;
But the problem is due to the behaviour of the Microsoft Project Server, that it updates ALL Tasks within a Project even if only one task was changed (each Task is one record in database).
I know that i can get the id of the item which should be updated, but i dont know how i could compare the to updated row with the data which is still in the database within the UPDATE Trigger?
To get what rows actually changed, then use this inside the trigger
SELECT
*
FROM
INSERTED I
JOIN
DELETED D ON I.KeyCol = D.KeyCol
WHERE
NOT EXISTS (
SELECT I.Col1, I.Col2, I.Col3, I.Col4 ...
INTERSECT
SELECT D.Col1, D.Col2, D.Col3, D.Col4 ...
)
The NOT EXISTS..INTERSECT deals with NULLs etc for you already

Verifying Syntax of a Massive SQL Update Command

I'm new to SQL Server and am doing some cleanup of our transaction database. However, to accomplish the last step, I need to update a column in one table of one database with the value from another column in another table from another database.
I found a SQL update code snippet and re-wrote it for our own needs but would love someone to give it a once over before I hit the execute button since the update will literally affect hundreds of thousands of entries.
So here are the two databases:
Database 1: Movement
Table 1: ItemMovement
Column 1: LongDescription (datatype: text / up to 40 char)
Database 2: Item
Table 2: ItemRecord
Column 2: Description (datatype: text / up to 20 char)
Goal: set Column1 from db1 to the value of Colum2 from db2.
Here is the code snippet:
update table1
set table1.longdescription = table2.description
from movement..itemmovement as table1
inner join item..itemrecord as table2 on table1.itemcode = table2.itemcode
where table1.longdescription <> table2.description
I added the last "where" line to prevent SQL from updating the column where it already matches the source table.
This should execute faster and just update the columns that have garbage. But as it stands, does this look like it will run? And lastly, is it a straightforward process, using SQL Server 2005 Express to just backup the entire Movement db before I execute? And if it messes up, just restore it?
Alternatively, is it even necessary to re-cast the tables as table1 and table 2? Is it valid to execute a SQL query like this:
update movement..itemmovement
set itemmovement.longdescription = itemrecord.description
from movement..itemmovement
inner join item..itemrecord on itemmovement.itemcode = itemrecord.itemcode
where itemmovement.longdescription <> itemrecord.description
Many thanks in advance!
You don't necessarily need to alias your tables but I recommend you do for faster typing and reduce the chances of making a typo.
update m
set m.longdescription = i.description
from movement..itemmovement as m
inner join item..itemrecord as i on m.itemcode = i.itemcode
where m.longdescription <> i.description
In the above query I have shortened the alias using m for itemmovement and i for itemrecord.
When a large number of records are to be updated and there's question whether it would succeed or not, always make a copy in a test database (residing on a test server) and try it out over there. In this case, one of the safest bet would be to create a new field first and call it longdescription_text. You can make it with SQL Server Management Studio Express (SSMS) or using the command below:
use movement;
alter table itemmovement add column longdescription_test varchar(100);
The syntax here says alter table itemmovement and add a new column called longdescription_test with datatype of varchar(100). If you create a new column using SSMS, in the background, SSMS will run the same alter table statement to create a new column.
You can then execute
update m
set m.longdescription_test = i.description
from movement..itemmovement as m
inner join item..itemrecord as i on m.itemcode = i.itemcode
where m.longdescription <> i.description
Check data in longdescription_test randomly. You can actually do a spot check faster by running:
select * from movement..itemmovement
where longdescription <> longdescription_test
and longdescription_test is not null
If information in longdescription_test looks good, you can change your update statement to set m.longdescription = i.description and run the query again.
It is easier to just create a copy of your itemmovement table before you do the update. To make a copy, you can just do:
use movement;
select * into itemmovement_backup from itemmovement;
If update does not succeed as desired, you can truncate itemmovement and copy data back from itemmovement_backup.
Zedfoxus provided a GREAT explanation on this and I appreciate it. It is excellent reference for next time around. After reading over some syntax examples, I was confident enough in being able to run the second SQL update query that I have in my OP. Luckily, the data here is not necessarily "live" so at low risk to damage anything, even during operating hours. Given the nature of the data, the updated executed perfectly, updating all 345,000 entries!

How to run a report server subscription through sql server trigger without potential "bottlenecking" the subscription table?

We are using SSRS reports to view data. We have subscriptions associated with the reports that are based on a SQL
trigger. This trigger updates the subscriptions table and dynamically changes the parameters of who the
email is being sent to. This works fine however, if the trigger executes twice within 4 seconds of one
one another then the last email recipient receives both the first subscribed report and the second.
The question is, how do two different email recipients obtain their own subscribed report
if the trigger was executed twice within 4 seconds?
anything after 4 seconds, the first recipient receives his email and the second recipient receives his email and everything is fine.
There seems to be a bottle neck where multiple users are potentially updating one row in the subscription table which can lead to potential problems.
Is there a way to create a subscription using a sql script and pass parameters to the Store Procedures that Report
Server is using to create a new or scheduled subscription?
My code is pasted below that shows once a value in a the table(EmployeeTimeSheets) is updated, a trigger executes bringing the inserted
ID (employee) to the parameters section of the subscription .
Is there a way to dynamically build report server subscriptions within sql server?
any help is most appreciated.
declare #WkEnd nvarchar(30);
declare #Employee nvarchar(50);
declare #UserName nvarchar(30);
declare #TimeStamp datetime;
declare #Title nvarchar(30);
declare #CompleteTS nvarchar(15);
/* If no email is sent to the email afer testing this trigger then that means its past 2pm on Monday.*/
IF (UPDATE(SelfReport))
BEGIN
set #WkEnd=(Select distinct i.[WkEnd] FROM inserted i INNER JOIN deleted AS d ON i.EmpId = d.EmpId)
set #Employee=(Select distinct e.[Employee] FROM inserted i inner join deleted d on i.empid=d.empid inner join employees e on i.empid=e.rowid)
set #UserName=(Select distinct e.[UserName] FROM inserted i inner join deleted d on i.empid=d.empid inner join employees e on i.empid=e.rowid)
set #Title=(Select distinct e.[Title] FROM inserted i inner join deleted d on i.empid=d.empid inner join employees e on i.empid=e.rowid)
update ReportServer.dbo.Subscriptions
set [Parameters]='<ParameterValues>
<ParameterValue><Name>WeekEnding</Name><Value>'+#WkEnd+'</Value></ParameterValue>
<ParameterValue><Name>SelecDepartment</Name><Value>'+#Title+'</Value></ParameterValue>
<ParameterValue><Name>SelectEmployee</Name><Value>'+#Employee+'</Value></ParameterValue></ParameterValues>',
[ExtensionSettings]= '<ParameterValues>
<ParameterValue><Name>TO</Name><Value>'+#UserName+'#processsolutions.com</Value></ParameterValue>
<ParameterValue><Name>IncludeReport</Name><Value>True</Value></ParameterValue>
<ParameterValue><Name>RenderFormat</Name><Value>PDF</Value></ParameterValue>
<ParameterValue><Name>Subject</Name><Value>Completed TimeSheet From '+#Employee+'</Value></ParameterValue>
<ParameterValue><Name>IncludeLink</Name><Value>False</Value></ParameterValue>
<ParameterValue><Name>Priority</Name><Value>NORMAL</Value></ParameterValue>
<ParameterValue><Name>Comment</Name><Value>TimeSheet Completion: ' +#Employee+ ' ' +#WkEnd+ '</Value></ParameterValue>
</ParameterValues>'
where SubscriptionID='530f3da4-a6b5-4594-aa6b-3bfd638600d3'
EXEC ReportServer.dbo.AddEvent #EventType='TimedSubscription',
#EventData='530f3da4-a6b5-4594-aa6b-3bfd638600d3'
END
It may seem kind of round-a-bout, but it will avoid bottlenecks and concurrency issues:
Don't let your trigger update the subscription table directly.
Instead, have the trigger INSERT into a queue table, with whatever information is needed to update the subscription table.
Have a job that runs however often you need, that updates the subscription table from the records in the queue, and then marks those records as done, so they don't get done again and again.
This avoids concurrency and bottlenecks because a job can't start if it is currently running, so you can't have two instances of the job trying to update the subscription table at the same time, the way you can with a trigger.
Of course, another solution is to train the people who are creating subscriptions to do it correctly without the need for a trigger to help them fill in parameter values. That's what we do here. : )

Tracking User activity log for SQL Server database

I have a database with multiple tables and I want to log the users activity via my MVC 3 web application.
User X updated category HELLO. Name changed from 'HELLO' to 'Hi There' on 24/04/2011
User Y deleted vehicle Test on 24/04/2011.
User Z updated vehicle Bla. Name changed from 'Blu' to 'Bla' on 24/04/2011.
User Z updated vehicle Bla. Wheels changed from 'WheelsX' to 'WheelsY' on 24/04/2011.
User Z updated vehicle Bla. BuildProgress changed from '20' to '50' on 24/04/2011
My initial idea is to have on all of my actions that have database crud, to add a couple lines of code that would enter those strings in a table.
Is there a better way of checking which table and column has been modified than to check every column one by one with if statements (first I select the current values, then check each of them with the value of the textbox) I did that for another ASPX web app and it was painful.
Now that I'm using MVC and ADO.NET Entity Data Model I'm wondering if a faster way to find the columns that were changed and build a log like the one above.
You can also accomplish this by putting your database into full recovery mode and then reading the transaction log.
When database is in a full recovery mode then sql server logs all Update, insert and delete (and others such as create, alter, drop..) statements into it's transaction log.
So, using this approach you dont need to make any additinal changes to your application or your database structure.
But you will need 3rd party sql transaction log reader. Red gate has a free solution for sql server 2000 only. If your server is 2005 or higher you would probably want to go with ApexSQL Log
Also, this approach will not be able to audit select statements but it's definately the easiest to implement if you dont really need to audit select queries.
The way I see, you have two options:
Create triggers in the database side, mapping changes in a table by table basis and getting result into a Log table
OR
Having the code handle the changes. You would have a base class with data and with reflection you could iterate all object properties and see what has changed. And then save that into your Log table. Of course, that coding would be on your Data Access Layer.
By the way, if you have a good code structure/architecture, I would go with the second option.
You could have a trigger (AFTER insert/update/deelte) on each table you want to monitor. The beauty is columns_updated() which returns a barbinary value, indicating which columns have been updated.
Here is some snippet of code that I put in each trigger:
IF (##ROWCOUNT = 0) return
declare #AuditType_ID int ,
#AuditDate datetime ,
#AuditUserName varchar(128),
#AuditBitMask varbinary(10)
select #AuditDate = getdate() ,
#AuditUserNAme = system_user,
#AuditBitMask = columns_updated()
-- Determine modification type
IF (exists (select 1 from inserted) and exists (select 1 from deleted))
select #AuditType_ID = 2 -- UPDATE
ELSE IF (exists (select * from inserted))
select #AuditType_ID = 1 -- INSERT
ELSE
select #AuditType_ID = 3 -- DELETE
(record this data to your table of choice)
I have a special function that can decode the bitmask values, but for some reason it is not pasting well here. Message me and I'll email it to you.

Resources