Sql Server - 在插入另一个事务时选择会产生意外结果

pok*_*oke 8 index sql-server sql-server-2008-r2 transaction

我偶然发现了从根本上改变了我对事务和锁定的知识的情况(虽然我知道的不多),我需要帮助来理解它。

假设我有一张这样的表:

CREATE TABLE [dbo].[SomeTable](
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[SomeData] [varchar](200) NOT NULL,
[Moment] [datetime] NOT NULL,
[SomeInt] [bigint] NOT NULL
) ON [PRIMARY]
Run Code Online (Sandbox Code Playgroud)

我运行这个“在事务中插入 1000 行”查询:

BEGIN TRAN t1

DECLARE @i INT = 0

WHILE @i < 1000
BEGIN
    SET @i = @i + 1

    INSERT INTO [SomeTable] ([SomeData] ,Moment, SomeInt)
    VALUES (CONVERT(VARCHAR(255), NEWID()), getdate(), @i)

    WAITFOR DELAY '00:00:00:010'
END

COMMIT TRAN t1
Run Code Online (Sandbox Code Playgroud)

在此事务运行时,我正在执行一个简单的选择:

SELECT Id, Moment, SomeData, SomeInt FROM [SomeTable]
Run Code Online (Sandbox Code Playgroud)

并非总是可以重现它(显然取决于时间),但有时选择查询将在插入事务完成后返回少于 1000 行。在我的无知中,我相信 select 将始终返回 1000 行(鉴于隔离级别为 Read Committed),但显然我误解了事务和锁定的工作方式。

但是,如果我在 Id 列(生成聚集索引)上放置一个主键,只要我尝试过,select 查询就会返回所有 1000 行。以其他方式放置索引,在复合键上使用聚集索引,在其他一些列上使用非聚集索引,可能会再次导致返回的行数少于我的预期。

所以,我有这些问题:

  1. 为什么 select 并不总是返回事务提交的所有行?
  2. 如果这是预期的行为,那么实际使它按我预期的那样工作的最佳方法是什么?基本上,我想选择在事务之后(或之前)返回表的状态,而不是一些半完成的数据。快照隔离目前不是一个选项。放置 TABLOCK 似乎正在做这项工作,但有更好的解决方案吗?在现实生活中,我有一些表,如果不是绝对必要的话,我不想锁定在这个级别。
  3. 为什么放置索引会改变这种行为?

提前致谢。

Mar*_*ith 12

在运行您的代码几次后,我还没有设法重现这一点。

不过,我认为当稍后的行插入到文件中的较早页面时,它必须发生。

所以操作顺序是(例如)

  • 在第 200、207、223 页上插入到堆中的行
  • Select 语句启动并执行分配有序扫描。发现第一页是 200 并且被阻塞等待行锁被释放。
  • 其他行由第一个事务插入。其中一些分配在 200 之前的页面上。插入事务提交。
  • 行锁被释放并继续分配有序扫描。文件中较早的行被遗漏。

该表包括 10 页。默认情况下,前 8 个页面将从混合区分配,然后分配一个统一区。也许在您的情况下,在使用混合范围之前,文件中有可用的空间可用于免费统一范围。

重现问题后,您可以通过在不同窗口中运行以下命令来测试该理论,并查看原始数据中缺失的行是否SELECT都出现在此结果集的开头。

SELECT [SomeData],
       Moment,
       SomeInt,
       file_id,
       page_id,
       slot_id
FROM   [SomeTable] 
/*Undocumented - Use at own risk*/
CROSS APPLY sys.fn_PhysLocCracker(%% physloc %%)
ORDER BY page_id, SomeInt
Run Code Online (Sandbox Code Playgroud)

针对索引表的操作将按索引键顺序而不是分配顺序进行,因此不会受此特定情况的影响。

可以对索引执行分配顺序扫描,但仅当表足够大并且隔离级别未提交读取或持有表锁时才考虑。

因为读提交通常会在读取数据后立即释放锁,所以对索引的扫描可能会读取两次行或根本不读取行(如果索引键被并发事务更新,导致行向前或向后移动)有关此类问题的更多讨论,请参阅Read Committed Isolation Level


顺便说一下,我最初设想的索引情况是索引位于相对于插入顺序增加的列之一(Id、Moment、SomeInt 中的任何一个)。然而,即使聚集索引是随机SomeData的,问题仍然不会出现。

我试过

DBCC TRACEON(3604, 1200, -1) /*Caution. Global trace flag. Outputs lock info
                               on every connection*/

SELECT TOP 2 *,
             %%LOCKRES%%
FROM   [SomeTable] WITH(nolock)
ORDER BY [SomeData];

SELECT *,
       %%LOCKRES%%
FROM   [SomeTable]
ORDER BY [SomeData];

/*Turn off trace flags. Doesn't check whether or not they were on already 
  before we started, with TRACEOFF*/
DBCC TRACEOFF(3604, 1200, -1)
Run Code Online (Sandbox Code Playgroud)

结果如下

在此处输入图片说明

第二个结果集包括所有 1,000 行。锁定信息显示,即使24c910701749在释放锁定时它被阻止等待锁定资源,它也不会从该点继续扫描。相反,它立即释放该锁并在新的第一行上获取行锁。

在此处输入图片说明