Tag triggers

Default Values, Triggers, and You

A friend of mine sent me an email the other day asking about default values in SQL Server. I realized that I’ve had to think about this a few times over the years and I’ve been asked about it more than once, too.

Setup

We need a table first, right? We’ll also want a few sample rows in there.

CREATE TABLE Employees (
  emp_id INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
  emp_name varchar(50) NULL);
GO

INSERT INTO Employees (emp_name) VALUES ('a');
INSERT INTO Employees (emp_name) VALUES ('b');
GO

SELECT * FROM Employees;

/*
emp_id      emp_name
----------- --------------------------------------------------
1           a
2           b
*/

How Defaults Work

So far we just have two rows in our two column table. It’s pretty boring. Let’s add a default value:

ALTER TABLE Employees
ADD last_modified DATETIME NULL CONSTRAINT Employees_last_modified
DEFAULT CURRENT_TIMESTAMP;
GO

We might as well add some new rows while we’re having fun with our employees, right?

INSERT INTO Employees (emp_name) VALUES ('c');
INSERT INTO Employees (emp_name) VALUES ('d');

What’s it look like now?

SELECT * FROM Employees;

/*
emp_id      emp_name                                           last_modified
----------- -------------------------------------------------- -----------------------
1           a                                                  NULL
2           b                                                  NULL
3           c                                                  2010-12-06 18:21:37.787
4           d                                                  2010-12-06 18:21:37.787
*/

Hold up. Employees 1 and 2 don’t have a last_modified value. Why not? Well, that’s because we’ve told SQL Server that our last_modified column can allow NULLs. They’re allowable in our table. If we wanted to automatically provide a default value when we added the constraint, we could do so by specifying the datatype as DATETIME NOT NULL. A best practice would be to add the column as a NULLable data type, add a value for all NULL rows, and set the column to NOT NULL.

If we do want to update a NULLable column and set it to the default value, we just issue an update using the DEFAULT keyword for the value. If that makes no sense, perhaps this example will help:

UPDATE Employees
SET last_modified = DEFAULT
WHERE last_modified IS NULL ;

What About Updates?

UPDATE Employees
SET emp_name = 'zzz'
WHERE emp_id = 3;

SELECT *
FROM Employees
WHERE emp_id = 3;

/*
emp_id      emp_name                                           last_modified
----------- -------------------------------------------------- -----------------------
3           zzz                                                2010-12-06 18:21:37.787
*/

As you can see, when we update employee 3, it doesn’t change the value of last_modified. That’s because the default value is only set on insert. We could specify DEFAULT in our UPDATE statement, but then we’d need to specify that every time we update the table. What can we do?

The Answer is Triggers

That’s right: triggers. If we want to track the modification timestamp of an object in the database, we need to use a trigger to keep things updated:

CREATE TRIGGER TR_Employees$AfterUpdate ON dbo.Employees
AFTER UPDATE
AS
BEGIN
  UPDATE  e
  SET     e.last_modified = CURRENT_TIMESTAMP
  FROM    dbo.Employees e
          JOIN inserted i ON e.emp_id = i.emp_id;
END
GO

UPDATE Employees
SET emp_name = 'asdf'
WHERE emp_id = 1;

SELECT * FROM Employees;

/*
emp_id      emp_name                                           last_modified
----------- -------------------------------------------------- -----------------------
1           asdf                                               2010-12-06 18:34:04.340
2           b                                                  NULL
3           zzz                                                2010-12-06 18:21:37.787
4           d                                                  2010-12-06 18:21:37.787
*/

And that, my friends, is how we keep a modification timestamp up to date.

Links for the Week of 2009.06.12

SQL Server

Denis Gobo provides a double dose of dynamic SQL hotness via Changing exec to sp_executesql doesn’t provide any benefit if you are not using parameters correctly and Avoid Conversions In Execution Plans By Using sp_executesql Instead of Exec. Following his tips in here will net you some considerable benefits in how your dynamic SQL behaves and performs in your production applications. The best part is that he provides a huge amount of example code to back up everything he’s saying.

Free SQL Server tools that might make your life a little easier Mladen Prajdic maintains an amazing list of free tools to help data folks get their job done. SSMS Tools Pack (which Mladen created and maintains) is incredibly helpful. I suspect that if he finds these other tools useful, they’ll probably save you many many hours over the years to come.

SQL Server Troubleshooting Tips and Tricks This isn’t a blog post so much as it is really important to keep around. It’s just a list of tips and tricks to keep things easy and painless with SSMS. If you didn’t check out the pain of the week webcast with Michelle Ufford and Brent Ozar on SSMS, you should do so when you get the chance, it’s well worth it!

More SQL Server Training Videos Training. Free. Videos. I’m in there, that’s really all the reason you need to visit that link.

BONUSTen SQL Server Functions That You Have Ignored Until Now Bonus link from Denis Gobo, once again, covering some great, little known, SQL Server functions.

Development

Unit Testing is Not a “Figure It Out Later” Read this. Read this now. Do it. We’ll still be here. Stop what you’re doing and read this. I cannot stress how important it is for you to read this, even us data folks. If I could put a blink tag around this paragraph, I would. (Thanks to Stephen Wright for finding this link.)

Defensive Programming – Assumptions Must be Guaranteed or Tested Aaron Alton blogs about the importance of defensive programming. While this is specifically from a SQL Server standpoint, it’s important no matter which language you’re using. In fact, Aaron proves an important point – you can put any development practice to use in any language.

Stuff & Things

Improving your intranet – keep it sustainable using kaizen – Patrick Walsh talks about using kaizen to produce evolutionary improvements to an intranet. You could do this with anything, really, but it’s a good read either way you look at it.

I cheated and stole the last two links from Lifehacker, but they’re just too good to not share.

CopyPasteCharacter.com Makes Special Characters Easy to Paste Yup, easy. Nice. Easy mode: engage.

LiberKey Installs 200+ Portable Applications I have too many thumbdrives laying around the house. Now I have a use for them.

Mirroring table changes through DDL triggers

Let’s say that we have an application that tracks and reports customer bills. Customers are able to view reports based on their bill. As time goes on, the customers have a need to create adjustments to their bill. It should be simple enough to add some identifying data to the bill to determine which bill is an adjustment, right?

Well, not quite. It turns out that the Bill table is being loaded automatically on a regular (hourly) basis by an SSIS package. If a modification comes through the SSIS package, the existing records are removed from the Bill table and a new record is inserted. Changing this process to account for adjustments from the web would require additional development time and slow down the load process.

So, if we can’t add new records to the Bill table, how should we store the adjusted records? We determine that the best method is to put them in a second table that will mirror the columns in the Bill table. This table will be on a separate filegroup to allow for frequent back ups.

A new table is created with the exact same columns and the word ‘Web’ is appended to the table name (these are web adjustments). Additionally, a cross-reference table is created between the two with a unique constraint on the BillId and WebBillId columns, thus ensuring that only one adjustment can be associated with one bill.

So, what happens when we need to add an additional column, index, or constraint to the Bill table? The exact same changes need to be applied to the BillWeb table. If they aren’t, there may be performance differences, data integrity problems, or an important column might not get added and then our reports won’t be accurate or else won’t run at all. This presents a problem. Customers want to be able to modify all of the fields in the billing tables and we need to make sure that they can. Short of being very careful every time we make a change, what other option is available to us?

DDL triggers!

DDL (data definition language) triggers are triggers that fire when database level events occur – a table is added, removed, columns are added, an index is altered, etc. In SQL Server 2005 it’s possible to trap and respond to these events.

I went ahead and scripted this out and tested it on my local machine. Everything worked. Special thanks to Brent Ozar for reviewing this code.

So, without further ado, here is the DDL trigger to modify a mirrored table when changes are made to the source table. Feel free to hit me up in the comments with any questions.

-- Create pvt and pvtWeb as test tables
CREATE TABLE [dbo].[pvt](
   [VendorID] [int] NULL,
   [Emp1] [int] NULL,
   [Emp2] [int] NULL,
   [Emp3] [int] NULL,
   [Emp4] [int] NULL,
   [Emp5] [int] NULL
) ON [PRIMARY];
GO

CREATE TABLE [dbo].[pvtWeb](
   [VendorID] [int] NULL,
   [Emp1] [int] NULL,
   [Emp2] [int] NULL,
   [Emp3] [int] NULL,
   [Emp4] [int] NULL,
   [Emp5] [int] NULL
) ON [PRIMARY];
GO

IF EXISTS(SELECT * FROM sys.triggers WHERE name = 'ddl_trigger_pvt_alter')
   DROP TRIGGER ddl_trigger_pvt_alter ON DATABASE;
GO

-- Create a trigger that will trap ALTER TABLE events
CREATE TRIGGER ddl_trigger_pvt_alter
ON DATABASE
FOR ALTER_TABLE
AS
  DECLARE @data XML;
  DECLARE @tableName NVARCHAR(255);
  DECLARE @newTableName NVARCHAR(255);
  DECLARE @sql NVARCHAR(MAX);

  SET @sql = '';
  -- Store the event in an XML variable
  SET @data = EVENTDATA();

  -- Get the name of the table that is being modified
  SELECT @tableName = @data.value('(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(255)');
  -- Get the actual SQL that was executed
  SELECT @sql = @data.value('(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]', 'NVARCHAR(MAX)');

  -- Figure out the name of the new table
  SET @newTableName = @tableName + 'Web';

  -- Replace the original table name with the new table name
  -- str_replace is from Robyn Page and Phil Factor’s delighful post on
  -- string arrays in SQL. The other posts on string functions are indispensible
  -- to handling string input
  --
  -- http://www.simple-talk.com/sql/t-sql-programming/tsql-string-array-workbench/
  -- http://www.simple-talk.com/sql/t-sql-programming/sql-string-user-function-workbench-part-1/
  -- http://www.simple-talk.com/sql/t-sql-programming/sql-string-user-function-workbench-part-2/
  SET @sql = dbo.str_replace(@tableName, @newTableName, @sql);

  -- Debug the SQL if needed.
  --PRINT @sql;

  IF OBJECT_ID(@newTableName, N'U') IS NOT NULL
  BEGIN
    BEGIN TRY
      -- Now that the table name has been changed, execute the new SQL
      EXEC sp_executesql @sql;
    END TRY
    BEGIN CATCH
      -- Rollback any existing transactions and report the full nasty
      -- error back to the user.
      IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;

      DECLARE @ERROR_SEVERITY INT,
              @ERROR_STATE    INT,
              @ERROR_NUMBER   INT,
              @ERROR_LINE     INT,
              @ERROR_MESSAGE  NVARCHAR(4000);

      SELECT @ERROR_SEVERITY = ERROR_SEVERITY(),
             @ERROR_STATE    = ERROR_STATE(),
             @ERROR_NUMBER   = ERROR_NUMBER(),
             @ERROR_LINE     = ERROR_LINE(),
             @ERROR_MESSAGE  = ERROR_MESSAGE();

      RAISERROR('Msg %d, Line %d, :%s',
                @ERROR_SEVERITY,
                @ERROR_STATE,
                @ERROR_NUMBER,
                @ERROR_LINE,
                @ERROR_MESSAGE);
    END CATCH
  END
GO

This site is protected with Urban Giraffe's plugin 'HTML Purified' and Edward Z. Yang's Powered by HTML Purifier. 219 items have been purified.