8

The table:

CREATE TABLE [dbo].[Session] (
    [SessionId]          UNIQUEIDENTIFIER NOT NULL,
    [CID]                INT              NULL,
    [DEST]               VARCHAR (50)     NOT NULL,
    [EndUser]            VARCHAR (50)     NULL,
    [Platform]           VARCHAR (5)      CONSTRAINT [DF_Session_Platform] DEFAULT ('WEB') NOT NULL,
    [SessionState]       VARCHAR (50)     NOT NULL,
    [ServerName]         VARCHAR (180)    NOT NULL,
    [StartDate]          DATETIME         CONSTRAINT [DF_Session_StartDate] DEFAULT (getutcdate()) NOT NULL,
    [UpdateDate]         DATETIME         NULL,
    [EndDate]            DATETIME         NULL,
    [SessionData]        XML              NULL,
    [StartMinute]        AS               (dateadd(minute,datediff(minute,(0),[StartDate]),(0))) PERSISTED,
    [ActivityBreadcrumb] NVARCHAR (MAX)   NULL,
    CONSTRAINT [PK_Session] PRIMARY KEY CLUSTERED ([StartDate] ASC, [SessionId] ASC)
);
GO

CREATE NONCLUSTERED INDEX [IX_Session_StartDate_EndDate]
    ON [dbo].[Session]([StartDate] ASC, [EndDate] ASC);
GO

CREATE NONCLUSTERED INDEX [IX_Session_CID_DEST]
    ON [dbo].[Session]([CID] ASC, [DEST] ASC, [StartDate] ASC, [SessionId] ASC)
    INCLUDE([Platform]);
GO

CREATE PRIMARY XML INDEX [IX_Session_SessionData]
    ON [dbo].[Session]([SessionData])
    WITH (PAD_INDEX = OFF);
GO

CREATE XML INDEX [IX_Session_SessionData_PROPERTY]
    ON [dbo].[Session]([SessionData])
    USING XML INDEX [IX_Session_SessionData] FOR PROPERTY
    WITH (PAD_INDEX = OFF);
GO

CREATE NONCLUSTERED INDEX [IX_StartMinute]
    ON [dbo].[Session]([StartMinute] DESC, [EndDate] ASC);
GO

CREATE NONCLUSTERED INDEX [IX_Session_CID_SessionId]
    ON [dbo].[Session]([CID] ASC, [SessionId] ASC);
GO

CREATE STATISTICS [ST_Session_StartDate_SessionId_CID]
    ON [dbo].[Session]([StartDate], [SessionId], [CID]);
GO

CREATE STATISTICS [ST_Session_SessionId_CID_DEST]
    ON [dbo].[Session]([SessionId], [CID], [DEST]);

The procedure that caused problems:

CREATE PROCEDURE [dbo].[SessionUpdate] 
    @SessionId uniqueidentifier, 
    @CID int,
    @DEST varchar(50),
    @EndUser varchar(50) = NULL,
    @Platform varchar(5) = NULL,
    @SessionState varchar(50),
    @ServerName varchar(180) ,
    @StatusDtm datetime,
    @EndDtm datetime,
    @Data xml,
    @ActivityBreadcrumb nvarchar(max) = NULL
AS
BEGIN

    SET NOCOUNT ON;

    MERGE AppLog.dbo.[Session] as target
    USING (Select @SessionId, @CID, @DEST, @EndUser, @Platform, @SessionState, @ServerName, @StatusDtm, @EndDtm, @Data, @ActivityBreadcrumb) as source (SessionId, CID, DEST, EndUser, [Platform], SessionState, ServerName, StatusDtm, EndDtm, Data, ActivityBreadcrumb)
    ON (target.SessionID = source.SessionId)
    WHEN MATCHED THEN
        Update Set target.UpdateDate = Source.StatusDtm,
            target.CID = coalesce(Source.CID, target.CID),
            target.Platform = coalesce(Source.Platform, target.Platform),
            target.SessionState = coalesce(Source.SessionState,Target.SessionState),
            target.SessionData = source.Data,
            target.ActivityBreadcrumb = source.ActivityBreadcrumb
    WHEN NOT MATCHED THEN
        Insert Values(Source.SessionId, Source.CID, Source.DEST, Source.EndUser, Source.[Platform], Source.SessionState, Source.ServerName, Source.StatusDtm, null, Source.EndDtm, source.Data, source.ActivityBreadcrumb);

END

The above procedure seemed to run fine for years. Scheduled queries remove year old data nightly. The application experienced a large amount of volume for a few days and we started seeing some blocking during that time. The issue persisted after the volume receded to normal levels and even below normal levels.

Rows were not getting inserted into the table for 10 to 20 minutes. This is what we see when we looked at blocking queries during that time: blocking queries

We made changes to indexes, the procedure and changed the how the application updates the table. The problem no longer happens. The proc now just inserts a row without updating:

CREATE PROCEDURE [dbo].[SessionUpdate] 
    @SessionId uniqueidentifier, 
    @CID int,
    @DEST varchar(50),
    @EndUser varchar(50) = NULL,
    @Platform varchar(5) = NULL,
    @SessionState varchar(50),
    @ServerName varchar(180) ,
    @StatusDtm datetime,
    @EndDtm datetime,
    @Data xml,
    @ActivityBreadcrumb nvarchar(max) = NULL
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO Session (SessionId, CID, DEST, EndUser, [Platform], SessionState, ServerName, StartDate, EndDate, SessionData, ActivityBreadcrumb)
    VALUES (@SessionId, @CID, @DEST, @EndUser, @Platform, @SessionState, @ServerName, @StatusDtm, @EndDtm, @Data, @ActivityBreadcrumb)
END

We suspect the reason the blocking was happening was due to statistics becoming old and the procedure recompiling. It looked like all of the procedure calls were waiting on one procedure to finish compiling. Here is the lock data:

<Database name="AppLog">
<Locks>
   <Lock request_mode="S" request_status="GRANT" request_count="1" />
</Locks>
<Objects>
   <Object name="SessionUpdate" schema_name="dbo">
      <Locks>
         <Lock resource_type="OBJECT" request_mode="Sch-S" request_status="GRANT" request_count="1" />
         <Lock resource_type="OBJECT.COMPILE" request_mode="X" request_status="WAIT" request_count="1" />
      </Locks>
   </Object>
</Objects>
</Database>

Does that sound correct?

I would like to understand better what was happening to avoid this situation again.

For example, does the new procedure no longer recompile, or if it does, why does it no longer block other calls?

If a call to the new procedure does cause it to recompile, and it takes 10-20 minutes to finish, does the insert still happen after the application has closed the connection due to a timeout?

Should we worry about other queries that use this table? Could they timeout due to statistics being updated?

6
  • 1
    Nice detailed question! I didn't read too deep since you're using MERGE which has been known to have a lot of issues - I'd try writing out an UPSERT yourself and see if that works better, and just avoid using MERGE as a default approach. Commented Aug 5, 2021 at 23:31
  • 1
    Check the database parameter Auto Update Statistics Asynchronously. If it's disabled optimizer could wait until the statistics would be updated. Commented Aug 6, 2021 at 6:26
  • 1
    Regarding the behavior after a timeout, a client timeout sends an attention request to SQL Server which cancels the executing query so the insert will not be performed.
    – Dan Guzman
    Commented Aug 6, 2021 at 10:29
  • 1
    You may want to have a look at Troubleshoot blocking issues caused by compile locks to see if any of these scenarios apply to you. You should also check out this post to see if you're hitting an issue with the security cache, which can cause similar problems. Commented Aug 6, 2021 at 14:42
  • Thank you @DenisRubashkin , I will do that. I would have thought that is the default for that setting.
    – sean
    Commented Aug 6, 2021 at 15:09

1 Answer 1

0

Not a full answer, but with information presented it's impossible to answer definetly.

I doubt this is a problem with statistics update and recompile. At least this wouldn't be my first guess. First thing I notice is that the MERGE joins on target.SessionID = source.SessionId This means you need to have an index on SessionID to do a effective seek. The DDL provided contains no index where first column is SessionID. The best thing SQL Server can do in this situation is to scan an index. Index scan is horrible for concurrency. It will block others from accessing the table. It can also go parallel, if the index is sufficently big. In this case we can see both: plenty of long lock waits and the blocking session has latch ACCESS_METHODS_DATASET_PARENT indicating parallel work. Now statistics update can go parallel too, but only since SQL Server 2016 and for databases in compatibility level 130. Source

Starting with SQL Server 2016 (13.x), sampling of data to build statistics is done in parallel, when using compatibility level 130, to improve the performance of statistics collection. The query optimizer will use parallel sample statistics, whenever a table size exceeds a certain threshold.

You should definetly check what compatibility level your database is on. Secondly: by default statistics are sampled, unless you force a full scan. Given that the MERGE does probably index scan already, I would be surprised, if additional table sampling added significant amount to query runtime. Not stating that this cannot happen. Just with given information, I would expect the missing index on SessionID to cause the problems.

1
  • Thank you for taking the time to look at my problem. You are correct that this table sorely needed an index on SessionID and we have since added one. But this was after we fixed the blocking issue with the change proc. The lock information on that top row (session_id=172) is full of: resource_type="METADATA.INDEXSTATS" and METADATA.STATS. Including a bunch on the Primary and Secondary XML index. We have also removed the secondary XML index. The XML we store in that column is large.
    – sean
    Commented Aug 11, 2021 at 21:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.