⏱️ Lectura: 14 min

Cada vez que PostgreSQL o SQLite confirman una escritura, ya grabaron ese cambio dos veces antes de tocar el archivo final de datos: primero en un registro secuencial, después en la página real. Esa duplicación deliberada se llama write-ahead logging y es la razón por la que un corte de energía a mitad de una transacción no te deja con una base de datos corrupta.

📑 En este artículo
  1. TL;DR
  2. Qué es el write-ahead logging y por qué existe
  3. Cómo funciona por dentro
  4. Ejemplos prácticos
  5. Cómo empezar: activar y verificar el WAL paso a paso
  6. Casos de uso reales
  7. Errores comunes y buenas prácticas
  8. Comparativa con alternativas
  9. Profundizando: group commit y replicación lógica
  10. Preguntas frecuentes
    1. ¿El write-ahead logging reemplaza a los backups?
    2. ¿Qué pasa si el archivo .wal crece demasiado en SQLite?
    3. ¿Puedo desactivar el WAL en PostgreSQL para ganar velocidad?
    4. ¿Cuál es la diferencia entre WAL físico y lógico?
    5. ¿SQLite y PostgreSQL implementan el WAL de la misma forma?
  11. Referencias

El concepto nace del proyecto ARIES de IBM en los años noventa y hoy corre dentro de casi cualquier motor transaccional serio: PostgreSQL, MySQL con InnoDB, SQLite y SQL Server lo usan de formas distintas pero con la misma idea de fondo. Este artículo explica cómo funciona por dentro, cómo activarlo y verificarlo con comandos reales, y en qué casos conviene evitarlo.

TL;DR

  • PRAGMA journal_mode=WAL; activa el modo write-ahead logging en SQLite en una sola línea.
  • Cada escritura se graba primero en el log y recién después se aplica al archivo de datos.
  • pg_current_wal_lsn() en PostgreSQL revela la posición exacta del log en cualquier momento.
  • Un checkpoint mal ajustado deja crecer el archivo .wal hasta ocupar más espacio que la base misma.
  • La replicación en streaming de PostgreSQL literalmente envía el WAL a cada réplica.
  • SQLite en modo WAL permite lectores concurrentes mientras un solo escritor modifica datos.
  • fsync=off gana velocidad, pero convierte cada apagón repentino en datos corruptos.
  • SHOW wal_level; y PRAGMA wal_checkpoint(TRUNCATE); confirman el estado real del log.

Qué es el write-ahead logging y por qué existe

Pensá en la bitácora de un barco. Antes de que el capitán actualice el mapa oficial con la posición final, anota cada maniobra en el cuaderno de bitácora: la hora, el rumbo, el viento. Si algo sale mal a mitad del viaje, ese cuaderno permite reconstruir exactamente qué pasó y dónde quedó el barco. Una base de datos con write-ahead logging hace lo mismo con cada cambio: lo anota en un log secuencial antes de tocar la página de datos definitiva.

El problema que resuelve es simple de enunciar y difícil de resolver bien. Una transacción puede modificar varias páginas en memoria, y el sistema operativo puede reiniciarse antes de que esas páginas lleguen a disco. Sin un registro previo del cambio, no hay forma de saber si una página a medio escribir en disco corresponde a una transacción confirmada, a una abortada o a una mezcla corrupta de ambas.

El algoritmo ARIES, publicado por investigadores de IBM, formalizó la solución en 1992: todo cambio se describe primero como un registro de log con un número de secuencia (LSN, log sequence number), ese registro se fuerza a disco con fsync antes de confirmar la transacción al cliente, y recién después el motor puede aplicar con calma la página modificada al archivo de datos. Si el proceso se cae en el medio, el log tiene todo lo necesario para rehacer las transacciones confirmadas y deshacer las que quedaron a mitad de camino.

La alternativa ingenua sería escribir cada página modificada directamente y esperar que nada falle en el momento exacto. Los motores que hacían eso en los años ochenta perdían datos con cada corte de luz mal sincronizado, y por eso el write-ahead logging terminó siendo el estándar de facto en PostgreSQL, MySQL, SQLite y prácticamente cualquier base de datos relacional moderna.

Cómo funciona por dentro

El flujo tiene tres actores: la aplicación que pide escribir, el log (una secuencia de archivos que crecen de forma append-only) y el archivo de datos final. Cuando confirmás una transacción, el motor arma un registro con el cambio exacto, le asigna un LSN creciente y lo agrega al final del log actual. Recién cuando ese registro llegó a disco de forma durable, el motor responde con el commit a la aplicación.

La página de datos modificada queda en memoria, en el buffer pool, y se escribe a disco más tarde, en un proceso llamado checkpoint. Un checkpoint recorre las páginas sucias en memoria, las vuelca al archivo de datos y anota en el log hasta qué LSN ya quedó aplicado. Eso acota cuánto log hay que releer si el sistema se reinicia: en vez de rehacer toda la historia desde el primer registro, la recuperación arranca desde el último checkpoint confirmado.

El siguiente diagrama resume ese flujo para una sola escritura, desde que la aplicación la pide hasta que el checkpoint la vuelca al archivo definitivo.

sequenceDiagram
    participant App as Aplicacion
    participant Log as Write-Ahead Log
    participant Disco as Archivo de datos
    App->>Log: pide escribir un cambio
    Log-->>App: confirma tras el fsync
    Note over Log,Disco: el checkpoint corre en segundo plano
    Log->>Disco: aplica la pagina modificada

Lo que hace rápido a este esquema es que el fsync solo toca el log, que es secuencial y por lo tanto barato de escribir. La página de datos, que puede estar dispersa en cualquier parte del archivo, se actualiza más tarde y sin apurar al cliente que espera la confirmación.

El log crece siempre al final del archivo, nunca se reescribe en el medio. Foto de Ales Krivec en Unsplash

Ejemplos prácticos

SQLite implementa write-ahead logging como un modo alternativo al journal por defecto. Activarlo es una sola instrucción:

PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;

La primera línea cambia el modo de journaling: en vez de copiar las páginas originales a un archivo rollback antes de modificarlas, SQLite ahora escribe los cambios a un archivo nombre.db-wal aparte y deja la base principal intacta hasta el próximo checkpoint. La segunda línea relaja el nivel de fsync porque en modo WAL ya no hace falta la garantía más estricta que exige synchronous=FULL; el propio manual de SQLite documenta esa combinación como la recomendada para la mayoría de las aplicaciones.

Un ejemplo más realista: una aplicación que inserta filas mientras otro proceso lee el mismo archivo al mismo tiempo, algo que en el modo journal clásico bloquearía al lector.

import sqlite3

con = sqlite3.connect("inventario.db")
con.execute("PRAGMA journal_mode=WAL;")
con.execute(
    "INSERT INTO productos (nombre, stock) VALUES (?, ?)",
    ("teclado mecanico", 120),
)
con.commit()
con.close()

Mientras ese proceso escribe, otro proceso puede abrir la misma base y ejecutar un SELECT sin esperar: en modo WAL los lectores ven una foto consistente del archivo tal como estaba antes de la escritura en curso, sin bloquearse contra ella.

En PostgreSQL el WAL no es opcional, siempre está activo, pero su nivel de detalle sí se configura. Para replicación en streaming hace falta subir wal_level por encima del mínimo:

-- en postgresql.conf
wal_level = replica
max_wal_senders = 5
wal_keep_size = 512MB

Con wal_level en replica, cada registro del log incluye la información suficiente para que un servidor réplica reconstruya el mismo estado sin acceso al archivo de datos original.

Cómo empezar: activar y verificar el WAL paso a paso

Para SQLite el flujo es directo. Corré la PRAGMA una vez por conexión y confirmá el resultado:

sqlite3 inventario.db "PRAGMA journal_mode=WAL;"
-- respuesta esperada: wal

sqlite3 inventario.db "PRAGMA journal_mode;"
-- confirma que sigue en wal despues de reconectar

Van a aparecer dos archivos nuevos junto a la base: inventario.db-wal con los cambios pendientes e inventario.db-shm, una zona de memoria compartida que coordina a los lectores y al escritor.

En PostgreSQL, después de editar postgresql.conf y reiniciar el servicio, confirmá el nivel activo y la posición actual del log:

SHOW wal_level;
-- respuesta esperada: replica

SELECT pg_current_wal_lsn();
-- devuelve algo como 0/3000060, la posicion actual del log

Si vas a habilitar replicación, el siguiente paso es crear un usuario dedicado y, en el servidor réplica, apuntar primary_conninfo a ese usuario. Los detalles exactos dependen de la versión, y la documentación de parámetros WAL de PostgreSQL los cubre con el detalle necesario para cada release.

Casos de uso reales

La replicación en streaming de PostgreSQL es, en el fondo, el WAL viajando por la red: el servidor primario no calcula nada especial para replicar, simplemente envía los mismos registros de log que ya genera para su propia recuperación ante fallos. Cada réplica los aplica en el mismo orden y llega al mismo estado, con un retraso de milisegundos a segundos según la latencia de red.

SQLite usa el mismo mecanismo con un objetivo distinto: aplicaciones móviles y de escritorio que necesitan que varios hilos lean la base mientras un proceso en segundo plano sincroniza datos. Una enorme cantidad de apps embebidas dependen de esa combinación de lectores no bloqueantes y un único escritor.

Otros sistemas toman prestada la misma idea sin llamarla write-ahead logging. Apache Kafka, por ejemplo, es en esencia un log distribuido de solo anexado que aplica el mismo principio a escala de clúster; el paralelismo con el WAL de una base de datos relacional no es casual, ambos resuelven el mismo problema de orden y durabilidad con la misma estructura de datos de fondo.

PostgreSQL puede mantener varias réplicas leyendo el mismo flujo de WAL en simultáneo. Foto de Juanjo Jaramillo en Unsplash

Errores comunes y buenas prácticas

El error más frecuente en SQLite es abrir una transacción larga sobre una base en modo WAL y dejarla abierta. Mientras esa transacción de lectura siga activa, el checkpoint automático no puede avanzar más allá del punto donde empezó, y el archivo .wal crece sin límite hasta que la transacción cierra.

⚠️ Ojo: un archivo .wal que no deja de crecer casi siempre indica una conexión de lectura abierta que nadie cerró.

En PostgreSQL el equivalente es una réplica caída o desconectada mientras wal_keep_size u otro slot de replicación siguen reteniendo segmentos de log para ella. Si esa réplica tarda demasiado en volver, el primario puede acumular gigabytes de WAL retenido esperando que alguien lo consuma.

Otro error clásico es desactivar fsync para ganar velocidad en cargas de escritura intensiva. fsync=off en PostgreSQL o PRAGMA synchronous=OFF en SQLite eliminan la garantía de durabilidad completa: el sistema operativo puede reportar la escritura como completada sin que el disco físico la haya recibido todavía, y un corte de energía en ese instante corrompe la base.

Como buena práctica, medí primero. La vista pg_stat_bgwriter en PostgreSQL muestra cuántos checkpoints fueron forzados por tiempo contra cuántos fueron forzados por acumular demasiado WAL, una señal directa de si conviene subir max_wal_size o revisar la carga de escritura.

Comparativa con alternativas

El write-ahead logging no es la única técnica para garantizar durabilidad, aunque sí es la más extendida en motores relacionales modernos.

TécnicaCuándo usarlaVentajaLimitación
Write-ahead loggingMotores transaccionales generales (PostgreSQL, SQLite, InnoDB)Recuperación rápida y replicación nativa a partir del mismo logHay que gestionar el crecimiento del log y ajustar los checkpoints
Shadow pagingMotores embebidos minimalistas con pocas escrituras concurrentesNo necesita fase de redo, la recuperación es casi instantáneaFragmenta el archivo y complica las transacciones concurrentes
Escritura asíncrona sin logCachés o métricas que toleran perder los últimos segundosMáxima velocidad de escritura posiblePérdida de datos garantizada ante un corte de energía

Profundizando: group commit y replicación lógica

Un solo fsync por transacción sería lento bajo carga alta, así que los motores modernos agrupan varios commits pendientes y los fuerzan a disco en un único fsync. Esto se llama group commit. PostgreSQL lo habilita a través de commit_delay y commit_siblings, esperando unos microsegundos adicionales para juntar más transacciones antes de pagar el costo del fsync.

También existe una distinción entre WAL físico y WAL lógico. El físico describe cambios en términos de páginas de disco, suficiente y rápido para recuperación y para réplicas que corren la misma versión exacta del motor. El lógico describe los cambios como operaciones (insertar esta fila, actualizar esta columna), lo que permite replicar entre versiones distintas o alimentar sistemas externos como colas de eventos.

💡 Tip: si necesitás enviar cambios de PostgreSQL a Kafka o a otro sistema externo, buscá logical decoding en la documentación: usa el mismo WAL como fuente, pero lo traduce a un formato legible por fuera del propio motor.

Los replication slots de PostgreSQL resuelven el problema de qué segmentos de WAL conservar: le indican al primario que no borre nada hasta que esa réplica en particular lo haya consumido, a costa de que una réplica caída para siempre puede llenar el disco del primario si nadie borra su slot.

flowchart TD
    A["Primario: genera WAL"] --> B["Replication slot retiene segmentos"]
    B --> C["Replica: consume el WAL"]
    C --> D[("Base de datos replica")]
    subgraph Cluster de PostgreSQL
    A
    B
    D
    end

📖 Resumen en Telegram: Ver resumen

Tu próximo paso: activá PRAGMA journal_mode=WAL; en una base SQLite de prueba y abrí dos conexiones simultáneas, una escribiendo en un bucle y otra leyendo, para ver con tus propios ojos que el lector nunca se bloquea.

Preguntas frecuentes

¿El write-ahead logging reemplaza a los backups?

No. El WAL protege contra caídas del proceso o del sistema operativo, pero un WAL corrupto o un disco que falla por completo se lleva log y datos por igual. Los backups siguen siendo necesarios; de hecho, PostgreSQL usa el propio WAL para construir backups continuos, pero eso es un uso adicional, no un reemplazo.

¿Qué pasa si el archivo .wal crece demasiado en SQLite?

El checkpoint automático se dispara cada 1000 páginas por defecto, pero si una lectura larga lo bloquea, el archivo sigue creciendo hasta que esa lectura termine. Forzarlo manualmente con PRAGMA wal_checkpoint(TRUNCATE); vacía el log y reduce el archivo a su tamaño mínimo.

¿Puedo desactivar el WAL en PostgreSQL para ganar velocidad?

No de forma completa: PostgreSQL siempre escribe WAL porque lo necesita para la recuperación ante fallos, incluso en una instalación de un solo nodo. Lo que sí se puede ajustar es el nivel de detalle con wal_level y la agresividad del fsync, con el compromiso de durabilidad que eso implica.

¿Cuál es la diferencia entre WAL físico y lógico?

El físico registra cambios de bajo nivel en páginas de disco; el lógico traduce esos mismos cambios a operaciones legibles como inserciones o actualizaciones, pensadas para replicar entre versiones distintas o exportar datos a sistemas externos.

¿SQLite y PostgreSQL implementan el WAL de la misma forma?

Comparten el principio, no la implementación. SQLite guarda un único archivo .wal por base y lo vuelca por completo en cada checkpoint; PostgreSQL divide el log en segmentos de 16 MB que se reciclan y puede mantener varias réplicas consumiendo el mismo flujo en simultáneo.

Referencias

📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.

Imagen destacada: Foto de Denny Müller en Unsplash


Andrés Morales

Desarrollador e investigador en inteligencia artificial. Escribe sobre modelos de lenguaje, frameworks, herramientas para devs y lanzamientos open source. Cubre papers de ML, ecosistema de startups tech y tendencias de programación.

0 Comentarios

Deja un comentario

Marcador de posición del avatar

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.