SQL Server

Isolation levels en SQL Server: cuál usar y por qué

READ COMMITTED, SNAPSHOT, SERIALIZABLE y el famoso NOLOCK: qué anomalías evita cada nivel en SQL Server, cuánto bloquea y cuál conviene en cada caso.

Por Leyton Manuel Espitia Diaz 6 min de lectura

Fragmento de SQL que activa READ_COMMITTED_SNAPSHOT en una base de datos

Los reportes bloquean a las ventas. Alguien encuentra la solución en un foro y agrega WITH (NOLOCK) a todas las consultas. Los bloqueos desaparecen. Unas semanas después, un reporte muestra un total que no coincide con nada, y nadie puede reproducirlo.

Los niveles de aislamiento deciden qué puede ver una transacción de lo que hacen las demás al mismo tiempo. Cada nivel cambia consistencia por concurrencia. Elegir bien requiere saber qué se gana y qué se pierde en cada uno.

Las anomalías que se intenta evitar

  • Lectura sucia (dirty read): lees datos que otra transacción modificó pero todavía no confirmó, y que quizás deshaga.
  • Lectura no repetible: lees una fila, otra transacción la modifica y confirma, y al leerla de nuevo obtienes otro valor.
  • Fantasmas: ejecutas la misma consulta dos veces y la segunda devuelve filas nuevas que otra transacción insertó.

Los niveles, del más permisivo al más estricto

Nivel Lectura sucia No repetible Fantasmas Bloqueo de lectores
READ UNCOMMITTED (NOLOCK) Sí Sí Sí No bloquea
READ COMMITTED (por defecto) No Sí Sí Bloquea mientras lee
REPEATABLE READ No No Sí Mantiene bloqueos hasta el final
SERIALIZABLE No No No Bloquea rangos completos
SNAPSHOT No No No No bloquea (usa versiones)

READ UNCOMMITTED y NOLOCK: más que "lecturas sucias"

NOLOCK no solo permite leer datos sin confirmar. Como no respeta los bloqueos, una lectura que recorre un índice mientras otra transacción mueve páginas (por una división de página, por ejemplo) puede saltarse filas o leerlas dos veces. Un SUM puede dar un total que nunca existió en ningún momento.

Para un tablero aproximado de "pedidos de hoy" puede ser aceptable. Para cualquier cosa que alguien vaya a usar para decidir, facturar o conciliar, no.

READ COMMITTED: el defecto con un problema conocido

Es el nivel por defecto en SQL Server. Evita lecturas sucias tomando bloqueos compartidos mientras lee. El problema es que los lectores esperan a los escritores: un UPDATE largo sobre la tabla de pedidos hace esperar a todos los SELECT que necesitan esas filas.

Es exactamente el escenario que lleva a la gente a usar NOLOCK.

READ COMMITTED SNAPSHOT (RCSI): la solución que casi nadie activa

RCSI cambia el comportamiento de READ COMMITTED: en lugar de esperar al escritor, el lector ve la última versión confirmada de la fila, guardada en el almacén de versiones (en tempdb).

ALTER DATABASE [MiBase] SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE;
  • Los lectores no bloquean a los escritores, ni los escritores a los lectores.
  • No hay lecturas sucias: solo se ve lo confirmado.
  • No hay que tocar el código de la aplicación.

Es el comportamiento por defecto en Azure SQL Database, y en muchos sistemas transaccionales en SQL Server reduce drásticamente los bloqueos. WITH ROLLBACK IMMEDIATE cancela las transacciones abiertas para poder aplicar el cambio, así que hazlo en una ventana de mantenimiento.

El costo: más uso de tempdb para guardar versiones y un pequeño aumento en el tamaño de las filas modificadas. Y un cambio de semántica que importa: una lectura puede ver un valor que, un milisegundo después, ya no es el actual.

Por qué ese cambio de semántica importa

-- Con RCSI: dos transacciones pueden leer Stock = 1 al mismo tiempo
SELECT @stock = Stock FROM Inventory.Products WHERE ProductId = @id;

IF @stock >= @quantity
    UPDATE Inventory.Products SET Stock = Stock - @quantity WHERE ProductId = @id;

Las dos leen el valor confirmado (1), las dos pasan la validación y las dos descuentan. El patrón "leer, validar, escribir" no es seguro bajo concurrencia en ningún nivel por defecto, pero con RCSI es más fácil caer en él porque las lecturas nunca esperan.

La corrección es hacer la validación en la propia escritura:

UPDATE Inventory.Products
SET Stock = Stock - @quantity
WHERE ProductId = @id AND Stock >= @quantity;

IF @@ROWCOUNT = 0
    THROW 50001, 'Stock insuficiente', 1;

O usar un token de concurrencia, como vimos en errores comunes con transacciones SQL.

SNAPSHOT: consistencia de toda la transacción

SNAPSHOT (distinto de RCSI) hace que toda la transacción vea los datos como estaban al empezar. Es útil para reportes que hacen varias consultas y necesitan que los números cuadren entre sí:

ALTER DATABASE [MiBase] SET ALLOW_SNAPSHOT_ISOLATION ON;

SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
    SELECT SUM(Total) FROM Sales.Orders WHERE ...;
    SELECT SUM(Amount) FROM Billing.Payments WHERE ...;
COMMIT;

Si la transacción intenta modificar una fila que otra cambió después de que empezó, falla con un conflicto de actualización que hay que manejar con un reintento.

SERIALIZABLE: para pocos casos y con cuidado

SERIALIZABLE garantiza que el resultado sea como si las transacciones se ejecutaran una tras otra. Lo logra bloqueando rangos de claves, lo que reduce mucho la concurrencia y aumenta los deadlocks.

Tiene sentido en operaciones muy puntuales, como asignar un número consecutivo sin huecos o verificar que no exista un registro antes de insertarlo. Es importante saber que TransactionScope en .NET usa Serializable por defecto: si lo usas sin configurarlo, probablemente estás usando el nivel más restrictivo sin saberlo.

using var scope = new TransactionScope(
    TransactionScopeOption.Required,
    new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted },
    TransactionScopeAsyncFlowOption.Enabled);

El trade-off

Cada nivel más estricto evita más anomalías y bloquea más. Los niveles basados en versiones (RCSI y SNAPSHOT) rompen esa relación a cambio de usar tempdb y de aceptar que lo que lees puede estar a punto de cambiar.

Recomendación práctica

  • Activa RCSI en bases transaccionales con problemas de bloqueos entre lectores y escritores, después de probar la carga en tempdb.
  • Usa SNAPSHOT para reportes que necesitan consistencia entre varias consultas.
  • Evita NOLOCK salvo en consultas de monitoreo donde un valor aproximado es aceptable.
  • No confíes en el nivel de aislamiento para validaciones del tipo "leer y luego escribir": valida en la propia escritura.

Conclusión

NOLOCK resuelve el síntoma (los bloqueos) a cambio de resultados que pueden ser incorrectos. RCSI resuelve el mismo síntoma sin sacrificar la corrección de lo leído. Antes de llenar el código de hints, vale la pena preguntarse si el problema se resuelve con una línea de configuración de la base de datos.

Leyton Manuel Espitia Diaz

Escrito por

Leyton Manuel Espitia Diaz

CEO y fundador de Nakanaba

Desarrollador de software y fundador de Nakanaba. Lidera la arquitectura de Vitrin: backend en .NET, aplicaciones en Angular y .NET MAUI, SQL Server y servicios en Cloudflare.

¿Tu base de datos se volvió lenta?

Revisamos consultas, índices y modelo de datos para que tu sistema responda rápido aunque los datos sigan creciendo.

Ver desarrollo a medida