SQL Server Hekaton: OLTP en memoria sin perder ACID

  • datos
  • dotnet

Cuando una tabla se vuelve el cuello de botella de un sistema transaccional, la respuesta reflejo suele ser “metamos un caché delante” — casi siempre Redis. Es una solución razonable, pero no es la única, y para un equipo que ya vive en el ecosistema .NET y SQL Server, hay una alternativa que rara vez se evalúa en serio: mover la tabla completa a memoria sin salir de SQL Server. Ese es el trabajo de Hekaton, el nombre en clave del motor In-Memory OLTP que Microsoft introdujo en SQL Server 2014.

El problema que Hekaton resuelve

Un motor relacional tradicional asume que los datos viven en disco: usa locks para proteger la concurrencia, latches para proteger las estructuras internas en memoria, y páginas que hay que leer y escribir. Bajo concurrencia extrema — miles de sesiones actualizando las mismas filas por segundo — esa maquinaria es exactamente lo que te frena, sin importar cuántos índices agregues.

Hekaton ataca el problema desde la raíz: las tablas memory-optimized no usan locks ni latches en absoluto. La concurrencia se resuelve con control de versiones optimista (cada actualización crea una nueva versión de la fila; las versiones viejas se recolectan después) y con estructuras de datos diseñadas para vivir enteramente en RAM. El resultado no es “una tabla normal pero más rápida”: es un motor de ejecución distinto por debajo, con las mismas garantías transaccionales de siempre.

Cómo son las tablas en memoria

Antes de crear una tabla memory-optimized, la base de datos necesita un filegroup dedicado para datos en memoria (aunque la tabla no persista en disco, SQL Server necesita este filegroup para los checkpoints de las tablas durables):

ALTER DATABASE Ventas
ADD FILEGROUP Ventas_InMemory CONTAINS MEMORY_OPTIMIZED_DATA;

ALTER DATABASE Ventas
ADD FILE (NAME = 'Ventas_InMemory_dir', FILENAME = 'C:\Data\Ventas_InMemory')
TO FILEGROUP Ventas_InMemory;

Con eso resuelto, una tabla memory-optimized durable se ve así — nótese el índice hash, obligatorio para lookups por igualdad, con un BUCKET_COUNT fijado en la creación:

CREATE TABLE dbo.Sesiones
(
    SesionId        UNIQUEIDENTIFIER NOT NULL
        CONSTRAINT PK_Sesiones PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1000000),
    UsuarioId       INT NOT NULL,
    CreadaEn        DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
    UltimaActividad DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
    Payload         NVARCHAR(4000) NULL,

    INDEX IX_Sesiones_Usuario NONCLUSTERED (UsuarioId)
)
WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);

DURABILITY = SCHEMA_AND_DATA es el modo “normal”: las filas se registran en el log de transacciones y sobreviven un reinicio, igual que una tabla en disco. Pero hay un segundo modo que es el que de verdad compite con Redis en su propio terreno:

CREATE TABLE dbo.ContadorRateLimit
(
    ClienteId   VARCHAR(100) NOT NULL
        CONSTRAINT PK_ContadorRateLimit PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 500000),
    Tokens      FLOAT NOT NULL,
    Actualizado DATETIME2 NOT NULL
)
WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_ONLY);

Con DURABILITY = SCHEMA_ONLY no hay logging ni checkpoint de datos: si el servidor se reinicia, la tabla queda vacía (solo se conserva la definición). A cambio, las escrituras no tocan disco en absoluto. Es, funcionalmente, un caché — el mismo caso de uso que resolvería un HSET en Redis, pero con tipos, constraints y BUCKET_COUNT en vez de una convención de nombres de key.

Índices y compilación nativa

Las tablas memory-optimized soportan dos tipos de índice: hash (para igualdad, como el de Sesiones arriba) y range (un nonclustered normal, respaldado por un Bw-tree, para ORDER BY o comparaciones por rango). Ambos viven en memoria junto con los datos; no hay páginas de índice que leer desde disco.

La otra pieza distintiva es la posibilidad de compilar un procedimiento almacenado directo a código máquina, evitando por completo el intérprete de T-SQL:

CREATE OR ALTER PROCEDURE dbo.RegistrarActividadSesion
    @SesionId UNIQUEIDENTIFIER
WITH NATIVE_COMPILATION, SCHEMABINDING
AS
BEGIN ATOMIC WITH
(
    TRANSACTION ISOLATION LEVEL = SNAPSHOT,
    LANGUAGE = N'Spanish'
)
    UPDATE dbo.Sesiones
    SET UltimaActividad = SYSUTCDATETIME()
    WHERE SesionId = @SesionId;
END;

Un procedimiento nativamente compilado tiene restricciones reales (sin SQL dinámico, subconjunto limitado de funciones integradas, todas las tablas referenciadas deben ser memory-optimized), pero para una operación caliente y repetitiva — como actualizar una sesión en cada request — el resultado es la latencia más baja que SQL Server puede ofrecer para esa operación.

Desde .NET, sigue siendo solo SQL

Esta es la parte que casi no se menciona cuando se compara con Redis: desde el código de aplicación, una tabla memory-optimized es indistinguible de una tabla normal. Mismo SqlConnection, mismo pool de conexiones, mismo ORM:

await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();

await using var command = new SqlCommand(
    "EXEC dbo.RegistrarActividadSesion @SesionId", connection);
command.Parameters.AddWithValue("@SesionId", sesionId);
await command.ExecuteNonQueryAsync();

No hay un cliente nuevo que instalar, ni un formato de serialización que decidir, ni un segundo connection string que rotar en un vault. Es exactamente el mismo código que ya escribís para cualquier otra tabla.

Hekaton frente a Redis

Redis y las tablas memory-optimized resuelven el mismo síntoma — datos calientes que no deberían tocar disco — desde arquitecturas opuestas:

Redis Hekaton (In-Memory OLTP)
Ubicación Proceso separado, con red o socket de por medio Dentro del mismo motor SQL Server
Transacciones MULTI/EXEC, sin rollback real ante errores en ejecución ACID completo, mismos niveles de aislamiento que el resto de SQL Server
Consultas Por key, estructuras de datos específicas (hash, set, sorted set) T-SQL completo, incluyendo JOIN con tablas en disco
Consistencia con la base principal La mantiene la aplicación (cache-aside, invalidación manual) No aplica: es la misma base de datos
Modelo de datos Clave-valor y estructuras derivadas Relacional, con tipos y constraints

Redis gana en escenarios que Hekaton no está diseñado para cubrir: escalar horizontalmente entre muchos nodos, pub/sub, estructuras de datos como sorted sets para leaderboards, o simplemente compartir estado entre servicios escritos en distintos lenguajes que no comparten una base de datos relacional.

Nota: esta comparación está deliberadamente enfocada en por qué elegir Hekaton. Redis tiene ventajas reales que no entran en este artículo — las voy a cubrir a fondo en un próximo post dedicado a Redis, sin la carga de estar contestándole a Hekaton.

Por qué, si ya sos un equipo .NET, esto suele ganar

Ninguna de estas razones es “Hekaton es más rápido que Redis” — depende del caso, y no lo es siempre. Son razones operativas, y son las que más pesan en la práctica:

  • Una sola fuente de verdad. Con Redis como caché, la aplicación es responsable de mantenerlo sincronizado con la base de datos. Con Hekaton, la tabla en memoria y la tabla en disco pueden vivir en la misma transacción — no hay ventana de inconsistencia que resolver a mano.
  • Un solo sistema que operar. Sin un servicio adicional que desplegar, monitorear, respaldar y asegurar por separado. Backups, Always On, permisos: todo lo que ya tenés para SQL Server aplica también a las tablas memory-optimized.
  • Sin capa de serialización. Redis guarda bytes o strings; tu objeto de dominio tiene que ir y volver de ese formato, y esa capa es donde aparecen bugs de versión de esquema. Una tabla memory-optimized ya tiene tipos.
  • El equipo ya sabe T-SQL. No hay una API nueva que aprender ni un cliente nuevo que auditar en el árbol de dependencias.

Esto no hace a Redis una mala elección — lo hace la elección correcta para problemas que Hekaton no resuelve. Pero si el problema real es “esta tabla necesita ser más rápida” y ya estás en SQL Server, vale la pena evaluar Hekaton antes de sumar una pieza más a la infraestructura.

Referencias