While I was maintaining one of our larger projects the other day, I came across something like this in an update trigger. I've anonymized the table and field names, but otherwise the logic is unaltered:
declare @id int, @val_old varchar(10), @val_new varchar(10) declare ChangeCursor cursor for select new.id, new.val, old.val from deleted old join inserted new on new.id = old.id open ChangeCursor fetch next from ChangeCursor into @id, @val_new, @val_old while @@fetch_status = 0 begin if ( @val_new <> @val_old ) begin raiserror( 'The value may not be changed for id %d; old value: %s, new value: %s', 16, 1, @id, @val_old, @val_new ) close ChangeCursor deallocate ChangeCursor return end fetch next from ChangeCursor into @id, @val_new, @val_old end close ChangeCursor deallocate ChangeCursor
Quick, what does this code do? My eyes skipped over all the cursor management boilerplate and went straight for the text of the RAISERROR, which explained in a single sentence what took about 30 lines of SQL to accomplish. Now, before I get into my suggestions for how this could be rewritten to be both faster and more readable, I'd like to point out that this trigger doesn't make the common beginner mistake of assuming that the INSERTED and DELETED tables have only one row each. Triggers fire once per triggering statement, not once per row, and this code was written to handle that case. However, I still have a beef with this code, because it uses a cursor, which does two things that I don't like: perform row-based logic instead of set-based logic, and increase the ratio of boilerplate code to actual logic, which reduces readability.
Let's try rewriting this trigger using set-based logic and compare the results. All the above code does is loop through a result set until it finds a row where a certain field changed, uses the current row's values to report an error, and exits the trigger. If you're scratching your head wondering if simply raising an error in a trigger is enough to abort the operation, the answer is no. This particular trigger was written to be called by C# code that would open a transaction, execute the update, and then either commit or rollback depending on whether any exceptions occurred. Purely as an aside, I don't prefer this pattern; I prefer to keep my transactions in stored procedures. My opinion is that T-SQL is a data manipulation language and C# is a general purpose language, so when there is data manipulation to be done, the more specialized language should be the first pick over the general purpose one. But anyway, back to the rewrite! The rewritten code should:
declare @id int, @val_new varchar(10), @val_old varchar(10) select top 1 @id = new.id, @val_new = new.val, @val_old = old.val from deleted old join inserted new on new.id = old.id where new.val <> old.val if ( @@rowcount > 0 ) begin raiserror( 'The value may not be changed for id %d; old value: %s, new value: %s', 16, 1, @id, @val_old, @val_new ) return end
This version is shorter mainly due to the removal of the boilerplate cursor code, but the result is not only just as fast or faster, but also much more quickly understood because the noise to logic ratio has been greatly reduced.
Remember Me
a@href@title, i, strike, u
Copyright © 2003-2008 Falafel Software Inc.
Subscribe to Falafel Blogs
The opinions expressed herein are Falafel's employees own personal opinions and do not represent Falafel Software's view in any way in case they go bananas!