27 October 2006 09:06 tonyrogerson

SQL 2000 Triggers - Using INSERTED/DELETED and effect on LDF + Disable Trigger Doesn't work

Did you know that when you use the INSERTED and DELETED system materialised tables in a FOR (AFTER) trigger they are actually materialised from the transaction log (LDF file). Why is this bad? Transaction log writing is sequential in nature and for optimum performance no other processes should be writing to the disk the transaction log is on (even other transaction logs for other databases), that way the disk head will be in exactly the correct position for the next write thereby reducing the latency for disk writes thereby allowing your transaction to commit faster.

But, did you also know that ALTER TABLE <blah> DISABLE TRIGGER <blah> only disables the functionality of the trigger, something else is going on that causes the same underlying additional writes to the transaction log which is the overhead of using the trigger, for instance I have a straight UPDATE, without the trigger it writes 843Kbytes to the transaction log, with the trigger it writes 80,028Kbytes – yes, that’s a tad more! Even disabling the trigger still causes the 80Mbytes to be written to the transaction log.

Note: I’m keeping this entry purely SQL Server 2000 (SP4 and SP4 with 2187 applied) tested on multiple servers; I’ll update for 2005 shortly, 2005 should use tempdb instead because of the version store.

Here is the script:-

Set up:-

CREATE DATABASE [play] ON  PRIMARY

( NAME = N'play', FILENAME = N'f:\MSSQL\MSSQL\data\play.mdf' , SIZE = 512000KB , FILEGROWTH = 10240KB )

 LOG ON

( NAME = N'play_log', FILENAME = N'f:\MSSQL\MSSQL\data\play_log.ldf' , SIZE = 512000KB , FILEGROWTH = 10240KB )

GO

EXEC dbo.sp_dbcmptlevel @dbname=N'play', @new_cmptlevel=80

GO

ALTER DATABASE [play] SET RECOVERY FULL

GO

 

create table mytest (

    id  int not null    identity primary key clustered,

    trade_quantity  int not null,

    pad_row char(4000) not null

)

create table summary (

    trade_quantity int not null

)

insert summary values( 0 )

 

set nocount on

declare @i int

set @i = 1

while @i <= 10000

begin

    insert mytest ( trade_quantity, pad_row ) values( @i, 's' )

    set @i = @i + 1

end

go

Ok, now the fun part; first lets test a straight update without the trigger.

select *

from ::fn_virtualfilestats( 20, 2 )

go

 

update mytest

    set trade_quantity = trade_quantity + 1,

        pad_row = cast( trade_quantity as char(4000) )

go

 

select *

from ::fn_virtualfilestats( 20, 2 )

go

Now, remember to substitute 20 with DB_ID() !

Results:-

--  NumberReads = 0

--  NumberWrites= 15

--  BytesWritten= 843KBytes

--  BytesRead   = 0

That’s good, we have only written to the LDF.

Now the trigger code, this is common logic, it updates a summary table; note – this additional write and materialisation from the LDF would also be there even if you are just querying the INSERTED and DELETED tables aka referential integrity triggers.

create trigger trg_test on mytest FOR update

as

begin

    update summary

        set trade_quantity = ( trade_quantity -

                             ( select sum( trade_quantity )

                               from deleted ) )

                           + ( select sum( trade_quantity )

                               from inserted )

 

end

go

And now the results…

select *

from ::fn_virtualfilestats( 20, 2 )

go

 

update mytest

    set trade_quantity = trade_quantity + 1,

        pad_row = cast( trade_quantity as char(4000) )

go

 

select *

from ::fn_virtualfilestats( 20, 2 )

go

 

--  NumberReads = 1344

--  NumberWrites= 1373

--  BytesWritten=  80,084KBytes

--  BytesRead   = 157,958KBytes

Look, we have had to do 157Mbytes of read FROM the LDF but also the write requirement has now grown from 843Kbytes to an astonishing 80Mbytes!

Now disable the trigger and try the test again…

alter table mytest disable trigger trg_test

go

 

select *

from ::fn_virtualfilestats( 20, 2 )

go

 

update mytest

    set trade_quantity = trade_quantity + 1,

        pad_row = cast( trade_quantity as char(4000) )

go

 

select *

from ::fn_virtualfilestats( 20, 2 )

go

 

--  NumberReads = 8

--  NumberWrites= 1355

--  BytesWritten= 80,082KBytes

--  BytesRead   = 32KBytes

The number of reads and bytes read has dramatically reduced now, but look at the number of bytes written – its still 80Mbytes! Stone me J I was expecting bytes written to be back to 843Kbytes!!

Lets drop the trigger and see what happens…

drop trigger trg_test

go

 

select *

from ::fn_virtualfilestats( 20, 2 )

go

 

update mytest

    set trade_quantity = trade_quantity + 1,

        pad_row = cast( trade_quantity as char(4000) )

go

 

select *

from ::fn_virtualfilestats( 20, 2 )

go

 

--  NumberReads = 0

--  NumberWrites= 15

--  BytesWritten= 843KBytes

--  BytesRead   = 0KBytes

Yep, back to normal now.

The amount of data written varies because in the non-trigger situation SQL Server is clever enough to only write to the log what needs to be changed, so it doesn't write the whole row, that is born out by running this statement multiple times, the first causes more writes because it has more 'changes' to do.

 

select *

from ::fn_virtualfilestats( 20, 2 )

go

 

update mytest

    set trade_quantity = trade_quantity + 1,

        pad_row = replicate( 'a', 4000 )

go

 

select *

from ::fn_virtualfilestats( 20, 2 )

go

-- 1st run = 1063424bytes
-- 2nd run =  782848bytes
-- 3rd run =  782848bytes

When you have a trigger, because you may require INSERTED and DELETED system materialised tables then it has to put down the whole row to the transaction log (I think it's worse than that, it needs to put down the old and new rows to the log), this is because the INSERTED and DELETED tables are in column terms a copy of the base table, they do only contain the rows being processed, but you start to see why the trigger is so much more hefty.

Why disabling the trigger is not giving the minimalistic behaviour I’m not sure, there must be some internal storage engine requirement because the trigger is there; but to summarise – if you are using triggers please be aware of the above behaviour; if you have long running batch jobs that use triggers that you disable before commencing it is best to drop the trigger first rather than disabling it.

Also - be aware of things that use triggers, for instance merge replication!

Filed under:

Comments

# re: SQL 2000 Triggers - Using INSERTED/DELETED and effect on LDF + Disable Trigger Doesn't work

27 October 2006 15:06 by Colin Leversuch-Roberts

Well my view has always been - if you want to cripple your database performance use triggers - nothing has ever changed my view on that. What of most concern to me over your findings is the log read , normally you'd try to optimise your tran log for writes ( unless you have a SAN when you probably can't ) so a read could prove very disruptive and at worst case make your server unresponsive.
very interesting.