The application I work on has an audit function which woks by setting SQL triggers on certain fields. When a change is detected, the trigger fires and adds a row to an audit table indicating what field changed, type of change (update, insert, or delete) key value for changed field, and before & after values. However, changes can be made by various program modules, and i want to track which module or sub-module made a particular change. this is a multi-user concurrent application, so different users (or scripts) may be running different modules, or different instances of the same module, at the same time. Locking prevents different users from changing the same record at the same time, and is working properly.
Here is the existing trigger for one table on updates. The triggers for insert and delete, and on a second table, are all quite similar. (table and field names have been changed)
ALTER trigger [dbo].[APP_trg_AU_Ptbl1]
on [dbo].[Ptbl1] for update as
declare #ct int, #now datetime, #id int
select #ct = ##rowcount
if #ct = 0 return
select #id = s.user_identity
from SpidUser s inner join master..sysprocesses m
on m.spid = s.spid and m.login_time = s.login_time
where m.spid = ##SPID
select #id = isnull(#id, 0)
select #now = getdate()
insert AuditTbl(edit_time, edit_type, useridentity, table_name,
field_name, old_value, new_value, _fk_table, _fk_person, _fk_event)
select #now, convert(char(1),'U'), #id, 'Ptbl1',
convert(varchar(255), 'item_code'), convert(varchar(255),
deleted.item_code), convert(varchar(255), _src.item_code),
convert(int, _src._pk), convert(int, EVENT.person_id), convert(int,
EVENT.EVENT_id)
FROM deleted INNER JOIN inserted AS _src ON deleted._pk=_src._pk INNER JOIN
EVENT ON _src._fk_event=EVENT.EVENT_id
WHERE 1 = CASE
WHEN _src.item_code is null THEN
CASE WHEN deleted.item_code is null THEN 0 ELSE 1 END
ELSE CASE WHEN deleted.item_code is null THEN 1
ELSE CASE WHEN _src.item_code = deleted.item_code THEN 0 ELSE 1 END
END
END
UNION ALL select #now, convert(char(1),'U'), #id, 'Ptbl1',
'item_sequence_num', convert(varchar(255), deleted.item_sequence_num),
convert(varchar(255), _src.item_sequence_num), _src._pk, EVENT.person_id,
EVENT.EVENT_id
FROM deleted INNER JOIN inserted AS _src ON deleted._pk=_src._pk INNER JOIN
EVENT ON _src._fk_event=EVENT.EVENT_id
WHERE 1 = CASE
WHEN _src.item_sequence_num is null THEN
CASE WHEN deleted.item_sequence_num is null THEN 0 ELSE 1 END
ELSE CASE WHEN deleted.item_sequence_num is null THEN 1
ELSE CASE WHEN _src.item_sequence_num = deleted.item_sequence_num THEN 0 ELSE 1 END
END
END
I want to be able to set a string that identifies the currently active module, so that the trigger would be able to pick it up and include it when adding a record to the audit table. I can't lock a table/field combo that everyone will write to, as that would serialize all DB access and destroy concurrency and thus performance. I am not sure where I can put or expose the string so that the trigger can reliably associate it with the specific change that caused the trigger to fire.
Client sites use a mix of SQL-server 2005, 2008, and 2012, so any code must work in all of these.
Any suggestions would be welcome.
Related
I have a Data Script, It executed about 3 minutes in a database, but when I try to execute that on another database (Different Servers) It takes more than 4 hours so I kill it.
when I get Server state the query is in wait (CXCONSUMER type) from first second. and Who_2 shows 25 Records with Cmd = OPEN CURSOR and 24 Status = suspended and 1 status = runnable.
How can I turn off parallelism in sql and run my script?
declare #PolicyId int, #ElhNo int, #Y int, #CapitalChangeZarib decimal(38, 10), #PrmChangeZarib decimal(38, 10)
declare PayPeriodCursor cursor local for
select Distinct CurrBNVer.PolicyId, CurrBNVer.ElhNo, CurrBNVer.Y, CurrBNVer.CapitalChangeZarib, CurrBNVer.PrmChangeZarib
from v_Table1 CurrBNVer
left join v_Table1 PreBNVer on CurrBNVer.PolicyId = PreBNVer.PolicyId and
CurrBNVer.ElhNo - 1 = PreBNVer.ElhNo
order by CurrBNVer.PolicyId, CurrBNVer.ElhNo
open PayPeriodCursor
fetch next from PayPeriodCursor into #PolicyId, #ElhNo, #Y, #CapitalChangeZarib, #PrmChangeZarib
while ##FETCH_STATUS = 0
begin
update Table2
set Filed1= 0.01 * #CapitalChangeZarib,
Filed2= 0.01 * #PrmChangeZarib
where PolicyId = #PolicyId and
ElhNo >= #ElhNo and
Year >= ISNULL(#Y, 0)
fetch next from PayPeriodCursor into #PolicyId, #ElhNo, #Y, #CapitalChangeZarib, #PrmChangeZarib
end
close PayPeriodCursor
deallocate PayPeriodCursor
go
Finaly I found what is the problem. I was using a view that includes a select * from table1
Somebody had been added a nullable column to table1 before I run my script. So I needed to Refresh the view before using that. But I didnt know about that adding field. So one of my important fields (used in my join) returned null for entire table(the field is not nullable but the view returns value of some other field instead). So it was only the join which took that long because of null values. All I did is refreshing the view and problem solved.
So my website is currently pulling stock information from "J" warehouse in a database called InvWarehouse, now we want to add "JT", so that it pulls from both "J" and "JT"
We have a scheduled stored procedure called "[spc_Schedule_Update_Qty_Stock]"that runs script in the back ground updating new information to the website. When I executed the stored procedure it returned a value of 0, I'm not exactly sure what that means.
ALTER PROCEDURE [dbo].[spc_Schedule_Update_Qty_Stock]
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
UPDATE item
SET QtyOnHand = CASE WHEN (inv.QtyOnHand - inv.QtyAllocated) < 0 THEN 0 ELSE (inv.QtyOnHand - inv.QtyAllocated) END--inv.QtyOnHand
FROM tb_Item item WITH (NOLOCK)
INNER JOIN [105.255.132.000].SysproCompanyA.dbo.InvWarehouse inv WITH (NOLOCK)
ON inv.StockCode = item.sCode COLLATE SQL_Latin1_General_CP1_CI_AS
WHERE inv.Warehouse in ('J','JT')
END
I expect the output of the following to be 9, if "J" has 7 belts and "JT" has 2, with what I did so far there's no change.
You have to do something like below. Your original query is doing calculation at individual warehouse level only. You have to aggregate the stocks at two warehouses and then you have to apply that for UPDATE operation.
;WITH WarehousesAvailability AS
(
SELECT inv.StockCode, Sum(inv.QtyOnHand) AS QtyOnHand, Sum(QtyAllocated) AS QtyAllocated
FROM [105.255.132.248].SysproCompanyA.dbo.InvWarehouse inv
WHERE inv.Warehouse in ('J','JT')
GROUP BY inv.StockCode
)
UPDATE item
SET QtyOnHand = CASE WHEN (inv.QtyOnHand - inv.QtyAllocated) < 0 THEN 0 ELSE (inv.QtyOnHand - inv.QtyAllocated) END--inv.QtyOnHand
FROM tb_Item item
INNER JOIN WarehousesAvailability inv
ON inv.StockCode = item.sCode COLLATE SQL_Latin1_General_CP1_CI_AS
I have a stored procedure as follows:
CREATE PROCEDURE [dbo].[RV_SM_WORKITEM_CHECKWORKBYTYPE]
(
#v_ServiceName Nvarchar(20)
,#v_WorkType Nvarchar(20)
,#v_WorkItemThreadId nvarchar(50)
)
AS BEGIN
;WITH updateView AS
(
SELECT TOP 1 *
FROM rv_sm_workitem WITH (UPDLOCK)
WHERE stateofitem = 0
AND itemtype = #v_worktype
ORDER BY ITEMPRIORITY
)
UPDATE updateView
SET assignedto = #v_ServiceName,
stateofitem = 1,
dateassigned = getdate(),
itemthreadid = #v_WorkItemThreadId
OUTPUT INSERTED.*
END
It does the job I need it to do, namely, grab 1 record with a highest priority, change it's state from Available(0) to Not-Available(1), and return the record for work to be done with it. I should be able to have many threads (above 20) use this proc and have all 20 constantly running/grabbing a new workitem. However I am finding that beyond 2 threads, addition threads are waiting on locks; I'm guessing the UPDLOCK is causing this.
I have 2 questions, is there a better way to do this?
Can I do this without the UPDLOCK in the cte since the update statement by default uses UPDLOCK? Note, at any given time, there are over 400,000 records in this table.
I had to so something similar once and this is what I would suggest:
AS BEGIN
DECLARE #results table (id int, otherColumns varchar(50))
WHILE (EXISTS(SELECT TOP 1 * FROM #results))
BEGIN
;WITH updateView AS
(
SELECT TOP 1 *
FROM rv_sm_workitem
WHERE stateofitem = 0
AND itemtype = #v_worktype
ORDER BY ITEMPRIORITY
)
UPDATE updateView
SET assignedto = #v_ServiceName,
stateofitem = 1,
dateassigned = getdate(),
itemthreadid = #v_WorkItemThreadId
OUTPUT INSERTED.* into #results
where stateofitem = 0
END
END
This ensures that the call cannot not allow a item to be double processed. (because of the where clause on the update statement).
There are other variations of this idea, but this is an easy way to convey it. This is not production ready code though, as it will continually circle in the while loop until there is something to process. But I leave it to you to decide how to break out or not loop and return empty (and let the client side code deal with it.)
Here is the answer that helped me when I had this issue.
MyTableA has several million records. On regular occasions every row in MyTableA needs to be updated with values from TheirTableA.
Unfortunately I have no control over TheirTableA and there is no field to indicate if anything in TheirTableA has changed so I either just update everything or I update based on comparing every field which could be different (not really feasible as this is a long and wide table).
Unfortunately the transaction log is ballooning doing a straight update so I wanted to chunk it by using UPDATE TOP, however, as I understand it I need some field to determine if the records in MyTableA have been updated yet or not otherwise I'll end up in an infinite loop:
declare #again as bit;
set #again = 1;
while #again = 1
begin
update top (10000) MyTableA
set my.A1 = their.A1, my.A2 = their.A2, my.A3 = their.A3
from MyTableA my
join TheirTableA their on my.Id = their.Id
if ##ROWCOUNT > 0
set #again = 1
else
set #again = 0
end
is the only way this will work if I add in a
where my.A1 <> their.A1 and my.A2 <> their.A2 and my.A3 <> their.A3
this seems like it will be horribly inefficient with many columns to compare
I'm sure I'm missing an obvious alternative?
Assuming both tables are the same structure, you can get a resultset of rows that are different using
SELECT * into #different_rows from MyTable EXCEPT select * from TheirTable and then update from that using whatever key fields are available.
Well, the first, and simplest solution, would obviously be if you could change the schema to include a timestamp for last update - and then only update the rows with a timestamp newer than your last change.
But if that is not possible, another way to go could be to use the HashBytes function, perhaps by concatenating the fields into an xml that you then compare. The caveat here is an 8kb limit (https://connect.microsoft.com/SQLServer/feedback/details/273429/hashbytes-function-should-support-large-data-types) EDIT: Once again, I have stolen code, this time from:
http://sqlblogcasts.com/blogs/tonyrogerson/archive/2009/10/21/detecting-changed-rows-in-a-trigger-using-hashbytes-and-without-eventdata-and-or-s.aspx
His example is:
select batch_id
from (
select distinct batch_id, hash_combined = hashbytes( 'sha1', combined )
from ( select batch_id,
combined =( select batch_id, batch_name, some_parm, some_parm2
from deleted c -- need old values
where c.batch_id = d.batch_id
for xml path( '' ) )
from deleted d
union all
select batch_id,
combined =( select batch_id, batch_name, some_parm, some_parm2
from some_base_table c -- need current values (could use inserted here)
where c.batch_id = d.batch_id
for xml path( '' ) )
from deleted d
) as r
) as c
group by batch_id
having count(*) > 1
A last resort (and my original suggestion) is to try Binary_Checksum? As noted in the comment, this does open the risk for a rather high collision rate.
http://msdn.microsoft.com/en-us/library/ms173784.aspx
I have stolen the following example from lessthandot.com - link to the full SQL (and other cool functions) is below.
--Data Mismatch
SELECT 'Data Mismatch', t1.au_id
FROM( SELECT BINARY_CHECKSUM(*) AS CheckSum1 ,au_id FROM pubs..authors) t1
JOIN(SELECT BINARY_CHECKSUM(*) AS CheckSum2,au_id FROM tempdb..authors2) t2 ON t1.au_id =t2.au_id
WHERE CheckSum1 <> CheckSum2
Example taken from http://wiki.lessthandot.com/index.php/Ten_SQL_Server_Functions_That_You_Have_Ignored_Until_Now
I don't know if this is better than adding where my.A1 <> their.A1 and my.A2 <> their.A2 and my.A3 <> their.A3, but I would definitely give it a try (assuming SQL Server 2005+):
declare #again as bit;
set #again = 1;
declare #idlist table (Id int);
while #again = 1
begin
update top (10000) MyTableA
set my.A1 = their.A1, my.A2 = their.A2, my.A3 = their.A3
output inserted.Id into #idlist (Id)
from MyTableA my
join TheirTableA their on my.Id = their.Id
left join #idlist i on my.Id = i.Id
where i.Id is null
/* alternatively (instead of left join + where):
where not exists (select * from #idlist where Id = my.Id) */
if ##ROWCOUNT > 0
set #again = 1
else
set #again = 0
end
That is, declare a table variable for collecting the IDs of the rows being updated and use that table for looking up (and omitting) IDs that have already been updated.
A slight variation on the method would be to use a local temporary table instead of a table variable. That way you would be able to create an index on the ID lookup table, which might result in better performance.
If schema change is not possible. How about using trigger to save off the Ids that have changed. And only import/export those rows.
Or use trigger to export it immediately.
I've done this before somewhere I'm sure of it!
I have a SQL Server 2000 table that I need to log changes to fields on updates and inserts into a second Logging table. A simplified version of the structure I'm using is below:
MainTable
ID varchar(10) PRIMARY KEY
DESCRIPTION varchar(50)
LogTable
OLDID varchar(10)
NEWID varchar(10)
For any other field something like this would work great:
Select i.DESCRIPTION As New, d.DESCRIPTION As Old
From Inserted i
LEFT JOIN Deleted d On i.ID=d.ID
...But obviously the join would fail if ID was changed.
I cannot modify the Tables in way, the only power I have in this database is to create a trigger.
Alternatively is there someone who can teach me time travelling and I'll go back into the past and ask myself back then how I did this? Cheers :)
Edit:
I think I need to clarify a few things here. This is not actually my database, it is a pre-existing system that I have almost no control of, other than writing this trigger.
My question is how can I retrieve the old primary key if said primary key was changed. I don't need to be told that I shouldn't change the primary key or about chasing up foreign keys etc. That's not my problem :)
DECLARE #OldKey int, #NewKey int;
SELECT #Oldkey = [ID] FROM DELETED;
SELECT #NewKey = [ID] FROM INSERTED;
This only works if you have a single row. Otherwise you have no "anchor" to link old and new rows. So check in your trigger for > 1 in INSERTED.
Is it possible to assume that the INSERTED and DELETED tables presented to you in a trigger are guaranteed to be in the same order?
I don't think it's possible. Imagine if you have 4 rows in the table:
1 Val1
2 Val2
3 Val3
4 Val4
Now issue the following update:
UPDATE MainTable SET
ID = CASE ID WHEN 1 THEN 2 WHEN 2 THEN 1 ELSE ID END
Description = CASE ID WHEN 3 THEN 'Val4' WHEN 4 THEN 'Val3' ELSE Description END
Now, how are you going to distinguish between what happened to rows 1 & 2 and what happened to rows 3 & 4. And more importantly, can you describe what's different between them? All of the stuff that tells you which columns have been updated won't help you.
If it's possible in this case that there's an additional key on the table (e.g. Description is UNIQUE), and your update rules allow it, you could write the trigger to prevent simultaneous updates to both keys, and then you can use whichever key hasn't been updated to correlate the two tables.
If you must handle multiple-row inserts/updates, and there's no alternate key that's guaranteed not to change, the only way I can see to do this is to use an INSTEAD OF trigger. For example, in the trigger you could break the original insert/update command into one command per row, grabbing each old id before you insert/update.
Within triggers in SQL Server you have access to two tables: deleted and inserted. Both of these have already been mentioned. Here's how they function depending on what action the trigger is firing on:
INSERT OPERATION
deleted - not used
inserted - contains the new rows being added to the table
DELETE OPERATION
deleted - contains the rows being removed from the table
inserted - not used
UPDATE OPERATION
deleted - contains the rows as they would exist before the UPDATE operation
inserted - contains the rows as they would exist after the UPDATE operation
These function in every way like tables. Therefore, it is entirely possible to use a row based operation such as something like the following (Operation exists only on the audit table, as does DateChanged):
INSERT INTO MyAuditTable
(ID, FirstColumn, SecondColumn, ThirdColumn, Operation, DateChanged)
VALUES
SELECT ID, FirstColumn, SecondColumn, ThirdColumn, 'Update-Before', GETDATE()
FROM deleted
UNION ALL
SELECT ID, FirstColumn, SecondColumn, ThirdColumn, 'Update-After', GETDATE()
FROM inserted
----new----
add an identity column to the table that the application can not change, you can then use that new column to join the inserted to the deleted tables within the trigger:
ALTER TABLE YourTableName ADD
PrivateID int NOT NULL IDENTITY (1, 1)
GO
----old----
Don't ever update/change key values. How can you do this and fix all of your foreign keys?
I wouldn't recommend ever using a trigger that can't handle a set of rows.
If you must change the key, insert a new row with the proper new key and values, use SCOPE_IDENTITY() if that is what your are doing. Delete the old row. Log for the old row that it was changed to the new row's key, which you should now have. I hope there is no foreign key on the changed key in your log...
You can create a new identity column on table MainTable (named for example correlationid) and correlate inserted and deleted tables using this column.
This new column should be transparent for existing code.
INSERT INTO LOG(OLDID, NEWID)
SELECT deleted.id AS OLDID, inserted.id AS NEWID
FROM inserted
INNER JOIN deleted
ON inserted.correlationid = deleted.correlationid
Pay attention, you could insert duplicate records in the log table.
Of course nobody should be changing the primary key on the table -- but that is exactly what triggers are supposed to be for (in part), is to keep people from doing things they shouldn't do. It's a trivial task in Oracle or MySQL to write a trigger that intercepts changes to primary keys and stops them, but not at all easy in SQL Server.
What you of course would love to be able to do would be to simply do something like this:
if exists
(
select *
from inserted changed
join deleted old
where changed.rowID = old.rowID
and changed.id != old.id
)
... [roll it all back]
Which is why people go out googling for the SQL Server equivalent of ROWID. Well, SQL Server doesn't have it; so you have to come up with another approach.
A fast, but sadly not bombproof, version is to write an instead of update trigger that looks to see whether any of the inserted rows have a primary key not found in the updated table or vice versa. This would catch MOST, but not all, of the errors:
if exists
(
select *
from inserted lost
left join updated match
on match.id = lost.id
where match.id is null
union
select *
from deleted new
left join inserted match
on match.id = new.id
where match.id is null
)
-- roll it all back
But this still doesn't catch an update like...
update myTable
set id = case
when id = 1 then 2
when id = 2 then 1
else id
end
Now, I've tried making the assumption that the inserted and deleted tables are ordered in such a way that cursoring through the inserted and deleted tables simultaneously will give you properly matching rows. And this APPEARS to work. In effect you turn the trigger into the equivalent of the for-each-row triggers available in Oracle and mandatory in MySQL...but I would imagine the performance will be bad on massive updates since this is not native behavior to SQL Server. Also it depends upon an assumption that I can't actually find documented anywhere and so am reluctant to depend on. But code structured that way APPEARS to work properly on my SQL Server 2008 R2 installation. The script at the end of this post highlights both the behavior of the fast-but-not-bombproof solution and the behavior of the second, pseudo-Oracle solution.
If anybody could point me to someplace where my assumption is documented and guaranteed by Microsoft I'd be a very grateful guy...
begin try
drop table kpTest;
end try
begin catch
end catch
go
create table kpTest( id int primary key, name nvarchar(10) )
go
begin try
drop trigger kpTest_ioU;
end try
begin catch
end catch
go
create trigger kpTest_ioU on kpTest
instead of update
as
begin
if exists
(
select *
from inserted lost
left join deleted match
on match.id = lost.id
where match.id is null
union
select *
from deleted new
left join inserted match
on match.id = new.id
where match.id is null
)
raisError( 'Changed primary key', 16, 1 )
else
update kpTest
set name = i.name
from kpTest
join inserted i
on i.id = kpTest.id
;
end
go
insert into kpTest( id, name ) values( 0, 'zero' );
insert into kpTest( id, name ) values( 1, 'one' );
insert into kpTest( id, name ) values( 2, 'two' );
insert into kpTest( id, name ) values( 3, 'three' );
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- This throws an error, appropriately
update kpTest set id = 5, name = 'FIVE' where id = 1
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- This allows the change, inappropriately
update kpTest
set id = case
when id = 1 then 2
when id = 2 then 1
else id
end
, name = UPPER( name )
go
select * from kpTest
/*
0 ZERO
1 TWO -- WRONG WRONG WRONG
2 ONE -- WRONG WRONG WRONG
3 THREE
*/
-- Put it back
update kpTest
set id = case
when id = 1 then 2
when id = 2 then 1
else id
end
, name = LOWER( name )
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
drop trigger kpTest_ioU
go
create trigger kpTest_ioU on kpTest
instead of update
as
begin
declare newIDs cursor for select id, name from inserted;
declare oldIDs cursor for select id from deleted;
declare #thisOldID int;
declare #thisNewID int;
declare #thisNewName nvarchar(10);
declare #errorFound int;
set #errorFound = 0;
open newIDs;
open oldIDs;
fetch newIDs into #thisNewID, #thisNewName;
fetch oldIDs into #thisOldID;
while ##FETCH_STATUS = 0 and #errorFound = 0
begin
if #thisNewID != #thisOldID
begin
set #errorFound = 1;
close newIDs;
deallocate newIDs;
close oldIDs;
deallocate oldIDs;
raisError( 'Primary key changed', 16, 1 );
end
else
begin
update kpTest
set name = #thisNewName
where id = #thisNewID
;
fetch newIDs into #thisNewID, #thisNewName;
fetch oldIDs into #thisOldID;
end
end;
if #errorFound = 0
begin
close newIDs;
deallocate newIDs;
close oldIDs;
deallocate oldIDs;
end
end
go
-- Succeeds, appropriately
update kpTest
set name = UPPER( name )
go
select * from kpTest;
/*
0 ZERO
1 ONE
2 TWO
3 THREE
*/
-- Succeeds, appropriately
update kpTest
set name = LOWER( name )
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- Fails, appropriately
update kpTest
set id = case
when id = 1 then 2
when id = 2 then 1
else id
end
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- Fails, appropriately
update kpTest
set id = id + 1
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- Succeeds, appropriately
update kpTest
set id = id, name = UPPER( name )
go
select * from kpTest;
/*
0 ZERO
1 ONE
2 TWO
3 THREE
*/
drop table kpTest
go