sql server: How to detect changed rows - sql

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

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;

Cant get the column name that is updated in a historical table in sql

I have a table in sql server with a lot of columns that can be updated every time. Now i want a trigger that inserts into a historical table that i have created the values that are updated in the main table. The historical table is like this:VarName(to store the name of the column that was updated in the main table, ID(the id of the row in the main table), OldValue(the old value that was updated, NewValue(the new value), Date(date updated). Im using this code but I want the trigger to automatically detect the columns changed and storing their names in VarName column. keeping in mind that in one row in the main table can be updated more than one field so if 3 field are updated, the historical table will add 3 rows with the occurring updates with their respective column name in the main table and their old and new values.
CREATE TRIGGER [dbo].UpdateOnMainTable
ON [dbo].[MainTable]
AFTER UPDATE
AS
BEGIN
INSERT [dbo].[Historical] (VarName, ID, OldValue, NewValue, Date)
SELECT ===> the code here
FROM inserted i
INNER JOIN deleted d
ON i.ID = d.ID
END
GO
I want a code that fills my historical table as I explained.
First, your structure requires that the data types be compatible for the columns in your history table.
Second, you need to unpivot and then work from there:
with ic as (
select i.id, v.*
from inserted i cross apply
(values ('col1', i.col1), ('col2', i.col2), ('col3', i.col3)
) v(col, val)
),
dc as (
select d.id, v.*
from deleted d cross apply
(values ('col1', d.col1), ('col2', idcol2), ('col3', d.col3)
) v(col, val)
)
insert into historical (varname, id, oldvalue, newvalue, date)
select ic.col, ic.id, dc.val, ic.val, getdate
from ic join
dc
on ic.id = dc.id and
ic.col = dc.col and
(ic.val <> dc.val or
ic.val is null and dc.val is not null or
ic.val is not null and dc.val is null
);
Note that if the columns have different types, then you need to cast them (presumably to strings) in the values() statements.

I am looking for a way for a trigger to insert into a second table only where the value in table 1 changes

I am looking for a way for a trigger to insert into a second table only where the value in table 1 changes. It is essentially an audit tool to trap any changes made. The field in table 1 is price and we want to write additional fields.
This is what I have so far.
CREATE TRIGGER zmerps_Item_costprice__update_history_tr ON [ITEM]
FOR UPDATE
AS
insert into zmerps_Item_costprice_history
select NEWID(), -- unique id
GETDATE(), -- CURRENT_date
'PRICE_CHANGE', -- reason code
a.ima_itemid, -- item id
a.ima_price-- item price
FROM Inserted b inner join item a
on b.ima_recordid = a.IMA_RecordID
The table only contains a unique identifier, date, reference(item) and the field changed (price). It writes any change not just a price change
Is it as simple as this? I moved some of the code around because comments after the comma between columns is just painful to maintain. You also should ALWAYS specify the columns in an insert statement. If your table changes this code will still work.
CREATE TRIGGER zmerps_Item_costprice__update_history_tr ON [ITEM]
FOR UPDATE
AS
insert into zmerps_Item_costprice_history
(
UniqueID
, CURRENT_date
, ReasonCode
, ItemID
, ItemPrice
)
select NEWID()
, GETDATE()
, 'PRICE_CHANGE'
, d.ima_itemid
, d.ima_price
FROM Inserted i
inner join deleted d on d.ima_recordid = i.IMA_RecordID
AND d.ima_price <> i.ima_price
Since you haven't provided any other column names I Have used Column2 and Column3 and the "Other" column names in the below example.
You can expand adding more columns in the below code.
overview about the query below:
Joined the deleted and inserted table (only targeting the rows that has changed) joining with the table itself will result in unnessacary processing of the rows which hasnt changed at all.
I have used NULLIF function to yeild a null value if the value of the column hasnt changed.
converted all the columns to same data type (required for unpivot) .
used unpivot to eliminate all the nulls from the result set.
unpivot will also give you the column name its has unpivoted it.
CREATE TRIGGER zmerps_Item_costprice__update_history_tr
ON [ITEM]
FOR UPDATE
AS
BEGIN
SET NOCOUNT ON ;
WITH CTE AS (
SELECT CAST(NULLIF(i.Price , d.Price) AS NVARCHAR(100)) AS Price
,CAST(NULLIF(i.Column2 , d.Column2) AS NVARCHAR(100)) AS Column2
,CAST(NULLIF(i.Column3 , d.Column3) AS NVARCHAR(100)) AS Column3
FROM dbo.inserted i
INNER JOIN dbo.deleted d ON i.IMA_RecordID = d.IMA_RecordID
WHERE i.Price <> d.Price
OR i.Column2 <> d.Column2
OR i.Column3 <> d.Column3
)
INSERT INTO zmerps_Item_costprice_history
(unique_id, [CURRENT_date], [reason code], Item_Value)
SELECT NEWID()
,GETDATE()
,Value
,ColumnName + '_Change'
FROM CTE UNPIVOT (Value FOR ColumnName IN (Price , Column2, Column3) )up
END
As I understand your question correctly, You want to record change If and only if The column Price value is changes, you dont need any other column changes to be recorded
here is your code
CREATE TRIGGER zmerps_Item_costprice__update_history_tr ON [ITEM]
FOR UPDATE
AS
if update(ima_price)
insert into zmerps_Item_costprice_history
select NEWID(), -- unique id
GETDATE(), -- CURRENT_date
'PRICE_CHANGE', -- reason code
a.ima_itemid, -- item id
a.ima_price-- item price
FROM Inserted b inner join item a
on b.ima_recordid = a.IMA_RecordID

Insert values into table from the same table

Using SQL server (2012)
I have a table - TABLE_A with columns
(id, name, category, type, reference)
id - is a primary key, and is controlled by a separte table (table_ID) that holds the the primary next available id. Usually insertions are made from the application side (java) that takes care of updating this id to the next one after every insert. (through EJBs or manually, etc..)
However,
I would like to to write stored procedure (called from java application) that
- finds records in this table where (for example) reference = 'AAA' (passed as
parameter)
- Once multiple records found (all with same reference 'AAA', I want it to INSERT new
records with new ID's and reference = 'BBB', and other columns (name, category, type)
being same as in the found list.
I am thinking of a query similar to this
INSERT INTO table_A
(ID
,NAME
,CATEGORY
,TYPE,
,Reference)
VALUES
(
**//current_nextID,**
(select NAME
from TABLE_A
where REFENCE in (/*query returning value 'AAA' */),
(select CATEGORY
from TABLE_A
where REFENCE in (/*query returning value 'AAA' */),
(select TYPE
from TABLE_A
where REFENCE in (/*query returning value 'AAA' */),
'BBB - NEW REFERENCE VALUE BE USED'
)
Since, I don't know how many records I will be inserting , that is how many items in the result set of a criteria query
select /*field */
from TABLE_A
where REFENCE in (/*query returning value 'AAA' */),
I don't know how to come up with the value of ID, on every record. Can anyone suggest anything, please ?
It's not clear from your question how sequencing is handled but you can do something like this
CREATE PROCEDURE copybyref(#ref VARCHAR(32)) AS
BEGIN
-- BEGIN TRANSACTION
INSERT INTO tablea (id, name, category, type, reference)
SELECT value + rnum, name, category, type, 'BBB'
FROM
(
SELECT t.*, ROW_NUMBER() OVER (ORDER BY id) rnum
FROM tablea t
WHERE reference = 'AAA'
) a CROSS JOIN
(
SELECT value
FROM sequence
WHERE table_id = 'tablea'
) s
UPDATE sequence
SET value = value + ##ROWCOUNT + 1
WHERE table_id = 'tablea'
-- COMMIT TRANSACTION
END
Sample usage:
EXEC copybyref 'AAA';
Here is SQLFiddle demo