Quantcast
Channel: SQL Server Blog
Viewing all articles
Browse latest Browse all 1849

Ugly Pragmatism For The Win

$
0
0

Yesterday I had my mind changed about the best way to do concurrency. I describe several methods in Mythbusting: Concurrent Update/Insert Solutions. My preferred method is to increase the isolation level and fine tune locks.

At least that was my preference. I recently changed my approach to use a method that gbn suggested in the comments. He describes his method as the “TRY CATCH JFDI pattern”. Normally I avoid solutions like that. There’s a rule of thumb that says developers should not rely on catching errors or exceptions for control flow. But I broke that rule of thumb yesterday.

By the way, I love the gbn’s description for the pattern “JFDI”. It reminds me of Shia Labeouf’s motivational video.

Okay, I’ll tell you the story.

The Original Defect

So there’s this table. It’s defined something like:

CREATETABLE dbo.AccountDetails(
  Email NVARCHAR(400)NOTNULLCONSTRAINT PK_AccountDetails PRIMARYKEY(Email),
  Created DATETIMENOTNULLCONSTRAINT DF_AccountDetails_Created DEFAULTGETUTCDATE(),
  Etc NVARCHAR(MAX))

And there’s a procedure defined something like:

CREATEPROCEDURE dbo.s_GetAccountDetails_CreateIfMissing(
  @Email NVARCHAR(400),
  @Etc NVARCHAR(MAX))AS 
  DECLARE @Created DATETIME;
  DECLARE @EtcDetails NVARCHAR(MAX);
 
  SETTRANSACTIONISOLATIONLEVEL SERIALIZABLE
  SET XACT_ABORT ONBEGINTRAN 
    SELECT
      @Created = Created,
      @EtcDetails = Etc
    FROM dbo.AccountDetailsWHERE Email = @Email
 
    IF @Created ISNULLBEGINSET @Created =GETUTCDATE();
      SET @EtcDetails = @Etc;
      INSERTINTO dbo.AccountDetails(Email, Created, Etc)VALUES(@Email, @Created, @EtcDetails);
    END 
  COMMIT 
  SELECT @Email as Email, @Created as Created, @EtcDetails as Etc

Applications executing this procedure were deadlocking with each other. If you’re keen, try to figure out why before reading ahead. It’s pretty close to the problem described in the Mythbusting post. Specifically this was method 3: increased isolation level.

Initial Fix

So I decided to fine tune locks. I added an UPDLOCK hint:

CREATEPROCEDURE dbo.s_GetAccountDetails_CreateIfMissing(
  @Email NVARCHAR(400),
  @Etc NVARCHAR(MAX))AS 
  DECLARE @Created DATETIME;
  DECLARE @EtcDetails NVARCHAR(MAX);
 
  SETTRANSACTIONISOLATIONLEVEL SERIALIZABLE
  SET XACT_ABORT ONBEGINTRAN 
    SELECT
      @Created = Created,
      @EtcDetails = Etc
FROM dbo.AccountDetailsWITH(UPDLOCK)WHERE Email = @Email
 
    IF @Created ISNULLBEGINSET @Created =GETUTCDATE();
      SET @EtcDetails = @Etc;
      INSERTINTO dbo.AccountDetails(Email, Created, Etc)VALUES(@Email, @Created, @EtcDetails);
    END 
  COMMIT 
  SELECT @Email as Email, @Created as Created, @EtcDetails as Etc

Bail Early If Possible

Okay, so this solution works. It’s concurrent and it performs just fine. I realized though that I can improve this further by avoiding the transaction and locks. Basically select the row and if it exists, bail early:

CREATEPROCEDURE dbo.s_GetAccountDetails_CreateIfMissing(
  @Email NVARCHAR(400),
  @Etc NVARCHAR(MAX))AS 
  DECLARE @Created DATETIME;
  DECLARE @EtcDetails NVARCHAR(MAX);
 
SELECT    @Created = Created,    @EtcDetails = EtcFROM dbo.AccountDetailsWHERE Email = @Email; IF(@Created ISNOTNULL)BEGINSELECT @Email as Email, @Created as Created, @EtcDetails as Etc;RETURN;END 
  SETTRANSACTIONISOLATIONLEVEL SERIALIZABLE
  SET XACT_ABORT ONBEGINTRAN 
    SELECT
      @Created = Created,
      @EtcDetails = Etc
    FROM dbo.AccountDetailsWITH(UPDLOCK)WHERE Email = @Email;
 
    IF @Created ISNULLBEGINSET @Created =GETUTCDATE();
      SET @EtcDetails = @Etc;
      INSERTINTO dbo.AccountDetails(Email, Created, Etc)VALUES(@Email, @Created, @EtcDetails);
    END 
  COMMIT 
  SELECT @Email as Email, @Created as Created, @EtcDetails as Etc;

Take A Step Back

Okay, this is getting out of hand. The query shouldn’t have to be this complicated.
Luckily I work with a guy named Chris. He’s amazing at what he does. He questions everything without being a nitpicker (there’s a difference). He read through the Mythbusters post and followed all the links in the comments. He asked whether gbn’s JFDI pattern wasn’t better here. So I implemented it just to see what that looked like:

CREATEPROCEDURE dbo.s_GetAccountDetails_CreateIfMissing(
  @Email NVARCHAR(400),
  @Etc NVARCHAR(MAX))ASBEGINTRYINSERTINTO dbo.AccountDetails Email, Etc
    SELECT @Email, @Etc
    WHERENOTEXISTS(SELECT*FROM dbo.AccountDetailsWHERE Email = @Email );
  ENDTRYBEGINCATCH-- ignore duplicate key errors, throw the rest.IF ERROR_NUMBER()<>2601 THROW; 
  ENDCATCH 
  SELECT Email, Created, Etc
  FROM dbo.AccountDetailsWHERE Email = @Email;

Look at how much better that looks! No elevated transaction isolation levels. No query hints. The procedure itself is half as long as it used to be. The SQL is so much simpler and for that reason, I prefer this approach. I am happy in this case to use error handling for control flow.

So I checked in the change and updated my pull request. Chris’s last comment before he approved the pull request was “Looks good. Ugly pragmatism FTW.”


Viewing all articles
Browse latest Browse all 1849

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>