更新 SQL 服务器索引键列后缺少行
Missing rows after updating SQL Server index key column
"T-SQL Querying"书(http://www.amazon.com/Inside-Microsoft-Querying-Developer-Reference/dp/0735626030)有一个有趣的例子,在聚集索引键列更新期间,在默认事务隔离级别下查询table,您可能会错过一行或连续阅读两次。它看起来是 acceptable,因为更新 table/entity 密钥无论如何都不是一个好主意。但是,我已经更新了这个示例,以便在您更新非聚集索引键列值时发生同样的情况。
以下是 table 结构:
SET NOCOUNT ON;
USE master;
IF DB_ID('TestIndexColUpdate') IS NULL CREATE DATABASE TestIndexColUpdate;
GO
USE TestIndexColUpdate;
GO
IF OBJECT_ID('dbo.Employees', 'U') IS NOT NULL DROP TABLE dbo.Employees;
CREATE TABLE dbo.Employees
(
empid CHAR(900) NOT NULL, -- this column should be big enough, so that 9 rows fit on 2 index pages
salary MONEY NOT NULL,
filler CHAR(1) NOT NULL DEFAULT('a')
);
CREATE INDEX idx_salary ON dbo.Employees(salary) include (empid); -- include empid into index, so that test query reads from it
ALTER TABLE dbo.Employees ADD CONSTRAINT PK_Employees PRIMARY KEY NONCLUSTERED(empid);
INSERT INTO dbo.Employees(empid, salary) VALUES
('A', 1500.00),('B', 2000.00),('C', 3000.00),('D', 4000.00),
('E', 5000.00),('F', 6000.00),('G', 7000.00),('H', 8000.00),
('I', 9000.00);
这是第一次连接需要做的事情(每次更新时,该行将在 2 个索引页之间跳转):
SET NOCOUNT ON;
USE TestIndexColUpdate;
WHILE 1=1
BEGIN
UPDATE dbo.Employees SET salary = 10800.00 - salary WHERE empid = 'I'; -- on each update, "I" employee jumps between 2 pages
END
这是第二次连接需要做的事情:
SET NOCOUNT ON;
USE TestIndexColUpdate;
DECLARE @c INT
WHILE 1 = 1
BEGIN
SELECT salary, empid FROM dbo.Employees
if @@ROWCOUNT <> 9 BREAK;
END
通常,此查询应该 return 我们在第一个代码示例中插入的 9 条记录。然而,很快,我看到 8 条记录被 returned。此查询从 "idx_salary" 索引中读取所有数据,该索引由先前的示例代码更新。
这似乎是对来自 SQL 服务器的数据一致性的相当松懈的态度。当从索引中读取数据时,我希望进行一些锁定协调,同时更新其键列。
我对这种行为的理解是否正确?这是否意味着,即使是非聚集索引键也不应更新?
更新:
要解决这个问题,你只需要在数据库上启用"snapshots"(READ_COMMITTED_SNAPSHOT ON)。不再有死锁或丢失行。我已经尝试在这里总结所有这些:http://blog.konstantins.net/2015/01/missing-rows-after-updating-sql-server.html
更新 2:
这似乎是同一个问题,就像这篇很好的旧文章一样:http://blog.codinghorror.com/deadlocked/
Do I interpret this behavior correctly?
是的。
Does this mean, that even non-clustered index keys should not be updated?
没有。您应该使用适当的隔离级别或使应用程序容忍 READ COMMITTED
允许的不一致。
这个缺失行的问题并不局限于聚簇索引。它是由在 b 树中移动一行引起的。聚簇索引和非聚簇索引作为 b 树实现,它们之间只有微小的物理差异。
所以你看到的是完全相同的物理现象。每当您的查询从 B 树中读取一定范围的行时,它都会应用。该范围的内容可以移动。
使用可为您提供所需保证的隔离级别。对于只读事务,快照隔离级别通常是一种非常优雅且完整的并发解决方案。它似乎适用于你的情况。
This seems to be quite lax attitude towards data consistency from SQL Server. I would expect some locking coordination, when data is being read from index, while its key column is being updated.
这是一个可以理解的请求。另一方面,您特别要求低级别的隔离。您可以一直拨到 SERIALIZABLE
个。 SERIALIZABLE
呈现给您如同串行执行。
缺失行只是一个 READ COMMITTED
允许的许多影响的特例。在允许各种其他不一致的情况下专门阻止它们是没有意义的。
SET NOCOUNT ON;
USE TestIndexColUpdate;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
DECLARE @c INT
WHILE 1 = 1
BEGIN
DECLARE @count INT
SELECT @count = COUNT(*) FROM dbo.Employees WITH (INDEX (idx_salary))
WHERE empid > '' AND CONVERT(NVARCHAR(MAX), empid) > '__'
AND salary > 0
if @count <> 9 BREAK;
END
"T-SQL Querying"书(http://www.amazon.com/Inside-Microsoft-Querying-Developer-Reference/dp/0735626030)有一个有趣的例子,在聚集索引键列更新期间,在默认事务隔离级别下查询table,您可能会错过一行或连续阅读两次。它看起来是 acceptable,因为更新 table/entity 密钥无论如何都不是一个好主意。但是,我已经更新了这个示例,以便在您更新非聚集索引键列值时发生同样的情况。
以下是 table 结构:
SET NOCOUNT ON;
USE master;
IF DB_ID('TestIndexColUpdate') IS NULL CREATE DATABASE TestIndexColUpdate;
GO
USE TestIndexColUpdate;
GO
IF OBJECT_ID('dbo.Employees', 'U') IS NOT NULL DROP TABLE dbo.Employees;
CREATE TABLE dbo.Employees
(
empid CHAR(900) NOT NULL, -- this column should be big enough, so that 9 rows fit on 2 index pages
salary MONEY NOT NULL,
filler CHAR(1) NOT NULL DEFAULT('a')
);
CREATE INDEX idx_salary ON dbo.Employees(salary) include (empid); -- include empid into index, so that test query reads from it
ALTER TABLE dbo.Employees ADD CONSTRAINT PK_Employees PRIMARY KEY NONCLUSTERED(empid);
INSERT INTO dbo.Employees(empid, salary) VALUES
('A', 1500.00),('B', 2000.00),('C', 3000.00),('D', 4000.00),
('E', 5000.00),('F', 6000.00),('G', 7000.00),('H', 8000.00),
('I', 9000.00);
这是第一次连接需要做的事情(每次更新时,该行将在 2 个索引页之间跳转):
SET NOCOUNT ON;
USE TestIndexColUpdate;
WHILE 1=1
BEGIN
UPDATE dbo.Employees SET salary = 10800.00 - salary WHERE empid = 'I'; -- on each update, "I" employee jumps between 2 pages
END
这是第二次连接需要做的事情:
SET NOCOUNT ON;
USE TestIndexColUpdate;
DECLARE @c INT
WHILE 1 = 1
BEGIN
SELECT salary, empid FROM dbo.Employees
if @@ROWCOUNT <> 9 BREAK;
END
通常,此查询应该 return 我们在第一个代码示例中插入的 9 条记录。然而,很快,我看到 8 条记录被 returned。此查询从 "idx_salary" 索引中读取所有数据,该索引由先前的示例代码更新。 这似乎是对来自 SQL 服务器的数据一致性的相当松懈的态度。当从索引中读取数据时,我希望进行一些锁定协调,同时更新其键列。
我对这种行为的理解是否正确?这是否意味着,即使是非聚集索引键也不应更新?
更新: 要解决这个问题,你只需要在数据库上启用"snapshots"(READ_COMMITTED_SNAPSHOT ON)。不再有死锁或丢失行。我已经尝试在这里总结所有这些:http://blog.konstantins.net/2015/01/missing-rows-after-updating-sql-server.html
更新 2: 这似乎是同一个问题,就像这篇很好的旧文章一样:http://blog.codinghorror.com/deadlocked/
Do I interpret this behavior correctly?
是的。
Does this mean, that even non-clustered index keys should not be updated?
没有。您应该使用适当的隔离级别或使应用程序容忍 READ COMMITTED
允许的不一致。
这个缺失行的问题并不局限于聚簇索引。它是由在 b 树中移动一行引起的。聚簇索引和非聚簇索引作为 b 树实现,它们之间只有微小的物理差异。
所以你看到的是完全相同的物理现象。每当您的查询从 B 树中读取一定范围的行时,它都会应用。该范围的内容可以移动。
使用可为您提供所需保证的隔离级别。对于只读事务,快照隔离级别通常是一种非常优雅且完整的并发解决方案。它似乎适用于你的情况。
This seems to be quite lax attitude towards data consistency from SQL Server. I would expect some locking coordination, when data is being read from index, while its key column is being updated.
这是一个可以理解的请求。另一方面,您特别要求低级别的隔离。您可以一直拨到 SERIALIZABLE
个。 SERIALIZABLE
呈现给您如同串行执行。
缺失行只是一个 READ COMMITTED
允许的许多影响的特例。在允许各种其他不一致的情况下专门阻止它们是没有意义的。
SET NOCOUNT ON;
USE TestIndexColUpdate;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
DECLARE @c INT
WHILE 1 = 1
BEGIN
DECLARE @count INT
SELECT @count = COUNT(*) FROM dbo.Employees WITH (INDEX (idx_salary))
WHERE empid > '' AND CONVERT(NVARCHAR(MAX), empid) > '__'
AND salary > 0
if @count <> 9 BREAK;
END