使用WITH(NOLOCK)和Transactions

时间:2013-12-09 21:03:50

标签: sql-server tsql sql-server-2012

假设我有一个简单的查询,例如

Select * From MyTable WITH (NOLOCK)

并且,在执行此查询的同时,另一个用户在事务的上下文中向该表中插入100行。

理论上,在提交或回滚事务之前,Select语句是否可以读取插入表中的100行的子集,因为它正在使用NOLOCK?如果我正确理解NOLOCK,这似乎是可能的,但想要验证。

SQL Server 2012

2 个答案:

答案 0 :(得分:12)

当然,您可以读取在事务开始和提交或回滚之间受影响的子集或所有未提交数据。这就是NOLOCK的重点 - 允许您读取未提交的数据,这样您就不必等待编写者,并避免放置大多数锁,以便编写者不必等待你。

证明#1

这很容易证明。在一个窗口中,创建此表:

CREATE TABLE dbo.what(id INT);

在第二个窗口中,运行此查询:

DECLARE @id INT;

WHILE 1 = 1
BEGIN
 SELECT @id = id FROM dbo.what WITH (NOLOCK) WHERE id = 2;

 IF @id = 2
 BEGIN
   PRINT @id;
   BREAK;
 END
END

现在回到第一个窗口,开始一个故意长时间运行的事务,但将其回滚:

BEGIN TRANSACTION;
GO

INSERT dbo.what SELECT 2;
GO 10000

ROLLBACK TRANSACTION;

在第一个窗口中启动时,第二个窗口中的查询将暂停,并将吐出已读取的未提交值。

证明#2

这主要是为了对@ Blam上面的评论提出质疑,我不同意这一点:

  

实际上我认为你可以在提交或回滚之前读取所有100个而不仅仅是一个子集。

您当然可以读取受事务影响的行的子集。尝试以下类似示例,这次将100个集合插入表中1000次,并使用(NOLOCK)检索查询中的计数。窗口#1(如果您尚未测试上面的证明#1):

CREATE TABLE dbo.what(id INT);

窗口#2:

DECLARE @c INT;

WHILE 1 = 1
BEGIN
 SELECT @c = COUNT(*) FROM dbo.what WITH (NOLOCK) WHERE id = 2;

 IF @c > 0
   PRINT @c;

 IF @c > 10000
   BREAK;
END

回到窗口#1:

BEGIN TRANSACTION;
GO

INSERT dbo.what SELECT TOP (100) 2 FROM sys.all_objects;
GO 1000

ROLLBACK TRANSACTION;

窗口#2将旋转,直到您开始交易。一旦你这样做,你就会开始看到计数涓涓细流。但它们不会是100的倍数(更不用说100,000,@Blam似乎正在制造的全部或全部声明)。以下是我的简略结果:

1
10
12
14
17
19
23
25
29
...
85
87
91
95
98
100
100
...
9700
9700
9763
9800
9838
9900
9936
10000
10000
10000
10080

NOLOCK查询显然不会等待任何单个语句在读取数据之前完成,更不用说整个事务了。因此,无论每个语句影响多少行,无论整个事务中有多少语句,您都可以获得任何流量状态的数据。

其他副作用

还有NOLOCK可以skip rows, or read the same row twice的情况,具体取决于扫描类型以及其他事务何时生成页面拆分。基本上发生的事情是,(NOLOCK)查询正在读取数据,其他写入实际上可以将数据移动到不同的位置 - 因为它们可以 - 将您已经读过的行移动到扫描中的前一点,或者将尚未阅读的行移到扫描前面的某个点。

<强>建议

一般来说,这是个坏消息,您应该考虑使用READ_COMMITTED_SNAPSHOT - 它具有允许读者不阻止编写者的相同好处,反之亦然,但在一定程度上为您提供一致的数据视图及时,忽略所有后续数据修改(虽然这对tempdb有影响,所以一定要测试它)。 Very thorough information here

答案 1 :(得分:3)

正如Aaron优雅地解释的那样,是的,您将阅读脏的未提交数据。但是如果你想避免读取脏数据,并且尚未准备好开始使用乐观锁定,你可以尝试使用如下所示的READPAST表提示,尽管这会导致它跳过任何行已锁定,因此您将看不到尚未提交的已插入和已更新的行。

SELECT *
FROM MyTable WITH (READPAST)

请注意,此表提示要求数据库以READ COMMITTED(缺省值)或REPEATABLE READ隔离级别运行。