Por que "Iniciar transação" antes de "Inserir consulta" bloqueia a tabela inteira?

11

Estou usando o SQL Server 2005 Express.

Em um cenário, adicionei o Begin Transactioncomando imediatamente antes de uma INSERTinstrução em um procedimento armazenado. Quando executei esse procedimento armazenado, ele bloqueou a tabela inteira e todas as conexões simultâneas mostraram uma exibição interrompida até o momento em que isso INSERTterminou.

Por que a tabela inteira fica bloqueada e como supero esse problema no SQL Server 2005 Express?

Editado

A consulta é como abaixo:

INSERT INTO <table2> SELECT * FROM <table1> WHERE table1.workCompleted = 'NO'
RPK
fonte
2
Não bloqueia a tabela no postgresql.
Scott Marlowe
Precisa de mais @RPK. Com a tabela DDL e uma amostra das inserções, podemos fornecer uma explicação precisa do que está acontecendo. Sem isso, estamos apenas adivinhando.
Mark-Storey-Smith
Esta pergunta é muito vaga. Estou removendo qualquer referência a outros DBMS e limitando as respostas ao SqlServer. Se o OP ou qualquer outro leitor quiser discutir os méritos desse conceito central em outras plataformas, devemos discuti-lo uma vez por plataforma. É prejudicial tornar isso uma junção cartesiana; haverá muitos tópicos diferentes de conversa em uma página.
jcolebrand

Respostas:

25

Essa resposta pode ser útil para a pergunta original, mas é principalmente para abordar informações imprecisas em outras postagens. Também destaca uma seção de bobagens no BOL.

E, como indicado na documentação do INSERT , ele adquirirá um bloqueio exclusivo na mesa. A única maneira de selecionar um SELECT na tabela é usar NOLOCK ou definir o nível de isolamento da transação.

A seção vinculada da BOL declara:

Uma instrução INSERT sempre adquire um bloqueio (X) exclusivo na tabela que modifica e mantém esse bloqueio até a transação ser concluída. Com um bloqueio exclusivo (X), nenhuma outra transação pode modificar dados; as operações de leitura podem ocorrer apenas com o uso da dica NOLOCK ou com o nível de isolamento não confirmado da leitura. Para mais informações, consulte Bloqueio do Database Engine .

NB: A partir de 27/08/2014, o BOL foi atualizado para remover as instruções incorretas citadas acima.

Felizmente, este não é o caso. Se assim fosse, as inserções em uma tabela ocorreriam em série e todos os leitores seriam bloqueados de toda a tabela até a transação de inserção ser concluída. Isso tornaria o SQL Server um servidor de banco de dados tão eficiente quanto o NTFS. Não muito.

O senso comum sugere que não pode ser assim, mas como Paul Randall aponta: " Faça um favor a si mesmo, não confie em ninguém ". Se você não pode confiar em ninguém, incluindo o BOL , acho que teremos que provar isso.

Crie um banco de dados e preencha uma tabela fictícia com várias linhas, observando o DatabaseId retornado.

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

Configure um rastreamento do criador de perfil que rastreie os eventos lock: adquirido e lock: liberado, filtrando o DatabaseId a partir do script anterior, definindo um caminho para o arquivo e observando o TraceId retornado.

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

Insira uma linha e pare o rastreio:

USE LockDemo
GO
INSERT MyTable DEFAULT VALUES
GO
EXEC sp_trace_setstatus 3, 0
EXEC sp_trace_setstatus 3, 2
GO

Abra o arquivo de rastreamento e você deve encontrar o seguinte:

Janela Profiler

A sequência de bloqueios efetuados é:

  1. Bloqueio Intent-Exclusive no MyTable
  2. Bloqueio de intenção-exclusivo na página 1: 211
  3. RangeInsert-NullResource na entrada de índice em cluster para o valor que está sendo inserido
  4. Fechadura exclusiva na chave

Os bloqueios são liberados na ordem inversa. Em nenhum momento um bloqueio exclusivo foi adquirido na mesa.

Mas esta é apenas uma inserção de lote! Não é o mesmo que duas, três ou dezenas correndo paralelamente.

Sim, ele é. O SQL Server (e possivelmente qualquer mecanismo de banco de dados relacional) não tem previsão de quais outros lotes podem estar em execução ao processar uma instrução e / ou lote, portanto a sequência de aquisição do bloqueio não varia.

E quanto aos níveis mais altos de isolamento, por exemplo, serializável?

Neste exemplo em particular, exatamente os mesmos bloqueios são feitos. Não confie em mim, tente!

Mark Storey-Smith
fonte
2
Muito informativo. Bom trabalho, @Mark!
Jcolebrand
0

Eu não faço muito trabalho T-SQL, mas lendo a documentação ...

Isso ocorre por design, conforme declarado em BEGIN TRANSACTION :

Dependendo das configurações atuais do nível de isolamento de transação, muitos recursos adquiridos para dar suporte às instruções Transact-SQL emitidas pela conexão são bloqueados pela transação até que ela seja concluída com uma instrução COMMIT TRANSACTION ou ROLLBACK TRANSACTION.

E, como indicado na documentação do INSERT , ele adquirirá um bloqueio exclusivo na mesa. A única maneira de selecionar um SELECT na tabela é usar NOLOCKou definir o nível de isolamento da transação.


fonte
4
Não tinha notado aquela declaração bastante mal formulada na BOL antes. Um bloqueio exclusivo em algo dentro da hierarquia de recursos será necessário, mas definitivamente nem sempre é a tabela.
Mark Storey-Smith
6
-1 para os documentos (não é sua culpa) - é fácil provar que isso não é verdade no isolamento de instantâneos, para que o cobertor "sempre adquira um bloqueio exclusivo (X)" esteja errado. Não tenho certeza sobre outros níveis de isolamento.
Jack diz que tente topanswers.xyz 03/10