October 2006 - Posts

Just picked the majority of my crop, grown in a mixture of green house and garden here in Harpenden just north of London in the UK.

Its the effort of about 8 plants at a price of about £1.50.

I know the small ones are lethal as my wife made a big pan of soup and only put one in there and it was really hot :).

Anybody got any ideas what to do with them? I'm going to try and create some chillie oil by putting some in olive oil.

 

 

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!

SQLServerFAQ.COM UK SQL Server User Community
Providing the Database Professional with the tools to stay current

Lot's of stuff going on now, we have an event tonight (24th) in Reading and also one on the 29th Nov again in Reading, we are working on other venues - we are planning one in Scotland for January and need people to send us topic suggestions - please email Martin Bell, SQL Server MVP; I'm also presenting on the 1st November in Oxford repeating the session I did at the VBUG conference.

I've been doing a lot of cluster work of late, building 2005 HA environments, one specific client we are using an EMC CX300 and having really big problems getting good performance on log writes, a write to the log is done in chunks of 512bytes to 64KBytes and the SAN just can't handle these small IO's very well; I expand in a blog entry how to go about seeing what transaction log throughput you get from your kit - extremely important because writing to the log is critical for a well performing and scalable system.

Don't forget to check out the events section, some good, albeit a bit BI orientated but I'll fix that; check out the 2 day SQL Tuning class and the 5 day BI boot camps.

PS.. No word on SP2 beta yet and if you are on SP4 consider moving to hotfix 2187; too many problems with SP4 imho.

Tony Rogerson, SQL Server MVP
tonyrogerson@sqlserverfaq.com
24th October
2006
 

Build a Microsoft Cluster environment for less than £17,000
Most work I do is for small to medium size companies, some with enterprise size systems some not; there is more interest in high availability and resilience in this domain now - not just playground of the money to burn enterprise companies. Anyway, in this post I comment on the HP MSA500 packaged cluster and what you can build for < £17K.

An average system (7 x 146GB disks, 4 in RAID 10, 2 in RAID 1 and 1 hot spare, 5GB of RAM) costs.....

Link to blog entry
 

SQL Server 2005 Rebuilding Indexes
Simon Sabin talks about problems he's had rebuilding indexes and performance reasons for doing so when upgrading.

Link to blog entry
 

Server Commissioning Part #1 - Transaction Log and Performance
For any write activity to the database the write performance to the transaction log is critical. If you have high latency on writing to the transaction log then your process will be slow. This article comes from problems Tony Rogerson has had building a cluster using the EMC CX300 and some must knows when building and trialling your kit.

Link to blog entry
 

Comments on the UPDATE statement
What's best? Specify only the columns you are updating or does it matter?

Link to blog entry
 

About the Group, UK's biggest SQL Server community

The group has nearly 9,500 members in the UK, the site holds over 900,500 news group posts, has over 300 distinct daily visitors generating in excess of 3,000 sessions. The site holds a growing number of short video demonstrations on SQL Server that can be viewed in Media Player.

We are one of the CodeZone Federated Search members, you will see us in the help system built into Visual Studio 2005 and SQL Server 2005—we made it into the product, my work is done :-)!

The group was founded in 1998 and is still run by Tony Rogerson who has been awarded SQL Server MVP (Most Valued Professional) for 8 years running, as well as running the group his day job is an Independent SQL Specialist/Consultant.

If you do not want to receive this briefing again then please reply with the subject 'remove' or logon to http://sqlserverfaq.com and adjust your profile.

Did you know SQL Server is clever enough not to log stuff it doesn't really need to.

So, this statement ->

UPDATE mytest 
   SET smallcol = smallcol +
1
     , somedata =
somedata
WHERE id = @i

Is the same as writing this ->

UPDATE mytest 
   SET smallcol = smallcol +
1
WHERE id = @i

Also, it's not the whole 8KB page that gets logged, it's just the data that's changed, so in this case the column smallcol; the above statements actually cause 512bytes write activity to the log.

Ok, referencing back to my commission server article increasing the number of rows in the transaction improves performance dramatically in this case; the single row, single integer column update requires a log write of 512 bytes per row, so for 100,000 rows its 48MBytes; however, changing the transaction so that 10000 rows are updated per transaction (still 10,000 update statements per commit) we get 11MBytes written, what a difference!

Finally, doing the UPDATE in one go as in below gives our best performance (doh, no suprises there!) just 9MBytes written to the log in 168 IO's.

UPDATE mytest 
   SET smallcol = smallcol +
1

I suppose this is more directly applicable to anybody doing batch job cursor updates than anything else.

I guess the moral of the story is this: when writing your update or population routines always go for the option that creates the least IO's; that means benchmarking using the statistics from ::fn_virtualfilestats (NumberWrites and NumberReads).

It’s quite difficult to work out if your kit will support the load that you want to put on it. Through my consultancy years I’ve often been in the position of trying to determine whether an existing system will be capable of scaling up to more users and more data – this is a difficult task, but there are methods.

 

In this article I suggest a method for determining the performance and ability of your hardware in servicing the write load for your system, specifically determining how many updates/inserts/deletes can be done with the performance of the transaction log being the only factor, so discounting concurrency (for the moment!).

 

We need to consider a couple of things before going any further, firstly an INSERT / UPDATE / DELETE is not complete and SQL Server will not allow the process to continue until it has been told that the data physically resides on in the transaction log on the disk (the LDF file), the MDF is written in a different matter, the throughput of INSERT’s, UPDATE’s and DELETE’s is directly related to how fast you can get data into the LDF on disk whereas the MDF is written in a different manner – dirty pages (changes) are batched up and written as a big burst of data around checkpoint time or when the SQL Server decides it needs the pages for some other data so has to write them out to disk.

 

Straight to the test, create a test database, change G: to where you want to test your transaction log performance and E: to your MDF file; try and keep them separate especially for this test but in practice writing to the transaction log is almost purely sequential and the MDF random so you want the transaction log on its own disks where nothing will be changing the disk head position.

 

CREATE DATABASE [IOMeter] ON  PRIMARY

( NAME = N'IOMeter', FILENAME = N'e:\IOMeter.mdf' , SIZE = 1048576KB , FILEGROWTH = 1024KB )

 LOG ON

( NAME = N'IOMeter_log', FILENAME = N'g:\IOMeter_log.ldf' , SIZE = 1048576KB , FILEGROWTH = 10%)

GO

EXEC dbo.sp_dbcmptlevel @dbname=N'IOMeter', @new_cmptlevel=90

GO

GO

ALTER DATABASE [IOMeter] SET RECOVERY FULL

GO

ALTER DATABASE [IOMeter] SET PAGE_VERIFY CHECKSUM 

GO

 

Let’s create our test table, this can be one of your own tables, in fact if you have the time you could actually use your own schema and test a specific stored procedure performance etc. but for this example I just want to illustrate some log behaviour so I’ll use a simple table…

 

USE IOMeter

GO

CREATE TABLE mytest (

    id  int not null identity primary key clustered,

    somedata char(4000) not null

)

GO

CREATE TABLE io_results (

    test_name varchar(100) not null,

    entry_date  datetime    not null default( getdate() ),

    write_ios   int         not null,

    write_bytes bigint      not null,

    transaction_size int    not null,

    ios_per_row decimal( 10, 3 ) not null,

    ios_per_xact decimal( 10, 3 ) not null,

    bytes_per_io decimal( 10, 3 ) not null,

    mbytes_per_sec decimal( 10, 3 ) not null,

    rows int not null,

    commits int not null,

    duration_seconds int not null

)

 

Now for the test, the is your test harness, if you are going to use your own stuff then I would suggest putting it in another stored procedure and replace the INSERT mytest with a call to your stored procedure.

 

CREATE PROC test_perf

    @test_name varchar(100),

    @tran_batch_size int

AS

BEGIN

    SET NOCOUNT ON

 

    IF @@TRANCOUNT > 0

        ROLLBACK            --  Make sure no transaction open

 

    WAITFOR DELAY '00:00:05'    --  System settle, helps seperate stuff in PERFMON

 

    TRUNCATE TABLE mytest

 

    CHECKPOINT                  --  Write any dirty pages to disk

 

    DBCC DROPCLEANBUFFERS       --  Empty data cache

    DBCC FREEPROCCACHE          --  Empty execution cache

 

    ALTER DATABASE IOMeter SET RECOVERY SIMPLE

    CHECKPOINT

    ALTER DATABASE IOMeter SET RECOVERY FULL

 

    WAITFOR DELAY '00:00:05'    --  System settle, helps seperate stuff in PERFMON

 

    DECLARE @i int

    DECLARE @c int

    SET @i = 1

    SET @c = 0

 

    SELECT 'START' AS op, GETDATE() AS op_time, *

    INTO #filestats

    FROM ::fn_virtualfilestats( db_id(), 2 )

 

    BEGIN TRAN

   

    WHILE @i <= 100000

    BEGIN

        INSERT mytest ( somedata ) VALUES( CAST( @i AS char(4000) ) )

 

        IF @i % @tran_batch_size = 0

        BEGIN

            SET @c = @c + 1

            COMMIT TRAN

            IF @i < 100000

                BEGIN TRAN

        END

 

        SET @i = @i + 1

    END

    IF @@TRANCOUNT > 0

        COMMIT TRAN

 

    INSERT #filestats (

        op, op_time, DbId, FileId, TimeStamp, NumberReads, BytesRead,

        IoStallReadMS, NumberWrites, BytesWritten,

        IoStallWriteMS, IoStallMS, BytesOnDisk, FileHandle )

    SELECT 'END', getdate(), DbId, FileId, TimeStamp, NumberReads, BytesRead,

        IoStallReadMS, NumberWrites, BytesWritten,

        IoStallWriteMS, IoStallMS, BytesOnDisk, FileHandle

    FROM ::fn_virtualfilestats( db_id(), 2 )

 

    INSERT io_results (

        test_name,

        write_ios,

        write_bytes,

        transaction_size,

        ios_per_row,

        ios_per_xact,

        bytes_per_io,

        mbytes_per_sec,

        rows,

        commits,

        duration_seconds )

    SELECT @test_name,

           Write_IOs   = e.NumberWrites - s.NumberWrites,

           Write_Bytes = e.BytesWritten - s.BytesWritten,

           transaction_size = @tran_batch_size,

           IOs_Per_Row = CAST( (e.NumberWrites - s.NumberWrites) AS decimal( 10, 3 ) ) / 100000,

           IOs_Per_Xact= CAST( (e.NumberWrites - s.NumberWrites) AS decimal( 10, 3 ) ) / @c,

           Bytes_Per_IO= CAST( ( e.BytesWritten - s.BytesWritten ) AS decimal( 20, 3 ) ) / (e.NumberWrites - s.NumberWrites),

           MBytes_Per_Sec = ( CAST( ( e.BytesWritten - s.BytesWritten ) AS decimal( 20, 3 ) ) / DATEDIFF( second, s.op_time, e.op_time ) ) / 1048576,

           Rows        = (SELECT COUNT(*) FROM mytest),

           Commits     = @c,

           duration_seconds = datediff( second, s.op_time, e.op_time )

    FROM #filestats s

        CROSS JOIN #filestats e

    WHERE s.op = 'START'

      AND e.op