El modo por defecto y sus límites
Por defecto, SQLite usa un rollback journal (diario de reversión). Cuando escribes, SQLite copia las páginas originales a un archivo -journal, modifica la base de datos principal y borra el diario al hacer commit. Si el proceso se cae a mitad de una escritura, el diario se reproduce en sentido inverso para deshacer el cambio parcial.
Es simple y seguro, pero tiene un detalle doloroso: lectores y escritores se pelean por el mismo archivo. Mientras un escritor tiene el bloqueo de la base de datos, ningún lector puede iniciar una nueva transacción. Y mientras hay lectores activos, el escritor espera. En una aplicación con tráfico —pongamos un servidor web atendiendo varias peticiones en paralelo— vas a ver errores SQLITE_BUSY antes de lo que te gustaría.
El modo WAL cambia esa dinámica.
Qué hace realmente el modo WAL en SQLite
El write-ahead logging (registro de escritura anticipada) le da la vuelta al modelo. En lugar de modificar el archivo principal de la base de datos en el sitio, el escritor añade las páginas confirmadas a un archivo aparte con sufijo -wal. Los lectores siguen leyendo el archivo principal, pero también consultan el WAL para ver versiones más recientes de las páginas que necesitan.
¿El resultado? Un escritor y cualquier número de lectores pueden estar activos al mismo tiempo. Cada lector ve una instantánea consistente del momento en que arrancó su transacción, y el escritor va añadiendo cosas al WAL sin tocar lo que los lectores están leyendo.
Esa única instrucción cambia la base de datos por completo. El modo es persistente: queda guardado en la cabecera del archivo, así que cualquier conexión futura usará WAL de forma automática. No hace falta ejecutarlo en cada conexión, basta con hacerlo una vez al provisionar la base de datos (o desde tu sistema de migraciones).
El pragma devuelve el nuevo modo. Si te devuelve wal, ya está todo listo. Si devuelve otra cosa, lo más probable es que el sistema de archivos no soporte memoria compartida (lo veremos más abajo).
Activar y verificar el modo WAL en SQLite
Puedes consultar el modo actual cuando quieras:
La primera llamada activa el modo WAL y devuelve el nuevo modo. La segunda (sin =) simplemente lo consulta. A partir de aquí, el directorio de tu messages.db contendrá tres archivos cuando haya actividad: messages.db, messages.db-wal y messages.db-shm. Los dos últimos aparecen y desaparecen según haya conexiones abiertas o no.
Los archivos -wal y -shm de SQLite
El modo WAL trae consigo dos archivos extra y conviene saber para qué sirve cada uno:
-walguarda las transacciones confirmadas que todavía no se han fusionado con la base de datos principal. Crece a medida que se escriben datos y se reduce (o se reinicia) cuando ocurre un checkpoint.-shmes un archivo de memoria compartida. Funciona como un índice del WAL para que todas las conexiones se pongan de acuerdo sobre qué páginas están dónde, sin tener que recorrer el WAL en cada consulta.
La consecuencia práctica: nunca copies una base de datos en modo WAL copiando solo el archivo .db. Los datos más recientes viven en el -wal, y sin él tu copia estará desactualizada o corrupta. O bien copias los tres archivos mientras ninguna conexión esté escribiendo o, mucho mejor, usas la API de backup de SQLite (la veremos en el siguiente capítulo).
Concurrencia en SQLite: un escritor, muchos lectores
WAL no te da escrituras concurrentes. SQLite sigue serializándolas: en cada momento hay exactamente una transacción que tiene el bloqueo de escritura. Lo que cambia es que las escrituras ya no bloquean a las lecturas, y las lecturas tampoco bloquean a las escrituras.
Una aplicación web típica corriendo sobre WAL se comporta así:
- Los endpoints con mucha lectura corren en paralelo sin pelearse.
- Los endpoints de escritura hacen una pequeña cola entre ellos, pero no bloquean las lecturas.
- Las lecturas largas (consultas analíticas, exportaciones) no obligan a esperar a quien escribe.
Si dos conexiones intentan escribir a la vez, la segunda recibe SQLITE_BUSY. La solución suele ser configurar un busy timeout razonable: le dices a SQLite que espere un poco antes de rendirse:
busy_timeout=5000 significa: «si hay un bloqueo activo, espera hasta 5 segundos antes de lanzar un error». Junto con WAL, esto cubre la contención que sufre la mayoría de las aplicaciones en la práctica. La variante BEGIN IMMEDIATE adquiere el bloqueo de escritura al iniciar la transacción, en lugar de hacerlo en la primera escritura, lo que evita toda una clase de deadlocks por escalado de bloqueo cuando varias conexiones quieren escribir a la vez.
Checkpoints: volcar el WAL a la base de datos
El archivo WAL no puede crecer indefinidamente. El checkpoint es el proceso que toma las páginas confirmadas en el WAL, las escribe en la base de datos principal y luego reinicia el WAL.
SQLite hace checkpoints automáticos cuando el WAL supera las ~1000 páginas (el valor por defecto de wal_autocheckpoint). Para la mayoría de los proyectos no hace falta tocar nada. Pero si quieres ajustarlo o disparar uno manualmente:
El pragma wal_checkpoint admite varios modos:
PASSIVE— hace checkpoint de todo lo que pueda sin molestar a lectores ni escritores. Es el modo por defecto.FULL— espera a que los escritores activos terminen y luego hace checkpoint de todo lo confirmado.RESTART— igual que FULL, pero además impide que los nuevos lectores sigan usando el WAL antiguo.TRUNCATE— igual que RESTART, y encima trunca el archivo WAL a cero bytes.
En la mayoría de servidores no hace falta llamarlo a mano. Ahora bien, si distribuyes una aplicación de escritorio y quieres mantener el tamaño de los archivos a raya al cerrarla, ejecutar un checkpoint TRUNCATE antes de cerrar la última conexión es una buena costumbre.
Pragmas que combinan bien con WAL
El modo WAL por sí solo ya rinde bien. Aun así, lo habitual en aplicaciones en producción es combinarlo con un par de ajustes adicionales:
Un recorrido rápido:
synchronous=NORMALes la combinación recomendada con WAL. Es seguro frente a caídas de la aplicación y del sistema operativo; solo un corte de luz en el instante exacto puede hacerte perder las últimas transacciones, y aun así la base de datos queda consistente. El valor por defecto,FULL, es más seguro pero notablemente más lento.busy_timeoutya lo vimos antes.foreign_keys=ONno tiene relación con WAL, pero conviene activarlo en cada conexión: SQLite deja la verificación de claves foráneas desactivada por defecto por compatibilidad hacia atrás.
Estos ajustes son por conexión (salvo journal_mode, que persiste). Ejecútalos justo después de abrir la conexión en el código de tu aplicación.
Cuándo el modo WAL no es la mejor opción
WAL es la recomendación por defecto, pero hay algunos escenarios en los que conviene replanteárselo:
- Sistemas de archivos en red. WAL depende de memoria compartida (
mmap) entre los procesos que acceden a la base de datos. NFS, SMB y similares no lo soportan de forma fiable. Si tu base de datos vive en un recurso de red, quédate con el rollback journal o, mejor todavía, no pongas SQLite en una unidad de red. - Medios de solo lectura. WAL necesita escribir los archivos
-waly-shm. Una base de datos en un CD-ROM o similar debe usar un modo de journal que no escriba (o abrirse en modo solo lectura conmode=ro). - Trabajos por lotes con un único escritor y sin lectores concurrentes. WAL no te perjudica, pero tampoco te aporta nada. El rollback journal por defecto cumple sin problemas.
Para el 95 % de las aplicaciones —backends web, apps de escritorio, apps móviles, dispositivos embebidos con almacenamiento local— WAL es la elección correcta.
Una configuración realista
Esta es la pinta que tienen la mayoría de las configuraciones de SQLite en producción, resumida en pragmas listos para ejecutar:
temp_store=MEMORY mantiene las tablas e índices temporales en RAM en vez de en disco: una ganancia pequeña y gratuita si te sobra memoria.
Configura todo esto una sola vez al abrir la conexión, dentro del setup de base de datos de tu aplicación, y ya tendrás cubierto casi todo lo que una app respaldada por SQLite necesita para comportarse bien bajo carga concurrente.
Lo siguiente: copias de seguridad y restauración
Ahora que tu base de datos viene acompañada de los archivos -wal y -shm, copiar el archivo principal ya no es una estrategia de backup segura. En el próximo capítulo veremos la forma correcta de respaldar una base de datos SQLite en caliente: el comando .backup, la API de backup online y qué hacer cuando necesitas una instantánea consistente sin tirar la app abajo.
Preguntas frecuentes
¿Qué es el modo WAL en SQLite?
WAL son las siglas de write-ahead logging (registro de escritura anticipada). En lugar de escribir los cambios directamente en el archivo principal de la base de datos y usar un rollback journal para deshacerlos si algo falla, SQLite añade los cambios a un archivo -wal aparte y los va integrando de vuelta cada cierto tiempo. La gran ventaja es la concurrencia: los lectores y un escritor pueden trabajar a la vez sin bloquearse entre sí.
¿Cómo se activa el modo WAL en SQLite?
Basta con ejecutar PRAGMA journal_mode=WAL; una sola vez. La configuración es persistente: queda guardada en la cabecera del archivo de la base de datos, así que las conexiones futuras también usarán WAL automáticamente. No hace falta repetirlo en cada conexión. Si todo va bien, el pragma devuelve el nuevo modo (wal).
¿El modo WAL permite escrituras concurrentes?
No. SQLite sigue serializando las escrituras: solo un escritor puede tener el bloqueo de escritura en cada momento. Lo que sí cambia con WAL es que los lectores ya no bloquean al escritor, y el escritor tampoco bloquea a los lectores. Para la mayoría de aplicaciones, ese era justo el cuello de botella que importaba.
¿Para qué sirven los archivos -wal y -shm?
El archivo -wal contiene los cambios ya confirmados que todavía no se han integrado en la base de datos principal. El -shm es un pequeño índice en memoria compartida que ayuda a las conexiones a localizar páginas dentro del WAL rápidamente. Ambos se recrean solos, pero ojo: si copias una base de datos, tienes que copiarlos junto con ella o usar la API de backup.