Why triggers sometimes suck

We took our annual trip Up North this last week. My son really wanted to get his first walleye this year, and I did everything I could to make it happen. The lake we were on is certainly not a “numbers” lake for any species except dinky bluegill. However, our second-to-last night we did manage to hook up with a great 24″ walleye. In many ways, seeing the thrill on kids’ faces as they catch a big fish is even more fun that catching the fish yourself.
Nate Walleye

Triggers are an extremely useful tool in SQL Server. However, I mostly hate them. Like any tool, they have a proper place and are quite effective when used properly. Unfortunately, in actual use they are more often than not used improperly.

Homer Fixes Camera

Tools used improperly

I’m not saying many third party databases have been created by Homer Simpson, but for some of them, it wouldn’t surprise me.  I came across this “interesting” setup while investigating a deadlocking issue on a third party vendor’s database:

CREATE TABLE [cwi].[dwtran]
    [ibcomp] [DECIMAL](3, 0) NOT NULL
  , [ibponr] [DECIMAL](12, 0) NOT NULL
  , [ibe2vn] [VARCHAR](3) NOT NULL
  , [ibkggm] [VARCHAR](80) NOT NULL
  , [ibhody] [DECIMAL](7, 0) NOT NULL
  , [ibectm] [DECIMAL](6, 0) NOT NULL
  , [ibamry] [VARCHAR](1) NOT NULL
  , [ibhpdy] [DECIMAL](7, 0) NOT NULL
  , [ibedtm] [DECIMAL](6, 0) NOT NULL
  , [ibhqdy] [DECIMAL](7, 0) NOT NULL
  , [ibeetm] [DECIMAL](6, 0) NOT NULL
  , [ibkncf] [VARCHAR](10) NOT NULL
  , [id] [INT] IDENTITY(1, 1) NOT NULL

CREATE TRIGGER [cwi].[assign_key]
ON cwi.dwtran
    UPDATE dw
    SET dw.ibponr = id
    FROM cwi.dwtran dw
        INNER JOIN Inserted i ON i.id = dw.id;

What is this trigger doing? It’s grabbing the id identity value for the row that just got inserted and setting the ibponr column to the exact same value.  This setup would probably work ok on a system that always accesses the table serially.  However, this table is a change history table that captures all transactions in a busy order management system for insertion into a data warehouse.  There are several application servers accessing the database at the same time.  The trigger was deadlocking with subsequent INSERTS causing transactions to fail.  Additionally, each insert into the table also required an update, effectively costing two transactions for a single insert.

In this case, since the incremental data warehouse load only ran once per day, I was able to create a SQL Agent job that update the missing ibponr records in a single update.  I ran the SQL Agent job right before the data warehouse load.  In a perfect world, the data warehouse load (which is run by the application, not in the database) would update those ibponr values as its first step.

Another reason I dislike triggers is that when they cause a rollback, the error can be vague and the reason for the rollback is often hard to find.

One last thing to keep in mind from a development perspective is that triggers have to be written from the perspective that multiple rows can be inserted simultaneously.  The trigger I showed above assumes that only one row will be inserted at a time, but that isn’t always the case!


Addressing login trigger failures in SQL Server

As I get older I have come to enjoy watching others fish, especially my children.  The thrill of catching a big fish is magnified by seeing the smile on someone else’s face when he/she is the one bringing it in.  Below is a nice sized largemouth bass my son caught on a recent fishing trip.

Two Sisters LM.jpg

In my previous post I showed how to create a login trigger to log sysadmin access to a SQL Server instance.  Almost immediately I received a comment describing how the failure of the trigger could almost completely prevent anyone from logging into the instance.  This is a major problem!

The reason this occurs makes sense if you think about it.  While attempting to login, the user executes some code in a trigger.  If that code is invalid, the trigger will fail and abort.  When that happens, the login aborts as well.  What could cause the trigger to fail?  Well, if the table (or other objects) you are accessing within the trigger is inaccessible to the user, or if it doesn’t even exist, the trigger will fail.

I tested this by using my working trigger, which logged sysadmin logins to a table called dbo.sysadminLogging.  Next I renamed the table to dbo.sysadminLogging1.

20170606 Renamed table

Next I tried to login in a new window in SSMS:

20170606 Failed login

First, let’s talk about how to get back into a server that has this issue.  We need to log into the SQL using SQLCMD with a dedicated administrator connection, then disable the trigger:

20170606 Disable trigger

After doing this everyone should now be able to log back into SQL Server as normal.

Now to prevent this type of event from happening, I suggest a small edit to my original trigger.  This edit will make sure the referenced objects are valid.  If not, the trigger does nothing.  It may also be a good idea to send an email to the DBA so they can investigate, and I’ve noted that in the comments.

CREATE TRIGGER [servertrigger_CheckForSysAdminLogin] ON ALL SERVER
       IF OBJECT_ID('DBMaint.dbo.sysadminLogging') IS NULL
               --Possibly send an email to the DBA, indicating the trigger is not working as expected
               GOTO Abort;--Do nothing

        IF IS_SRVROLEMEMBER('sysadmin') = 1
                INSERT  INTO DBMaint.dbo.sysadminLogging
                        ( [Login] , LoginDate )
                VALUES  ( ORIGINAL_LOGIN() , GETDATE() );




This newer version of the trigger should cut down on the chances that this functionality will come back to bite you. Special thanks to james youkhanis for pointing this out.

How to capture database changes with a DDL Trigger

Casting and retrieving big, heavy muskie lures for a full day can actually be quite a bit of work. Lures weighing up to a pound can be tough to cast, and lures with heavy action take a lot of effort to pull through the water. Using such large, heavy gear also requires standing instead of sitting, and (not to sound like a wimp but…) a full day of standing adds to the fatigue.
Not all fishing is such hard work. There is something both satisfying and relaxing about sitting back with your feet up, enjoying a cold beer, and holding a rod waiting to feel the sharp tap of a fish bite. Walleye fishing often provides this type of relaxing leisure time. Walleye are a popular fish to catch due to their taste, though they are also quite challenging. They have a reputation for being a finicky fish and will refuse many different presentations before choosing one to bite. Their bite is also quite subtle, and if you aren’t paying attention you can often miss it. By the time you realize you have a fish on your line, the walleye will have felt the hook and spit out the bait. Another complication is that small panfish will often find and steal your bait before the walleye get a chance. Many a time I have tried to set the hook on what I thought was a good sized walleye, only to pull up a small bluegill, or even worse, a hook with only a small scrap of worm because the bluegills have pulled it apart and eaten the rest.
How nice it would be to have some way to watch and monitor the bait, so it wouldn’t be stolen by smaller fish and we wouldn’t miss the walleye when they do show up. Many database administrators have the same feelings about their databases. We want to be able to see exactly what is changing, when, and who is making those changes. While we try to lock down our environments so most of, if not all, the changes have to go through us, there are often business requirements that force us to give the ability to make minor changes others. This is especially true in lower environments, where developers can often be tasked with creating tables or stored procedures that will be migrated up to production.
Fortunately, SQL Server gives us the ability to capture all DDL changes through the use of a DDL Trigger. DDL stands for Data Definition Language, and it refers to statements that work against objects rather than the data within them. Some examples are DROP TABLE, CREATE STORED PROCEDURE, and ALTER VIEW. You can capture these statements, including who ran them, from where, and at what time through the use of the DDL Trigger. Here’s Microsoft’s explanation on DDL triggers.  The first thing to do is to create a table to hold all the captured events:

CREATE TABLE Sandbox.dbo.DDLEvents
      ID INT IDENTITY(1, 1) ,
      EventDate DATETIME2 ,
      EventType NVARCHAR(100) ,
      EventDDL NVARCHAR(MAX) ,
      EventXML XML ,
      DatabaseName NVARCHAR(MAX) ,
      SchemaName NVARCHAR(255) ,
      ObjectName NVARCHAR(255) ,
      HostName NVARCHAR(255) ,
      IPAddress VARCHAR(20) ,
      ProgramName NVARCHAR(1000) ,
      LoginName NVARCHAR(255)

Next we need to create the actual trigger.

USE [master]
        DECLARE @EventData XML = EVENTDATA();
        DECLARE @ip VARCHAR(32) = ( SELECT  client_net_address
                                    FROM    sys.dm_exec_connections
                                    WHERE   session_id = @@SPID
        INSERT  Sandbox.dbo.DDLEvents
                ( EventDate ,
                  EventType ,
                  EventDDL ,
                  EventXML ,
                  DatabaseName ,
                  SchemaName ,
                  ObjectName ,
                  HostName ,
                  IPAddress ,
                  ProgramName ,
                SELECT  GETDATE() ,
                        @EventData.value('(/EVENT_INSTANCE/EventType)[1]', 'NVARCHAR(100)') ,
                        @EventData.value('(/EVENT_INSTANCE/TSQLCommand)[1]', 'NVARCHAR(MAX)') ,
                        @EventData ,
                        @EventData.value('(/EVENT_INSTANCE/DatabaseName)[1]', 'NVARCHAR(MAX)') ,
                        @EventData.value('(/EVENT_INSTANCE/SchemaName)[1]', 'NVARCHAR(255)') ,
                        @EventData.value('(/EVENT_INSTANCE/ObjectName)[1]','NVARCHAR(255)') ,
                        HOST_NAME() ,
                        @ip ,
                        PROGRAM_NAME() ,

A couple important notes to make on this create statement:

  • This trigger must be created in the master database
  • I added the “WITH EXECUTE AS ‘sa'”. Without this clause, each user who executes a statement that fires the trigger will need permission on everything the trigger uses. Since this is going into a holding table, giving everyone permission for this table would be a huge hassle. It is VERY IMPORTANT to understand if the user/process has insufficient permissions, the entire transaction will be rolled back. This can also be difficult to find as rollbacks due to insufficient permissions within triggers often do not pass out helpful error messages.
  • On the “FOR CREATE_PROCEDURE, ALTER_PROCEDURE,…” you can use this to specify which events should fire the trigger. Maybe you’re only interested in finding out who is dropping tables so you’d only need to add that to the trigger.
  • The IP address of the user/process that is executing the statement had to be retrieved from a separate DMV.

One last step to make after creating the trigger is to enable it:


Now after running a few DDL statements I see them logged in my table:

20141229 DDLEvents Captured
So there is one way to tell who’s doing what DDL in your database. Next time you need to confront one of your “bluegill” teammates, you’ll have proof that he has been messing around with your bait/database.