1

I was working mostly on PostgreSQL, but recently I was assigned to project with SqlServer and I encountered very strange behavior of this engine. I am using transaction in my code and connect with server via System.Data.SqlClient library. The code in transaction is approximately 1000 lines long, so I would like to not copy it here, but transaction is handled via code below:

using (var transaction = connection.BeginTransaction(IsolationLevel.ReadCommited)) 
{
    //here code goes
    //1. inserting new table metadata via inserts and updates 
    //2. creating new tables according to users project
    //3. post execute actions via inserts and updates
    
    //here is intended transaction freeze
    await Task.Delay(1000 * 60 * 2);
}

During this execution I cannot perform any operation on database (query execution in SSMS or some code execution in application - doesn't matter). Simple selects f.e. SELECT * FROM "TableA" hangs, retrieving database properties in SSMS hangs etc. Any independent query waits for this one transaction to be completed.

I found several articles and answers here on SO, and based on those I tried following solutions:

  1. Use WITH (NOLOCK) or WITH (READPAST) in SELECT statement
  2. Changing database property Is Read Commited Snapshot ON to true
  3. Changing transaction isolation level in code (all possible levels were tested)

None of the above solutions works. I tested on 3 different computers: desktop, two laptops - the same behavior (SqlServer and SSMS was installed with default options).

In this thread: Understanding locking behavior in SQL Server there are some explanation of transaction isolation levels and locks but the problem is that WITH (NOLOCK) doesn't work for me as mentioned in 1).

This is very big problem for me, because my asynchronous application works synchronously because of that weird locks. Oracle and postgres databases works perfectly fine, the problem concerns SqlServer only.

I don't use EntityFramework - I handle connections myself.

Windows 10 Pro

Microsoft SQL Server Developer (64-bit) version 15.0.2000.5

System.Data.SqlClient version 4.8.3

.NET 6.0

Any clues?

Update 1: As pointed out in comments I have indeed schema changes in my transaction - CREATE TABLE and ALTER TABLE statements mixed with standard UPDATES and SELECTS. My app allows user to create own tables (in limited functionality) and when this table is registered in table via INSERT there are some CREATES to adjust table structure.

Update 2: I can perform SELECT * FROM sys.dm_tran_locks I executed DBCC SQLPERF ('sys.dm_os_wait_stats', CLEAR); The problem remains.

Adam Mrozek
  • 1,410
  • 4
  • 26
  • 49
  • stored procedure with explicit transaction management ? – Sergey Jul 22 '22 at 09:01
  • I understand why you don't want to copy 1000 lines of code into your question, but without knowing what the code is doing there's very little we can say. If your procedure uses multiple statements, try to narrow down the problem to a particular statement (one option is to just comment out other statements and run one at a time, or try to use sql profiler or extended events). Are you making schema changes in the procedure? Any `alter` statements? Dynamic SQL? Are you running out of memory? Can you select from sys.dm_tran_locks? Checked wait stats (you can do this when the proc isn't running)? – allmhuran Jul 22 '22 at 09:08
  • @allmhuran indeed there are some schema changes in that transaction. Thans for pointing this out. I modified question. – Adam Mrozek Jul 22 '22 at 09:15
  • 1
    That could be the culprit. Quiesce the enviornment and clear the wait stats with `DBCC SQLPERF ('sys.dm_os_wait_stats', CLEAR); `. Then execute a sample load (that includes this problematic procedure), and then run a query against the wait stats. Paul Randal has a predefined query in [this artcile](https://www.sqlskills.com/blogs/paul/wait-statistics-or-please-tell-me-where-it-hurts/) which will filter "system" waits that we probably don't care about. It's pretty old and might miss some newer waits, but it will probably do the job. Come back and add the results to your question. – allmhuran Jul 22 '22 at 09:22
  • Looks like its a long process, i would break it up into different parts, you should get in an out as fast as possible not keep a transaction open whilst you do code relates stuff. Get the data you want and then start the transaction. A side note, your isolation level can also be set to ReadUncommited which is the same as NOLOCK. But agree that we need to know what part is the issue, with a wait like that it looks like you are doing way to much, do what you need then add/update whatever you need in a transaction. – Andrew Jul 22 '22 at 09:34
  • Also I believe that ReadUncommited and NOLOCK will work for reading which can cause phantom reads. But I believe SQL Server will still get a schema lock which means you cant modify the schema which it sounds like you might be doing. – Andrew Jul 22 '22 at 09:36
  • @DanGuzman I confirm that re-designed method works fine. Please post the answer I will accept it. – Adam Mrozek Jul 22 '22 at 11:47

2 Answers2

1

The cause of the locking issue is DDL (CREATE TABLE, etc.) within a transaction. This will acquire and hold restrictive locks on system table meta-data and block other database activity that need access to object meta-data until the transaction is committed.

This is an app design problem as one should not routinely execute DDL functions in application code. If that design cannot be easily remediated, perform the DDL operations separately in a short transaction (or with utocommit statements without an explict transaction) and handle DDL rollback in code.

Dan Guzman
  • 43,250
  • 3
  • 46
  • 71
0

You can use this useful stored procedure which I picked up somewhere along my travels. It recently helped me see the locking on a table and showed that after setting READ UNCOMMITED it was no longer doing row/page/table locks, but still had the schema lock. I believe you may have schema locks if you are modifying them! and also as commented don't keep a transaction open long, in and out is key.

What this does is runs the stored procedure every seconds 20 times, so you will get a snapshot of locking, a really useful stored procedure to remember.

EXEC [dbo].CheckLocks @Database = 'PriceBook'
WAITFOR DELAY '00:00:01'
GO 20

The stored procedure is as follows:

/*
This script can be run to find locks at the current time

We can run it as follows:

EXEC [dbo].CheckLocks @Database = 'PriceBook'
WAITFOR DELAY '00:00:01'
GO 10

*/

CREATE OR ALTER PROCEDURE [dbo].[CheckLocks]
    @Database NVARCHAR(256)
AS
BEGIN
    -- Get the sp_who details
    IF object_id('tempdb..#WhoDetails') IS NOT NULL
    BEGIN
        DROP TABLE #WhoDetails
    END

    CREATE TABLE #WhoDetails (
        [spid] INT,    
        [ecid] INT,    
        [status] VARCHAR(30),    
        [loginame] VARCHAR(128),
        [hostname] VARCHAR(128),
        [blk] VARCHAR(5),
        [dbname] VARCHAR(128),
        [cmd] VARCHAR(128),
        [request_id] INT
    )

    INSERT INTO #WhoDetails EXEC sp_who

    -- Get the sp_lock details
    IF object_id('tempdb..#CheckLocks') IS NOT NULL
    BEGIN
        DROP TABLE #CheckLocks
    END

    CREATE TABLE #CheckLocks (
        [spid] int,    
        [dbid] int,    
        [ObjId] int,    
        [IndId] int,    
        [Type] char(4),    
        [Resource] nchar(32),    
        [Mode] char(8),    
        [Status] char(6)
    )

    INSERT INTO #CheckLocks EXEC sp_lock

    SELECT DISTINCT
    W.[loginame],
    L.[spid], 
    L.[dbid], 
    db_name(L.dbid) AS [Database], 
    L.[ObjId], 
    object_name(objID) AS [ObjectName],
    L.[IndId], 
    L.[Type], 
    L.[Resource], 
    L.[Mode], 
    L.[Status]--, 
    --ST.text, 
    --IB.event_info
    FROM #CheckLocks AS L
    INNER JOIN #WhoDetails AS W ON W.spid = L.spid
    INNER JOIN sys.dm_exec_connections AS EC ON EC.session_id = L.spid
    --CROSS APPLY sys.dm_exec_sql_text(EC.most_recent_sql_handle) AS ST
    --CROSS APPLY sys.dm_exec_input_buffer(EC.session_id, NULL) AS IB -- get the code that the session of interest last submitted
    WHERE L.[dbid] != db_id('tempdb')
    AND L.[Type] IN ('PAG', 'EXT', 'TAB')
    AND L.[dbid] = db_id(@Database)

    /*
    https://learn.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-lock-transact-sql?view=sql-server-ver15
    
    Lock modes are as follows
    ------------------------------
    S = Shared
    U = Update
    X = Exclusive
    IS = Indent Shared
    IS = Intent Update
    IX = Intent Exclusive
    Sch-S = Schema Stability lock so no we cant remove tables or indexes in use

    Lock Type are as follows:
    ------------------------------
    RID = Single row lock
    KEY = Lock within an index that protects a range of keys
    PAG = Page level lock
    EXT = Extend Lock
    TAB = Table Lock
    DB = Database lock

    */

END

This is what you might see if you can catch the locking, this was before an after example, left and right.

enter image description here

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Andrew
  • 2,571
  • 2
  • 31
  • 56
  • Hmm, transaction is locked but this code returns nothing. – Adam Mrozek Jul 22 '22 at 10:02
  • You should get lots of SELECT output, for each execution e.g. 20 and some of them should/might catch the locks. If not then not sure what else to propose without looking at the code you are running. Keep this for a rainy day anyway as could come in handy :) – Andrew Jul 22 '22 at 10:17