为什么在“插入查询”之前“开始事务”会锁定整个表?

RPK*_*RPK 11 sql-server-2005

我正在使用 SQL Server 2005 Express。

在一个场景中,我在存储过程中Begin TransactionINSERT语句之前添加了命令。当我执行这个存储过程时,它锁定了整个表,所有并发连接都显示挂起,直到INSERT完成。

为什么整个表都被锁定了,我如何在 SQL Server 2005 Express 中解决这个问题?

已编辑

查询如下:

INSERT INTO <table2> SELECT * FROM <table1> WHERE table1.workCompleted = 'NO'
Run Code Online (Sandbox Code Playgroud)

Mar*_*ith 25

这个答案可能对原始问题有帮助,但主要是为了解决其他帖子中的不准确信息。它还突出了 BOL 中的一段废话。

并且如INSERT文档所述,它将获取表上的排他锁。对表进行 SELECT 的唯一方法是使用 NOLOCK 或设置事务的隔离级别。

BOL 的链接部分指出:

INSERT 语句总是在它修改的表上获取一个排他 (X) 锁,并保持该锁直到事务完成。使用排他 (X) 锁,其他事务无法修改数据;读取操作只能在使用 NOLOCK 提示或读取未提交隔离级别时发生。有关详细信息,请参阅在数据库引擎中锁定

注意:截至 2014 年 8 月 27 日,BOL 已更新以删除上面引用的错误陈述。

幸运的是,情况并非如此。如果是这样,对表的插入将连续发生,并且所有读取器都将被阻止访问整个表,直到插入事务完成。这将使 SQL Server 成为与 NTFS 一样高效的数据库服务器。不是特别的。

常识表明它不可能如此,但正如保罗兰德尔指出的那样,“帮自己一个忙,不要相信任何人”。如果你不能相信任何人,包括BOL,我想我们只需要证明这一点。

创建一个数据库并用一堆行填充一个虚拟表,注意返回的 DatabaseId。

SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;

USE [master]
GO

IF EXISTS (SELECT name FROM sys.databases WHERE name = N'LockDemo')
DROP DATABASE [LockDemo]
GO

DECLARE @DataFilePath NVARCHAR(4000)
SELECT 
    @DataFilePath = SUBSTRING(physical_name, 1, CHARINDEX(N'master.mdf', LOWER(physical_name)) - 1)
FROM 
    master.sys.master_files
WHERE 
    database_id = 1 AND file_id = 1

EXEC ('
CREATE DATABASE [LockDemo] ON  PRIMARY 
( NAME = N''LockDemo'', FILENAME = N''' + @DataFilePath + N'LockDemo.mdf' + ''', SIZE = 2MB , MAXSIZE = UNLIMITED, FILEGROWTH = 2MB )
 LOG ON 
( NAME = N''LockDemo_log'', FILENAME = N''' + @DataFilePath + N'LockDemo_log.ldf' + ''', SIZE = 1MB , MAXSIZE = UNLIMITED , FILEGROWTH = 1MB )
')

GO

USE [LockDemo]
GO

SELECT DB_ID() AS DatabaseId

CREATE TABLE [dbo].[MyTable]
(
    [id] [int] IDENTITY(1,1) PRIMARY KEY CLUSTERED
    , [filler] CHAR(4030) NOT NULL DEFAULT REPLICATE('A', 4030) 
)
GO

INSERT MyTable DEFAULT VALUES;
GO 100
Run Code Online (Sandbox Code Playgroud)

设置一个探查器跟踪来跟踪 lock:acquired 和 lock:released 事件,过滤上一个脚本中的 DatabaseId,设置文件路径并注意返回的 TraceId。

declare @rc int
declare @TraceID int
declare @maxfilesize BIGINT
declare @databaseid INT
DECLARE @tracefile NVARCHAR(4000)

set @maxfilesize = 5 
SET @tracefile = N'D:\Temp\LockTrace'
SET @databaseid = 9

exec @rc = sp_trace_create @TraceID output, 0, @tracefile, @maxfilesize, NULL 
if (@rc != 0) goto error

declare @on bit
set @on = 1
exec sp_trace_setevent @TraceID, 24, 32, @on
exec sp_trace_setevent @TraceID, 24, 1, @on
exec sp_trace_setevent @TraceID, 24, 57, @on
exec sp_trace_setevent @TraceID, 24, 3, @on
exec sp_trace_setevent @TraceID, 24, 51, @on
exec sp_trace_setevent @TraceID, 24, 12, @on
exec sp_trace_setevent @TraceID, 60, 32, @on
exec sp_trace_setevent @TraceID, 60, 57, @on
exec sp_trace_setevent @TraceID, 60, 3, @on
exec sp_trace_setevent @TraceID, 60, 51, @on
exec sp_trace_setevent @TraceID, 60, 12, @on
exec sp_trace_setevent @TraceID, 23, 32, @on
exec sp_trace_setevent @TraceID, 23, 1, @on
exec sp_trace_setevent @TraceID, 23, 57, @on
exec sp_trace_setevent @TraceID, 23, 3, @on
exec sp_trace_setevent @TraceID, 23, 51, @on
exec sp_trace_setevent @TraceID, 23, 12, @on

-- DatabaseId filter
exec sp_trace_setfilter @TraceID, 3, 0, 0, @databaseid

-- Set the trace status to start
exec sp_trace_setstatus @TraceID, 1

-- display trace id for future references
select TraceID=@TraceID
goto finish

error: 
select ErrorCode=@rc

finish: 
go
Run Code Online (Sandbox Code Playgroud)

插入一行并停止跟踪:

USE LockDemo
GO
INSERT MyTable DEFAULT VALUES
GO
EXEC sp_trace_setstatus 3, 0
EXEC sp_trace_setstatus 3, 2
GO
Run Code Online (Sandbox Code Playgroud)

打开跟踪文件,您应该找到以下内容:

探查器窗口

采取的锁的顺序是:

  1. MyTable 上的意向排他锁
  2. 页面上的 Intent-Exclusive 锁 1:211
  3. RangeInsert-NullResource 在被插入的值的聚集索引条目上
  4. 钥匙独享锁

然后以相反的顺序释放锁。在任何时候都没有在表上获得排他锁。

但这只是一批插入!这与并行运行的两个、三个或数十个不同。

是的。SQL Server(以及可以说是任何关系数据库引擎)在处理语句和/或批处理时没有预见到其他批处理可能正在运行,因此锁定获取的顺序不会改变。

更高的隔离级别(例如可序列化)怎么样?

对于这个特定的例子,采用了完全相同的锁。不要相信我,试试吧!

  • 信息量很大。干得好@Mark! (2认同)

小智 0

我不做太多 T-SQL 工作,但通过阅读文档...

这是设计使然,如BEGIN TRANSACTION中所述:

根据当前事务隔离级别设置,为支持连接发出的 Transact-SQL 语句而获取的许多资源将被事务锁定,直到使用 COMMIT TRANSACTION 或 ROLLBACK TRANSACTION 语句完成为止。

正如INSERT文档所述,它将获取表上的独占锁。对表进行 SELECT 的唯一方法是使用NOLOCK或设置事务的隔离级别。

  • -1 对于文档(不是你的错) - 很容易证明这在快照隔离中不是正确的,因此毯子“总是获取独占(X)锁”是错误的。不确定其他隔离级别。 (6认同)
  • 之前没有注意到 BOL 中措辞相当糟糕的声明。资源层次结构中的某些内容需要独占锁,但它绝对不总是表。 (4认同)