¿Por qué "Comenzar transacción" antes de "Insertar consulta" bloquea toda la tabla?

11

Estoy usando SQL Server 2005 Express.

En un escenario, agregué un Begin Transactioncomando justo antes de una INSERTdeclaración en un procedimiento almacenado. Cuando ejecuté este procedimiento almacenado, bloqueó toda la tabla y todas las conexiones concurrentes mostraron una pantalla bloqueada hasta el momento en que esto INSERTterminó.

¿Por qué se bloquea toda la tabla y cómo supero este problema en SQL Server 2005 Express?

Editado

La consulta es la siguiente:

INSERT INTO <table2> SELECT * FROM <table1> WHERE table1.workCompleted = 'NO'
RPK
fuente
2
No bloquea la tabla en postgresql.
Scott Marlowe
Necesita más @RPK. Con la tabla DDL y una muestra de los insertos, podemos darle una explicación precisa de lo que está sucediendo. Sin eso, solo estamos adivinando.
Mark Storey-Smith
Esta pregunta es muy vaga. Estoy eliminando cualquier referencia a otros DBMS y limitando las respuestas a SqlServer. Si el OP o cualquier otro lector quiere discutir los méritos de este concepto central en otras plataformas, entonces deberíamos discutirlo una vez por plataforma. Es perjudicial hacer de esto una unión cartesiana, habrá demasiados hilos de conversación diferentes en una página.
jcolebrand

Respuestas:

25

Esta respuesta puede resultar útil para la pregunta original, pero es principalmente para abordar información inexacta en otras publicaciones. También destaca una sección de tonterías en BOL.

Y como se indica para la documentación de INSERT , adquirirá un bloqueo exclusivo sobre la mesa. La única forma en que se puede hacer un SELECT contra la tabla es usar NOLOCK o establecer el nivel de aislamiento de la transacción.

La sección vinculada de BOL establece:

Una instrucción INSERT siempre adquiere un bloqueo exclusivo (X) en la tabla que modifica y mantiene ese bloqueo hasta que se complete la transacción. Con un bloqueo exclusivo (X), ninguna otra transacción puede modificar datos; las operaciones de lectura solo pueden realizarse con el uso de la sugerencia NOLOCK o el nivel de aislamiento de lectura no comprometida. Para obtener más información, consulte Bloqueo en el motor de base de datos .

NB: A partir de 2014-8-27, BOL se ha actualizado para eliminar las declaraciones incorrectas citadas anteriormente.

Afortunadamente este no es el caso. Si fuera así, las inserciones en una tabla ocurrirían en serie y todos los lectores quedarían bloqueados de toda la tabla hasta que se complete la transacción de inserción. Eso haría que SQL Server sea un servidor de base de datos tan eficiente como NTFS. No muy.

El sentido común sugiere que no puede ser así, pero como señala Paul Randall, " Hazte un favor, no confíes en nadie ". Si no puede confiar en nadie, incluido BOL , supongo que tendremos que demostrarlo.

Cree una base de datos y complete una tabla ficticia con un montón de filas, observando que se devuelve el 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

Configure un seguimiento del generador de perfiles que rastreará los eventos de bloqueo: adquiridos y bloqueados: liberados, filtrando en el DatabaseId del script anterior, configurando una ruta para el archivo y observando el TraceId devuelto.

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

Inserte una fila y detenga la traza:

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

Abra el archivo de rastreo y debería encontrar lo siguiente:

Ventana Profiler

La secuencia de bloqueos tomados es:

  1. Bloqueo de intención exclusiva en MyTable
  2. Bloqueo de intención exclusiva en la página 1: 211
  3. RangeInsert-NullResource en la entrada de índice agrupado para el valor que se inserta
  4. Cerradura exclusiva en la llave

Los bloqueos se liberan en orden inverso. En ningún momento se ha adquirido un candado exclusivo sobre la mesa.

¡Pero esto es solo un lote de inserción! Eso no es lo mismo que dos, tres o docenas corriendo en paralelo.

Sí lo es. SQL Server (y posiblemente cualquier motor de base de datos relacional) no tiene previsión sobre qué otros lotes pueden estar ejecutándose cuando procesa una declaración y / o lote, por lo que la secuencia de adquisición de bloqueo no varía.

¿Qué pasa con los niveles de aislamiento más altos, por ejemplo, serializable?

Para este ejemplo particular, se toman exactamente los mismos bloqueos. ¡No confíes en mí, pruébalo!

Mark Storey-Smith
fuente
2
Muy informativo. Buen trabajo @ Mark!
jcolebrand
0

No hago mucho trabajo T-SQL pero al leer la documentación ...

Esto es por diseño, como se indica en COMENZAR TRANSACCIÓN :

Dependiendo de la configuración actual del nivel de aislamiento de la transacción, la transacción bloquea muchos recursos adquiridos para admitir las instrucciones Transact-SQL emitidas por la conexión hasta que se completa con una instrucción COMMIT TRANSACTION o ROLLBACK TRANSACTION.

Y como se indica para la documentación de INSERT , adquirirá un bloqueo exclusivo sobre la mesa. La única forma en que se puede hacer un SELECT contra la tabla es usar NOLOCKo establecer el nivel de aislamiento de la transacción.


fuente
44
No había notado esa declaración mal redactada en BOL antes. Se requerirá un bloqueo exclusivo en algo dentro de la jerarquía de recursos, pero definitivamente no siempre es la tabla.
Mark Storey-Smith
66
-1 para los documentos (no es su culpa): es fácil demostrar que esto no es cierto en el aislamiento de instantáneas, por lo que la mantilla "siempre adquiere un bloqueo exclusivo (X)" está mal. No estoy seguro acerca de otros niveles de aislamiento.
Jack dice que intente topanswers.xyz