Trigger to update timestamp if new field value != old field value - sql

I'm trying to update a field called LastUpdate with a timestamp whenever either one of 2 other fields are changed to a value that's different from the original value. Those two columns are LoadDate and CompleteDate. The trigger I have works, but since I'm updating the row each time I run my process, regardless of whether the date changes, its updating the timestamp. I need to compare to see if the old LoadDate is different from the new LoadDate, or if the old CompleteDate is different from the new CompleteDate. Here's my trigger:
CREATE TRIGGER time_stamp
ON my_table
FOR UPDATE, INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE #LoadDate datetime, #CompleteDate datetime
SELECT #LoadDate = LoadDate, #CompleteDate = CompleteDate FROM my_table
UPDATE my_table
SET LastUpdate = GETDATE()
WHERE LoadDate = #LoadDate
AND CompleteDate = #CompleteDate
END

You should just need one statement in your trigger. Something like this ought to do:
update MT
set LastUpdate = GetDate()
from My_Table as MT inner join
-- Inner join with inserted to pick up only those rows inserted or updated.
inserted as i on i.YourIdColumn = MT.YourIdColumn left outer join
-- Left outer join with deleted to pick up the "before" values if this is an UPDATE.
deleted as d on d.YourIdColumn = MT.YourIdColumn
where
-- LoadDate has changed or this is an INSERT.
( i.LoadDate <> d.LoadDate or d.LoadDate is NULL ) or
-- CompleteDate has changed or this is an INSERT.
( i.CompleteDate <> d.CompleteDate or d.CompleteDate is NULL )
Note that this is performing a set operation. An INSERT or UPDATE that affects multiple rows will cause the trigger to fire once and process all of the affected rows.

Related

Copying the rows within the same table

I need to move what's been appended at the end of my table to its very beginning, however
the same record is being copied into destination.
In other words, between the ids 1 and 3567 I only have the record from the id 3567 repeated until the end. I believe that my outer and even inner sub-query lacks something ?
Thanks for the hint
Query:
UPDATE dbo.TABLE
SET Xwgs = dt.Xwgs, Ywgs = dt.Ywgs
FROM
(
SELECT
Xwgs,
Ywgs
FROM dbo.TABLE
WHERE
Id BETWEEN 3567 AND 7243
) dt
WHERE
Id BETWEEN 1 AND 3566
Is this what you want?
update t
set xwgs = dt.xwgs, ywgs = dt.ywgs
from mytable t
inner join (
select xwgs, ywgs
from mytable
where id between 3567 and 7243
) dt
on t.id = dt.id - 3566
The main difference with your query is that it properly correlates the target table and the derived table.
Note that this does not actually move the rows; all it does is copy the values from the upper bucket to the corresponding value in the lower bucket.
You know that You can always sort Your table with ORDER BY id DESC right?
Sometimes its needed do something strange. I do it like that:
Copy the whole table into a temp table (it may be #temporary table)
Drop or Truncate or Delete records from that table
Insert those records again from my temp table
Drop temp table
But an UPDATE is also a solution.
Tip: You can allow inserting values into identity (autoincreament) id column with SET IDENTITY_INSERT
SELECT *
INTO tmp__MyTable -- this will create a new table
FROM MyTable
ORDER BY id
DELETE FROM dbo.MyTable -- will throw an error on foreign keys conflicts
INSERT INTO MyTable (col,col2) -- column list here
SELECT col,col2
FROM tmp__MyTable
ORDER BY id DESC
-- or something like that:
-- ORDER BY CASE WHEN id <= 3566 THEN -id ELSE id END
-- DROP TABLE tmp__MyTable

Trigger for UPDATE runs many time on batch Updates

All of my tables have a Trigger for CRUD operations.
here is a sample:
ALTER TRIGGER [dbo].[Cities_tr] ON [dbo].[Cities] AFTER INSERT, UPDATE
AS
BEGIN
DECLARE #operation CHAR(6)
SET #operation = CASE WHEN EXISTS (SELECT * FROM inserted) AND EXISTS (SELECT * FROM deleted)
THEN 'Update'
WHEN EXISTS (SELECT * FROM inserted)
THEN 'Insert'
WHEN EXISTS(SELECT * FROM deleted)
THEN 'Delete'
ELSE NULL
END
IF #operation = 'Insert'
INSERT INTO history ([dt],[tname],[cuser] ,[id],op)
SELECT GETDATE(),'Cities', i.ldu, i.CityId,#operation
FROM inserted i
set nocount on
IF #operation = 'Update'
INSERT INTO history ([dt],[tname],[cuser] ,[id],op)
SELECT GETDATE(),'Cities', i.ldu, i.CityId,#operation
FROM deleted d, inserted i
END
If I update one row, everything works fine and trigger inserts one row in history.
For example
update top(1) cities set f=1
But if more than one row updated, updatedrow^2 rows will be inserted.
For example 9 for 3 rows 100 for 10 rows...
What is wrong with my trigger and how could I solve it?
The problem with your code is that you are cross joining inserted and deleted. On a multi-rows update, both contain many rows, which the cartesian product multiplies.
It looks like you actually want to log the "new" rows (either inserted or updated). If so, you don’t want to select from deleted. Also, the conditional logic can be moved within a single query, which allows to simplify your code as follows:
ALTER TRIGGER dbo.Cities_tr
ON dbo.Cities
AFTER INSERT, UPDATE
AS
BEGIN
INSERT INTO history (dt, tname, cuser, id, op)
SELECT
getdate(),
'Cities',
ldu,
cityId,
case when exists (select 1 from deleted) then 'Update' else 'Insert' end
FROM inserted;
END
On the other hand, if you want to log both the "old" and "new" rows (which is not what your code does, even on a single-row update), then you want to union all two queries that select from inserted and deleted.
You are cross joining inserted and deleted. Normally, they would be joined using the table's primary key, which is presumably CityId:
INSERT INTO history ([dt], [tname], [cuser] , [id], op)
SELECT GETDATE(), 'Cities', i.ldu, i.CityId, #operation
FROM deleted d JOIN
inserted i
ON d.CityId = i.CityId;
In this case, deleted is not being used, so it does not even need to be included in the query.
You could implement the entire trigger as a single query in the table using LEFT JOIN:
INSERT INTO history ([dt], [tname], [cuser] , [id], op)
SELECT GETDATE(), 'Cities', i.ldu, i.CityId,
(CASE WHEN d.CityId IS NOT NULL THEN 'Update' ELSE 'Insert' END)
FROM inserted i LEFT JOIN
deleted d
ON d.CityId = i.CityId;

T-SQL UPDATE statement affects less records than select statement

I am trying to update a DateTime column of one table with a date column from another table.
Before updating it, I am getting the records affected in order to see previously what records will be affected in the UPDATE. So I perform a SELECT statement with below WHERE clause:
NOTE:
DateTimeField is of type DateTime
DateField is of type Date
Code:
SELECT tblToUpdate.*
FROM MyTable1 tblToUpdate
INNER JOIN MyTable2 fromTbl on tblToUpdate.Id = fromTbl.Id
WHERE
ISNULL(fromTbl.DateField, GETDATE()) >= DATEFROMPARTS(1753, 1, 1)
AND ((fromTbl.DateField IS NOT NULL AND tblToUpdate.DateTimeField IS NULL)
OR
(fromTbl.DateField IS NULL AND tblToUpdate.DateTimeField IS NOT NULL)
OR
fromTbl.DateField <> CAST(tblToUpdate.DateTimeField AS DATE))
UPDATE tblToUpdate
SET tblToUpdate.DateTimeField = fromTbl.DateField
FROM MyTable1 tblToUpdate
INNER JOIN MyTable2 fromTbl ON tblToUpdate.Id = fromTbl.Id
WHERE
ISNULL(fromTbl.DateField, GETDATE()) >= DATEFROMPARTS(1753, 1, 1)
AND (
(fromTbl.DateField IS NOT NULL AND tblToUpdate.DateTimeField IS NULL)
OR
(fromTbl.DateField IS NULL AND tblToUpdate.DateTimeField IS NOT NULL)
OR
fromTbl.DateField <> CAST(tblToUpdate.DateTimeField AS DATE)
)
Note that I check DateField in Where clause to be in DateTime range before I update it.
The problem is that the number of records returned by the SELECT statement is not the same as the number of records affected returned by UPDATE statement.
The UPDATE statement affects fewer records than the SELECT statement returns.
Why is it happening if from and where clause are the same in both statements?
I think the number of records returned by SELECT and affected by UPDATE statements respectively should be the same.
This is easy to demonstrate. When there are multiple rows meeting the join predicates you will get differing row counts from a select and an update.
create table Header(HeadID int identity, Name varchar(50))
insert Header select 'test'
create table Details(DetailsID int identity, HeadID int, Name varchar(50))
insert Details values(1, 'asdf'),(1,'qwer')
select * --this returns 2 rows
from Header h
join Details d on d.HeadID = h.HeadID
update h --only 1 row affected
set Name = 'what?'
from Header h
join Details d on d.HeadID = h.HeadID

Update Table From Select

I am using ms-sql server. I have table which I want to update from select statement. For example the table which I want to update is Table_A with 2 rows in it. The update statement from which I want to update Table_A return 10 rows. So I want to update Table_A 10 times. The problem is that Table_A is updated 2 times(the count of rows in Table_A).
Example:
CREATE TABLE #tmp
(
AccountID INT,
Inflow DECIMAL(10,2)
)
DECLARE #n INT = 0
WHILE (#n <10 )
BEGIN
INSERT INTO #tmp SELECT 2, 10
SET #n += 1
END
UPDATE dbo.Table_A
SET Balance += sss.Inflow
FROM ( SELECT t.AccountID ,
t.Inflow
FROM #tmp AS t
) AS sss
WHERE dbo.tAccount.AccountID = sss.AccountID;
-- Updates only 2 times
-- What I expected here is Table_A to be updated as many times as the count of the select statement which is 10, based on the insert before.
Your expectation is wrong. Admittedly, the documentation buries this idea:
The example runs without error, but each SalesYTD value is updated
with only one sale, regardless of how many sales actually occurred on
that day. This is because a single UPDATE statement never updates the
same row two times.
The documentation continues with the solution:
In the situation in which more than one sale for a specified
salesperson can occur on the same day, all the sales for each sales
person must be aggregated together within the UPDATE statement, as
shown in the following example:
So, simply aggregate before doing the join:
UPDATE dbo.Table_A
SET Balance += sss.Inflow
FROM (SELECT t.AccountID, SUM(t.Inflow) as Inflow
FROM #tmp t
GROUP BY t.AccountId
) sss
WHERE dbo.tAccount.AccountID = sss.AccountID;
Note you can also write this as:
UPDATE a
SET Balance += sss.Inflow
FROM dbo.Table_A a JOIN
(SELECT t.AccountID, SUM(t.Inflow) as Inflow
FROM #tmp t
GROUP BY t.AccountId
) sss
ON a.AccountID = sss.AccountID;
This makes the JOIN more explicit.

sql server: How to detect changed rows

I want to create a trigger to detect whether a row has been changed in SQL Server. My current approach is to loop through each field, apply COLUMNS_UPDATED() to detect whether UPDATE has been called, then finally compare the values of this field for the same row (identified by PK) in inserted vs deleted.
I want to eliminate the looping from the procedure. Probably I can dump the content of inserted and deleted into one table, group on all columns, and pick up the rows with count=2. Those rows will count as unchanged.
The end goal is to create an audit trail:
1) Track user and timestamp
2) Track insert, delete and REAL changes
Any suggestion is appreciated.
Instead of looping you can use BINARY_CHECKSUM to compare entire rows between the inserted and deleted tables, and then act accordingly.
Example
Create table SomeTable(id int, value varchar(100))
Create table SomeAudit(id int, Oldvalue varchar(100), NewValue varchar(100))
Create trigger tr_SomTrigger on SomeTable for Update
as
begin
insert into SomeAudit
(Id, OldValue, NewValue)
select i.Id, d.Value, i.Value
from
(
Select Id, Value, Binary_CheckSum(*) Version from Inserted
) i
inner join
(
Select Id, Value, Binary_CheckSum(*) Version from Deleted
) d
on i.Id = d.Id and i.Version <> d.Version
End
Insert into sometable values (1, 'this')
Update SomeTable set Value = 'That'
Select * from SomeAudit