File this under bad idea thong
In the tradition of Klaus Aschenbrenner wanting to prevent people from writing SELECT * queries, I thought it might be nice to prevent people from running deletes or updates without a WHERE clause.
In a vaguely scientific way.
Now, I don’t really condone this. It just seemed funny at the time.
There’s no way to intercept a query and check for a WHERE clause, but we can… Aw, hell, let’s just do the demo.
Here’s a table, and a 100 row insert.
Why 100 rows? Because I’m awful at math and using bigger numbers would confuse me.
CREATE TABLE dbo.TriggerTest ( Id INT NOT NULL PRIMARY KEY CLUSTERED, DateCol DATETIME, StringCol VARCHAR(50) ); GO INSERT dbo.TriggerTest ( Id, DateCol, StringCol ) SELECT r, DATEADD(DAY, r, GETDATE()), 'A' FROM (SELECT TOP 100 ROW_NUMBER() OVER (ORDER BY @@SPID) AS r FROM sys.messages AS m) AS x GO
And uh, here’s a trigger. Oh boy.
CREATE OR ALTER TRIGGER dbo.UseAWhereClauseNextTime ON dbo.TriggerTest FOR UPDATE, DELETE AS BEGIN DECLARE @rc INT = @@ROWCOUNT; DECLARE @table_rows INT = ( SELECT row_count FROM sys.dm_db_partition_stats WHERE object_id = OBJECT_ID('dbo.TriggerTest') AND index_id IN ( 0, 1 )); IF EXISTS ( SELECT 1 FROM Inserted ) AND EXISTS ( SELECT 1 FROM Deleted ) BEGIN IF (( @rc * 100 ) / ( @table_rows )) >= 98 BEGIN RAISERROR('USE A WHERE CLAUSE DUMMY', 0, 1) WITH NOWAIT; ROLLBACK; END; END; IF NOT EXISTS ( SELECT 1 FROM Inserted ) AND EXISTS ( SELECT 1 FROM Deleted ) IF ( ( @table_rows = 0 ) OR ( @table_rows > 0 AND (( @rc * 100 ) / ( @table_rows + @rc )) >= 98 )) BEGIN RAISERROR('USE A WHERE CLAUSE DUMMY', 0, 1) WITH NOWAIT; ROLLBACK; END; END; GO
If you’re looking at the code, and you hate me, I don’t blame you.
We have an after trigger that takes the row count of the update or delete, and checks it against the number of rows in the table.
If the number of rows affected is within 98% of the rows in the table, the transaction is rolled back, and our end user is reminded to use a WHERE clause.
Why 98%? Well, system views and functions aren’t guaranteed to be up to the second reliable, and I needed to pick a number.
There’s no mechanism inside of triggers to tell you if you inserted, updated, or deleted rows, so when you write a trigger to handle more than one action, the only way to tell what happened is to look at the internal tables.
If there’s stuff in inserted and deleted, it’s an update. If there’s just stuff in one or the other, it’s… one or the other.
Does it work?
Like a charm!
If a charm had no charm whatsoever.
UPDATE t SET t.DateCol = DATEADD(DAY, 1, t.DateCol) FROM dbo.TriggerTest AS t GO DELETE t FROM dbo.TriggerTest AS t GO
Results in:
USE A WHERE CLAUSE DUMMY Msg 3609, Level 16, State 1, Line 45 The transaction ended in the trigger. The batch has been aborted. USE A WHERE CLAUSE DUMMY Msg 3609, Level 16, State 1, Line 50 The transaction ended in the trigger. The batch has been aborted.
As we can see here: Your Mom.
But these two queries run just fine.
UPDATE t SET t.DateCol = DATEADD(DAY, 1, t.DateCol) FROM dbo.TriggerTest AS t WHERE t.Id > 3 DELETE TOP (97) t FROM dbo.TriggerTest AS t
Why? They’re only 97% of the table.
Only.
Because that’s cool, right?
Is there a downside?
Hell yeah. If you’ve got really big tables, the rollback can take a while.
Rollbacks are single threaded, dontcha know?
And there’ll be blocking.
But it might be better than having to restore the whole database.
Maybe.
But what if I need to hit the whole table?
Batch your modifications, you animal.
Where am I wrong?
Brent and Tara really kicked the tires hard on this one for, and I appreciate it.
My half baked idea would have be cold and soggy without them.
Now you, dear reader, get to tell me where I’m still wrong.
Thanks for reading!
Are you being paid fairly? Let's find out: the 2018 Data Professional Salary Survey is open.