How to determine what fields were update in an update trigger - sql

UPDATE: Using Update_Columns() is not an answer to this question, as the fields may change in the order which will break the trigger (Update_Columns depends on the column order).
UPATE 2: I already know that the Deleted and Inserted tables hold the data. The question is how to determine what has changed without having to hard code the field names as the field names may change, or fields may be added.
Lets say I have a table with three fields.
The row already exists, and now the user updates fields 1 and 2.
How do I determine, in the Update Trigger, what the field were updated, and what the before and after values where?
I want to then log these to a log table. If there were two fields update, it should result in two rows in the history table.
Table
Id intField1 charField2 dateField3
7 3 Fred 1995-03-05
Updated To
7 3 Freddy 1995-05-06
History Table
_____________
Id IdOfRowThatWasUpdated BeforeValue AfterValue (as string)
1 7 Fred Freddy
2 7 1995-03-05 1995-05-06
I know I can use the Deleted table to Get the old values, and the inserted table to get the new values. The question however, is how to do this dynamically. In other words, the actual table has 50 columns, and I don't want to hard code 50 fields into a SQL statement, and also if the fields change, and don't want to have to worry about keeping the SQL in sync with table changes.
Greg

you can use one of my favorite XML-tricks to do this:
create trigger utr_Table1_update on Table1
after update, insert, delete
as
begin
with cte_inserted as (
select id, (select t.* for xml raw('row'), type) as data
from inserted as t
), cte_deleted as (
select id, (select t.* for xml raw('row'), type) as data
from deleted as t
), cte_i as (
select
c.ID,
t.c.value('local-name(.)', 'nvarchar(128)') as Name,
t.c.value('.', 'nvarchar(max)') as Value
from cte_inserted as c
outer apply c.Data.nodes('row/#*') as t(c)
), cte_d as (
select
c.ID,
t.c.value('local-name(.)', 'nvarchar(128)') as Name,
t.c.value('.', 'nvarchar(max)') as Value
from cte_deleted as c
outer apply c.Data.nodes('row/#*') as t(c)
)
insert into Table1_History (ID, Name, OldValue, NewValue)
select
isnull(i.ID, d.ID) as ID,
isnull(i.Name, d.Name) as Name,
d.Value,
i.Value
from cte_i as i
full outer join cte_d as d on d.ID = i.ID and d.Name = i.Name
where
not exists (select i.value intersect select d.value)
end;
sql fiddle demo

In this post:
How to refer to "New", "Old" row for Triggers in SQL server?
It is mentioned that/how you can access the original and the new values, and if you can access, you can compare them.
"INSERTED is the new row on INSERT/UPDATE. DELETED is the deleted row on DELETE and the updated row on UPDATE (i.e. the old values before the row was updated)"

Related

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.

Deciphering SQL query

I am currently reviewing a query without access to the databases on which the query is performed. (It's not ideal but that's what I am tasked with). I am not a SQL expert and trying to identify what the below code does as I cannot run the query. It is reading from and writing to the same temp table (duplicating?). I don't know what the source of 'Y' is or what the end result is. Any help is appreciated. Thank you.
INSERT INTO #temp1
SELECT X.CURSTATUS ,X.GENDER ,Y.PACKAGE ,X.AGE ,1 AS factor1 ,1 AS factor2;
FROM #temp1 X WITH (NOLOCK) ,
( SELECT 'P1' AS PACKAGE UNION ALL SELECT 'P2' ) Y
WHERE X.PACKAGE = 'P5';
It is not really writing to the same table. It is "appending" rows to the same table. That is, existing data in the table is not affected.
What it is doing is adding rows for packages "P1" and "P2" for all "P5" packages. This adds the new rows to the table; the "P5" row remains.
For every row in #temp that has a PACKAGE value of "P5", the query is inserting two new rows with PACKAGE values of "P1" & "P2" respectivlly.
Reformatting the query and replacing obsolete syntax with modem syntax should make it easier to understand.
INSERT INTO #temp1 (CURSTATUS, GENDER, PACKAGE, AGE, factor1, factor2)
SELECT
X.CURSTATUS,
X.GENDER,
Y.PACKAGE,
X.AGE,
1 AS factor1,
1 AS factor2
FROM
#temp1 X
CROSS JOIN (
SELECT 'P1' AS PACKAGE
UNION ALL
SELECT 'P2'
) Y
WHERE
X.PACKAGE = 'P5';
INSERT INTO #temp1
-- this is where that data is being inserted into. It should BTW have columns explicitly defined, this format is a SQL antipattern
SELECT X.CURSTATUS ,X.GENDER ,Y.PACKAGE ,X.AGE ,1 AS factor1 ,1 AS factor2;
FROM #temp1 X WITH (NOLOCK) ,
-- this is selecting the current rows from #temp
( SELECT 'P1' AS PACKAGE UNION ALL SELECT 'P2' ) Y
--Y is a two record table with one column called package, since there is no specific join shown, it is a cross join - Again an antipattern, it is far better to explicitly use the Cross Join keywords to make it clear what is going on.
WHERE X.PACKAGE = 'P5';
-- this filters the records from #temp to grab only those where the record values is 'P5'. Since it cross joins to the Two record table Y, it takes the data in the other columns for the P% records and inserts new records for P1 and P2. If you have ten P5 records, this insert would insert 10 P1 records and 10 P2 records.

Populating table from table of value

I've got a largish Oracle table (30M rows) which contains three columns: ID, fieldname, value. I need a query that will update the target table (which contains 93 columns) from the source data. So if the first row of the source table is 1,'first_name','Robert' then this will update the row where ID=1 updating first_name column with the value 'Robert'.
Is this even possible with a query or do I need to process it with another tool?
Hmmm. You can do this with a query. I would suggest building an index on the first table on id, fieldname, value and then running the following update 93 times:
update targettable tt
set field1 = (select max(value) from sourcetable st where st.id = tt.id and st.fieldname = 'field1')
where exists (select 1 from sourcetable st where st.id = tt.id and st.fieldname = 'field1');
You can actually write this all as one query, but it gets complicated to handle rows where only some fields are updated.

SQL: Dedupe table data and manipulate merged data

I have an SQL table with:
Id INT, Name NVARCHAR(MAX), OldName NVARCHAR(MAX)
There are multiple duplicates in the name column.
I would like to remove these duplicates keeping only one master copy of 'Name'. When the the dedupe happens I want to concatenate the old names into the OldName field.
E.G:
Dave | Steve
Dave | Will
Would become
Dave | Steve, Will
After merging.
I know how to de-dupe data using something like:
with x as (select *,rn = row_number()
over(PARTITION BY OrderNo,item order by OrderNo)
from #temp1)
select * from x
where rn > 1
But not sure how to update the new 'master' record whilst I am at it.
This is really too complicated to do in a single update, because you need to update and delete rows.
select n.name,
stuff((select ',' + t2.oldname
from sqltable t2
where t2.name = n.name
for xml path (''), type
).value('/', 'nvarchar(max)'
), 1, 1, '') as oldnames
into _temp
from (select distinct name from sqltable) n;
truncate table sqltable;
insert into sqltable(name, oldnames)
select name, oldnames
from _temp;
Of course, test, test, test before deleting the old table (copy it for safe keeping). This doesn't use a temporary table. That way, if something happens -- like a server reboot -- before the insert is finished, you still have all the data.
Your question doesn't specify what to do with the id column. You can add min(id) or max(id) to the _temp if you want to use one of those values.

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