Programming language used to interact with SQL Server databases
Following up on your comment under Erland's answer about the QUERY STORE ASYNC FLUSH locks: yes, those locks belong to SQL Server itself. I reproduced them on SQL Server 2022 (16.0.4305.5, Developer edition in Docker). I created a database with Query Store on, ran a small insert workload, and polled sys.dm_tran_locks every 50 ms.
What I saw while Query Store flushed its data
- Sessions with the command
QUERY STORE ASYNC FLUSH(and in other polls,BRKR TASK) held aDATABASESlock withrequest_owner_type = SHARED_TRANSACTION_WORKSPACEandrequest_owner_id = 0, exactly like the lock in your original question. - The same sessions also held
TRANSACTION-owned locks:DATABASE(subtypeQDS),METADATA(QDS_STATEMENT_STABILITY,QDS_QUERY_TEXT_STABILITY),OBJECT,PAGE,KEYandRID. - When one of those transaction IDs could be found in
sys.dm_tran_active_transactions, it was namedQDS base transactionorQDS nested transaction. - None of these transactions ever appeared in
sys.dm_tran_session_transactions. That's why you can't map them through the session transaction DMVs: they aren't user transactions bound to a session. - All of these sessions had
is_user_process = 0insys.dm_exec_sessions. Theirlogin_namewassa, so the login doesn't tell them apart. -
CHECKPOINTandTASK MANAGERbackground sessions also briefly held locks in the database, including the sameSHARED_TRANSACTION_WORKSPACElock. - The session IDs of these background tasks varied (2, 5, 36, 57, 76), so the old rule "session_id 50 or lower is system" doesn't hold.
For comparison, a user session running BEGIN TRAN; UPDATE ... showed the same session-level SHARED_TRANSACTION_WORKSPACE lock (owner ID 0, as Erland explained). Its TRANSACTION locks had a request_owner_id equal to CURRENT_TRANSACTION_ID() and to the transaction in sys.dm_tran_session_transactions.
A verification approach that separates the two
The supported, documented discriminator is sys.dm_exec_sessions.is_user_process, which is 0 for system sessions. The check that worked for me:
-
is_user_process = 0: SQL Server background task. Don't treat it as an unauthorized transaction. -
is_user_process = 1: the session must be on your allowlist. EveryTRANSACTION-owned lock must have arequest_owner_idthat one of your test sessions registered withCURRENT_TRANSACTION_ID()and that appears insys.dm_tran_session_transactionsfor that session. -
SHARED_TRANSACTION_WORKSPACElocks: attribute them to the session, not a transaction.
Here's the query I used (replace SynthDb with your database name):Following up on your comment under Erland's answer about the QUERY STORE ASYNC FLUSH locks: yes, those locks belong to SQL Server itself. I reproduced them on SQL Server 2022 (16.0.4305.5, Developer edition in Docker). I created a database with Query Store on, ran a small insert workload, and polled sys.dm_tran_locks every 50 ms.
What I saw while Query Store flushed its data
- Sessions with the command
QUERY STORE ASYNC FLUSH(and in other polls,BRKR TASK) held aDATABASESlock withrequest_owner_type = SHARED_TRANSACTION_WORKSPACEandrequest_owner_id = 0, exactly like the lock in your original question. - The same sessions also held
TRANSACTION-owned locks:DATABASE(subtypeQDS),METADATA(QDS_STATEMENT_STABILITY,QDS_QUERY_TEXT_STABILITY),OBJECT,PAGE,KEYandRID. - When one of those transaction IDs could be found in
sys.dm_tran_active_transactions, it was namedQDS base transactionorQDS nested transaction. - None of these transactions ever appeared in
sys.dm_tran_session_transactions. That's why you can't map them through the session transaction DMVs: they aren't user transactions bound to a session. - All of these sessions had
is_user_process = 0insys.dm_exec_sessions. Theirlogin_namewassa, so the login doesn't tell them apart. -
CHECKPOINTandTASK MANAGERbackground sessions also briefly held locks in the database, including the sameSHARED_TRANSACTION_WORKSPACElock. - The session IDs of these background tasks varied (2, 5, 36, 57, 76), so the old rule "session_id 50 or lower is system" doesn't hold.
For comparison, a user session running BEGIN TRAN; UPDATE ... showed the same session-level SHARED_TRANSACTION_WORKSPACE lock (owner ID 0, as Erland explained). Its TRANSACTION locks had a request_owner_id equal to CURRENT_TRANSACTION_ID() and to the transaction in sys.dm_tran_session_transactions.
A verification approach that separates the two
The supported, documented discriminator is sys.dm_exec_sessions.is_user_process, which is 0 for system sessions. The check that worked for me:
-
is_user_process = 0: SQL Server background task. Don't treat it as an unauthorized transaction. -
is_user_process = 1: the session must be on your allowlist. EveryTRANSACTION-owned lock must have arequest_owner_idthat one of your test sessions registered withCURRENT_TRANSACTION_ID()and that appears insys.dm_tran_session_transactionsfor that session. -
SHARED_TRANSACTION_WORKSPACElocks: attribute them to the session, not a transaction.
Here's the query I used (replace SynthDb with your database name):
SELECT l.request_session_id,
s.is_user_process,
r.command,
l.resource_type,
l.request_mode,
l.request_owner_type,
l.request_owner_id,
at.name AS transaction_name,
st.transaction_id AS session_transaction_id,
CASE
WHEN s.is_user_process = 0 THEN 'SQL Server background task'
WHEN l.request_owner_type = 'TRANSACTION' AND st.transaction_id IS NULL THEN 'CHECK: user lock with unknown transaction'
ELSE 'User session: check session/transaction against allowlist'
END AS classification
FROM sys.dm_tran_locks AS l
JOIN sys.dm_exec_sessions AS s ON s.session_id = l.request_session_id
LEFT JOIN sys.dm_exec_requests AS r ON r.session_id = l.request_session_id
LEFT JOIN sys.dm_tran_active_transactions AS at
ON l.request_owner_type = 'TRANSACTION' AND at.transaction_id = l.request_owner_id
LEFT JOIN sys.dm_tran_session_transactions AS st
ON st.session_id = l.request_session_id AND st.transaction_id = l.request_owner_id
WHERE l.resource_database_id = DB_ID(N'SynthDb');
During a flush, this showed the QDS nested transaction locks as "SQL Server background task" and my test session as "User session".
Don't rely on command or the QDS transaction names. They're useful for logging, but they're descriptive values, not a documented contract, and I saw the same Query Store work under two different command names.
If you'd rather not see these locks at all
Query Store is on by default for new databases in SQL Server 2022, and that includes LocalDB. If your synthetic test database doesn't need it, turning it off removes this source of background locks:
ALTER DATABASE SynthDb SET QUERY_STORE = OFF;
Other background tasks such as checkpoint can still take short locks, so keep the is_user_process rule either way.
Let me know how it goes.