MySQL vs SQLite: señales de migración — cuándo es el momento de actualizar

¿Te fue útil?

SQLite es el equivalente en bases de datos a una navaja multiusos bien hecha. Es compacta, afilada y, de alguna forma, siempre está ahí cuando la necesitas.
Hasta que un día intentas construir una casa con ella, y el mango empieza a doler.

Ahí es cuando los equipos empiezan a discutir en Slack: “SQLite está bien”. “No, es el cuello de botella”. “Solo necesitamos índices”.
Mientras tanto, los usuarios ven cargadores girando, y tu teléfono de guardia se está calentando.

La diferencia real: base de datos en archivo vs base de datos servidor (y por qué importa)

SQLite y MySQL ambos hablan SQL. Ese vocabulario compartido induce a la gente a pensar que son intercambiables.
No lo son. La primera diferencia no es la sintaxis, las características ni siquiera la velocidad. Es la arquitectura.

SQLite: una biblioteca con un archivo

SQLite es un motor de base de datos embebido. Vive dentro de tu proceso y almacena datos en un único archivo (más archivos laterales como
-wal y -shm cuando se usa el modo WAL). No hay un servidor de base de datos al que conectarse. Tu aplicación lee y escribe
bytes a través de la biblioteca SQLite, directamente, usando la semántica del sistema de archivos como el límite de durabilidad.

Por eso SQLite es fácil de distribuir, fácil de probar y sorprendentemente rápida en una sola máquina con concurrencia simple.
También por eso el sistema de archivos y el subsistema de almacenamiento forman parte de la historia de corrección de la base de datos, te guste o no.

MySQL: un servicio separado con concurrencia a nivel de proceso

MySQL es un servidor. Es un proceso separado con su propia gestión de memoria, comportamiento del pool de hilos, bloqueos internos, registros de rehacer,
buffer pool, capacidades de replicación y un protocolo de red. Tu aplicación envía peticiones; MySQL las programa, coordina la concurrencia y persiste
cambios a través de su motor de almacenamiento (típicamente InnoDB).

Esta separación te da una superficie operativa clara: puedes monitorizarla, ajustarla, replicarla, hacer copias de seguridad sin detener el mundo y
aislar la carga de la base de datos de la carga de la aplicación. También introduce sobrecarga operativa. Felicidades, ahora eres responsable de un servicio de base de datos.

Qué significa eso en producción

Si tu app es un único proceso en una sola máquina con baja concurrencia de escritura, SQLite puede ser una opción fantástica.
Si tienes múltiples instancias de la aplicación, una cola de escrituras, expectativas estrictas de durabilidad o alguna ambición de alta disponibilidad, el “es solo un archivo”
de SQLite se convierte en “es solo un archivo… compartido en un sistema distribuido”, y esa frase termina con un informe de incidente.

La decisión no es “SQLite es de juguete, MySQL es real”. La decisión es si los modos de fallo de tu sistema se manejan mejor por
bloqueo a nivel de archivo y semántica del SO (SQLite) o por una capa de concurrencia y durabilidad hecha a propósito con herramientas operativas (MySQL).

Idea parafraseada de Werner Vogels (CTO de Amazon): deberías planear para el fallo en lugar de fingir que no ocurrirá. Las bases de datos son donde fingir sale caro.

Hechos e historia que explican los modos de fallo actuales

  • SQLite fue creado en 2000 por D. Richard Hipp, inicialmente para soportar herramientas internas. Fue diseñado para ser embebido y fácil de desplegar.
  • El estilo de licencia “dominio público” de SQLite es una razón importante por la que está en todas partes: los proveedores no necesitan gimnasia legal para incluirlo en productos.
  • SQLite es la base de datos por defecto en muchas pilas móviles porque es pequeña, fiable en un dispositivo y no requiere un servidor.
  • El modo WAL (write-ahead logging) en SQLite mejoró la concurrencia sustancialmente, pero aún no te da “muchos escritores” como las bases de datos servidor.
  • MySQL comenzó a mediados de los años 1990 y creció en hosting web, donde muchos clientes concurrentes y servicios de larga duración son la norma.
  • InnoDB se convirtió en el motor de almacenamiento por defecto de MySQL a partir de MySQL 5.5, en gran parte porque ofrecía transacciones, bloqueo a nivel de fila y recuperación tras fallos.
  • La replicación moldeó la identidad operativa de MySQL: réplicas asincrónicas, escalado de lectura y patrones de failover se convirtieron en práctica estándar en operaciones web.
  • La corrección de SQLite depende de las garantías del sistema de archivos. La mayoría de los sistemas de archivos locales están bien; los sistemas de archivos en red y capas de almacenamiento “creativas” pueden comportarse de forma extraña.

Señales de migración: exactamente cuándo SQLite ha quedado pequeño

1) Ves “database is locked” bajo carga real

SQLite puede manejar múltiples lectores, y con WAL puede manejar un lector mientras hay un escritor activo. Pero la concurrencia de escrituras es el precipicio.
Un escritor a la vez. Si tu carga tiene ráfagas de escrituras—sesiones, eventos, contadores, estados de bandeja, colas de tareas—la latencia de cola aumentará.

La señal: las solicitudes no fallan inmediatamente; se quedan colgadas. Tu p95 se convierte en p99. Entonces los usuarios se quejan. Luego añades reintentos.
Luego amplificas la manada atronadora. Este es el punto en el que deberías dejar de negociar con la física y empezar a planear la migración.

2) Escalaste la app horizontalmente y SQLite se volvió un problema de archivo compartido

Ejecutar SQLite con múltiples instancias de la aplicación funciona solo cuando cada instancia tiene su propio archivo de base de datos (por inquilino, por nodo o por dispositivo).
El momento en que pones un archivo SQLite en almacenamiento compartido para varias instancias, creas un problema de bloqueo y consistencia distribuida.

Incluso si “funciona en staging”, puede degradarse en tormentas de bloqueos, lecturas obsoletas o riesgos de corrupción dependiendo de tu capa de almacenamiento.
MySQL existe específicamente para que no tengas que apostar tu uptime a cómo se comporta tu montaje NFS durante pérdida de paquetes.

3) Necesitas alta disponibilidad, no solo copias de seguridad

Las copias de seguridad no son alta disponibilidad. SQLite es genial para copias de seguridad porque el artefacto es un archivo, pero conmutar por error no es un problema de copiar archivos.
Si tu negocio requiere seguir aceptando escrituras durante la falla de un nodo, quieres replicación, herramientas de elección/failover y un lugar para aplicar
políticas operativas. Eso es territorio MySQL.

4) Necesitas latencia predecible bajo cargas mixtas

SQLite puede ser extremadamente rápido para lecturas puntuales y transacciones pequeñas. Pero cuando una consulta se vuelve grande—un escaneo accidental, un índice faltante,
una operación tipo vacuum según tus patrones—todo tu proceso puede sufrir porque el motor de base de datos está dentro de él.

Con MySQL, la base de datos tiene su propia memoria y comportamiento de planificación. Puedes aislar y ajustar. Puedes matar una consulta. Puedes establecer límites por usuario.
Puedes evitar que tu app se convierta en daño colateral.

5) Luchas por la durabilidad y la semántica de backups

La durabilidad de SQLite depende del uso correcto de transacciones y de que el almacenamiento subyacente respete las semánticas de fsync. Muchos equipos
sin saberlo configuran pragmas que cambian durabilidad por velocidad. Luego se sorprenden cuando un fallo borra escrituras recientes.

Si ya estás discutiendo sobre los ajustes de synchronous y “qué tan arriesgado es en realidad”, ya estás pagando el costo cognitivo.
MySQL te ofrece controles de durabilidad estándar en la industria y patrones establecidos de backup/failover.

6) Necesitas observabilidad operativa que puedas entregar al on-call

SQLite no trae por defecto performance schema, registros de consultas lentas, estado de replicación, métricas de buffer pool ni comandos admin estandarizados.
Puedes instrumentarlo, pero estás construyendo tu propia capa de operaciones de base de datos.

Si el on-call necesita responder “¿qué hace la base de datos ahora mismo?” y lo mejor que puedes ofrecer es “puedo añadir unos logs y redeployar”,
eso no es una estrategia operativa. Es esperanza con pasos extra.

7) Tu modelo de datos creció más allá del “un archivo” operativamente

La naturaleza de un solo archivo de SQLite es conveniente hasta que se convierte en un artefacto de despliegue. Enviar, migrar, bloquear, copiar y validar ese archivo
se transforma en un ritual de alto riesgo. Con MySQL, las migraciones de esquema siguen siendo riesgosas, pero al menos forman parte de un mundo con herramientas maduras,
enfoques de migración online y playbooks establecidos.

8) Estás construyendo flujos de datos multitenant o con control de acceso

SQLite no tiene cuentas de usuario, ni autenticación a nivel de red, ni un modelo de privilegios fino como una base de datos servidor.
Si tu postura de seguridad necesita separación real de funciones o control de acceso con auditoría, MySQL es el ajuste más natural.

Broma #1: SQLite es como un portero muy eficiente para un club minúsculo—hasta que tu app invita a todo Internet y insiste en que sigue siendo “un lugar pequeño”.

Guía de diagnóstico rápido: encuentra el cuello de botella en 15 minutos

El trabajo no es “decidir que MySQL es mejor”. El trabajo es demostrar qué está fallando. Aquí está el orden que ahorra tiempo.

Primero: confirma la categoría del síntoma (bloqueo, IO, CPU o diseño de consulta)

  1. Comprueba la contención de bloqueos: errores como database is locked, timeouts o esperas largas alrededor de escrituras.
  2. Comprueba la latencia de almacenamiento: aumento del disk await, bloqueos de fsync o IOPS saturadas.
  3. Comprueba la CPU: núcleo único al 100% en el proceso de la app (SQLite se ejecuta en proceso), o tiempo de ejecución de consultas que sube con la CPU.
  4. Comprueba planes de consulta: escaneos completos e índices faltantes. SQLite hará felizmente lo incorrecto rápido hasta que ya no lo haga.

Segundo: reproduce con un benchmark controlado

Usa una prueba orientada a escrituras y otra orientada a lecturas. Mide latencia p95/p99, no solo throughput. SQLite a menudo parece bien hasta que la latencia de cola empeora.

Tercero: decide si la solución es táctica o estratégica

  • Solución táctica: añadir un índice, reducir la frecuencia de escrituras, agrupar escrituras, habilitar WAL, establecer un busy timeout o cambiar los límites de las transacciones.
  • Solución estratégica: migrar a MySQL cuando necesites escrituras concurrentes, HA, mejores controles operativos o rendimiento predecible con múltiples clientes.

Tareas prácticas: comandos, salidas y decisiones (12+)

Estas son las comprobaciones de “muéstrame”. Cada tarea incluye un comando, qué significa la salida y qué hacer después.
Los comandos asumen un host Linux con un archivo SQLite en /var/lib/app/app.db y un servicio MySQL si estás comparando.

Task 1: Confirmar modo WAL y ajustes synchronous (durabilidad vs velocidad)

cr0x@server:~$ sqlite3 /var/lib/app/app.db 'PRAGMA journal_mode; PRAGMA synchronous;'
wal
2

Qué significa: wal mejora la concurrencia de lectores frente a escritores. synchronous=2 es FULL (más seguro).
Si ves off o 0, puede que estés cambiando seguridad ante fallos por rendimiento.

Decisión: Si necesitas durabilidad y estás usando synchronous=OFF, arregla eso primero. Si FULL penaliza el rendimiento y las escrituras son frecuentes, eso es una señal de migración.

Task 2: Comprobar busy timeout (cómo te comportas bajo contención)

cr0x@server:~$ sqlite3 /var/lib/app/app.db 'PRAGMA busy_timeout;'
0

Qué significa: 0 significa “fallar inmediatamente” bajo contención de bloqueo (o propagar errores rápidamente).

Decisión: Establece un busy timeout sensato en la aplicación (y posiblemente en los pragmas) si los picos de bloqueo son menores. Si necesitas timeouts largos para sobrevivir, estás parcheando una arquitectura de un escritor.

Task 3: Identificar tablas calientes y cobertura de índices rápidamente

cr0x@server:~$ sqlite3 /var/lib/app/app.db ".schema" | sed -n '1,40p'
CREATE TABLE events(
  id INTEGER PRIMARY KEY,
  user_id INTEGER NOT NULL,
  created_at TEXT NOT NULL,
  payload TEXT NOT NULL
);
CREATE INDEX idx_events_user_created ON events(user_id, created_at);

Qué significa: Estás comprobando si existen las rutas de acceso obvias. La falta de un índice compuesto es una razón común por la que SQLite “de repente se volvió lenta”.

Decisión: Si los problemas de rendimiento se deben a índices faltantes, a menudo puedes retrasar la migración. Si indexar mejora lecturas pero las escrituras siguen bloqueadas, aún debes migrar.

Task 4: Explicar el plan de consulta (detectar el escaneo completo)

cr0x@server:~$ sqlite3 /var/lib/app/app.db "EXPLAIN QUERY PLAN SELECT * FROM events WHERE user_id=42 ORDER BY created_at DESC LIMIT 50;"
QUERY PLAN
`--SEARCH events USING INDEX idx_events_user_created (user_id=?)

Qué significa: Esto es bueno: búsqueda por índice, no escaneo completo.

Decisión: Si ves SCAN TABLE en una tabla grande en rutas de producción, corrige el esquema/consulta primero. No migres solo para evitar añadir un índice.

Task 5: Comprobar tamaño de la BD y estadísticas de páginas (crecimiento y presión IO)

cr0x@server:~$ sqlite3 /var/lib/app/app.db 'PRAGMA page_size; PRAGMA page_count;'
4096
258000

Qué significa: El tamaño aproximado es page_size * page_count (~1.0 GiB aquí). Las BDs más grandes aumentan las penalizaciones por fallos de caché y el dolor de vacuum/mantenimiento.

Decisión: Si la BD crece rápidamente y estás en un único nodo, planifica MySQL antes—especialmente si necesitas mantenimiento online.

Task 6: Comprobar bloat del archivo WAL (presión de checkpoint)

cr0x@server:~$ ls -lh /var/lib/app/app.db*
-rw-r----- 1 app app 1.0G Dec 30 10:12 /var/lib/app/app.db
-rw-r----- 1 app app 6.2G Dec 30 10:12 /var/lib/app/app.db-wal
-rw-r----- 1 app app  32K Dec 30 10:12 /var/lib/app/app.db-shm

Qué significa: Un archivo WAL enorme suele significar que el checkpoint no da abasto (lectores largos, checkpoints mal configurados o ráfagas de escritura).

Decisión: Investiga transacciones de lectura de larga duración. Si no puedes controlarlas (muchos servicios, jobs en segundo plano), migra—MySQL maneja esta clase de problema con más gracia.

Task 7: Medir latencia de disco (IO es el asesino silencioso)

cr0x@server:~$ iostat -x 1 3
Linux 6.1.0 (server) 	12/30/2025 	_x86_64_	(8 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.00    0.00    6.00   18.00    0.00   64.00

Device            r/s     w/s   rkB/s   wkB/s  await  svctm  %util
nvme0n1         120.0   300.0  6400.0 22000.0   28.5   1.1   95.0

Qué significa: await ~28ms al 95% de utilización es duro. Las escrituras de SQLite pueden ser intensivas en fsync dependiendo de pragmas y patrones de transacción.

Decisión: Si el almacenamiento es lento, arreglar el disco puede resolver problemas tanto de SQLite como de MySQL. Si ya tienes almacenamiento decente y aún ves contención de bloqueos, la migración sigue siendo probable.

Task 8: Encontrar descriptores abiertos y procesos que tocan la BD (comprobación de multi-writer)

cr0x@server:~$ lsof /var/lib/app/app.db | head
app      2314 app   12u   REG  253,0 1073741824  12345 /var/lib/app/app.db
app      2314 app   13u   REG  253,0 6657199308  12346 /var/lib/app/app.db-wal
worker   2551 app   10u   REG  253,0 1073741824  12345 /var/lib/app/app.db

Qué significa: Múltiples procesos están usando el mismo archivo DB. Eso está bien en una máquina si está coordinado, pero aumenta la probabilidad de contención de bloqueos y transacciones largas.

Decisión: Si tu arquitectura naturalmente requiere muchos escritores, deja de intentar que SQLite se comporte como una BD servidor.

Task 9: Comprobar tipo de sistema de archivos y opciones de montaje (suposiciones de durabilidad)

cr0x@server:~$ findmnt -no FSTYPE,OPTIONS /var/lib/app
ext4 rw,relatime,errors=remount-ro

Qué significa: ext4 local suele ser sensato. Si ves montajes NFS/CIFS/FUSE, las semánticas de durabilidad y bloqueo pueden ponerse interesantes de forma negativa.

Decisión: Si el archivo DB vive en almacenamiento en red y la disponibilidad importa, migra. No conviertas tu base de datos en un experimento con un sistema de archivos distribuido.

Task 10: Observar esperas de bloqueo y timeouts en los logs de la app (el síntoma visible por humanos)

cr0x@server:~$ grep -E "database is locked|SQLITE_BUSY|timeout" /var/log/app/app.log | tail -n 5
2025-12-30T10:11:58Z ERROR db write failed: SQLITE_BUSY: database is locked
2025-12-30T10:11:59Z WARN retrying transaction after SQLITE_BUSY
2025-12-30T10:12:02Z ERROR request_id=9f2d api=/events POST latency_ms=4210 sqlite_busy_retries=5

Qué significa: Los reintentos por busy inflan la latencia. No estás solo lento; eres inestable bajo ráfagas.

Decisión: Si los reintentos por bloqueo se correlacionan con picos de tráfico y las escrituras son centrales para el producto, planifica la migración. Si es una operación administrativa rara, puedes aislarla en su lugar.

Task 11: Ejecutar un microtest rápido de concurrencia de escritura (¿colapsa con dos escritores?)

cr0x@server:~$ bash -lc 'for i in {1..2}; do (time sqlite3 /var/lib/app/app.db "BEGIN; INSERT INTO events(user_id,created_at,payload) VALUES(42,datetime(\"now\"),\"x\"); COMMIT;" ) & done; wait'
real	0m0.012s
user	0m0.003s
sys	0m0.002s
real	0m1.104s
user	0m0.004s
sys	0m0.003s

Qué significa: Un escritor termina rápido; el otro espera alrededor de un segundo (o más bajo carga). Esa espera se vuelve latencia visible para el usuario.

Decisión: Si tu perfil de producción espera escrituras concurrentes, migra. Si las escrituras son raras y puedes agruparlas, puedes permanecer en SQLite.

Task 12: Validar la integridad de SQLite (¿ya estás en problemas?)

cr0x@server:~$ sqlite3 /var/lib/app/app.db "PRAGMA integrity_check;"
ok

Qué significa: Bien. Si devuelve otra cosa, tienes corrupción y debes tratarlo como un incidente de producción.

Decisión: El riesgo de corrupción es un forzador: prioriza la migración y arregla inmediatamente el almacenamiento/manejo de transacciones.

Task 13: Comparar con una línea base MySQL (¿puede absorber la concurrencia?)

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_running'; SHOW GLOBAL STATUS LIKE 'Innodb_row_lock_time';"
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| Threads_running | 18    |
+-----------------+-------+
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| Innodb_row_lock_time   | 1240  |
+------------------------+-------+

Qué significa: MySQL puede tener muchos hilos activos. El tiempo de bloqueo de fila te da una ventana hacia la contención (no perfecto, pero útil).

Decisión: Si MySQL muestra tiempo de bloqueo de fila manejable mientras SQLite hace timeouts, esa es tu justificación de migración en una captura.

Task 14: Comprobar la postura de durabilidad de MySQL (no migres a un nuevo riesgo)

cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit'; SHOW VARIABLES LIKE 'sync_binlog';"
+------------------------------+-------+
| Variable_name                | Value |
+------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1   |
+------------------------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| sync_binlog   | 1     |
+---------------+-------+

Qué significa: Estos ajustes son los valores por defecto de “me importa la durabilidad” para muchos sistemas de producción.

Decisión: Si migras a MySQL pero pones estos valores relajados sin entender las semánticas de fallo, no actualizaste—solo cambiaste el estilo de pérdida de datos.

Tres mini-historias corporativas (y la lección que necesitas)

Mini-historia 1: El incidente causado por una suposición equivocada

Un pequeño equipo de plataforma lanzó un servicio que almacenaba el estado de flujo de trabajo del cliente en SQLite. Era elegante: un binario, un archivo de BD, copias de seguridad sencillas.
Incluso usaban modo WAL y tenían un “busy timeout”, así que se sentían preparados.

Luego la empresa añadió una segunda instancia detrás de un balanceador de carga para redundancia. El archivo DB se movió a un montaje compartido para que ambas instancias pudieran “ver el mismo estado”.
Funcionó durante una semana, que es la duración más peligrosa en ingeniería porque enseña la lección equivocada.

Bajo uso pico, ambas instancias intentaron escribir. Las esperas de bloqueo se empezaron a acumular. La lógica de reintentos reintentó diligentemente, lo que aumentó la tasa de escritura,
lo que incrementó la contención de bloqueos. Los usuarios vieron timeouts, y el on-call vio la CPU mayormente inactiva. “Pero no es computación”, dijeron. “Está bien”.

El verdadero culpable fue la suposición: “Si ambos procesos pueden leer el archivo, pueden coordinar escrituras de forma segura sobre un sistema de archivos en red”.
Esa suposición es una trampa. SQLite espera ciertas semánticas de bloqueo y fsync. El almacenamiento en red puede proveerlas a veces, hasta que no.

La solución no fue heroica: desplegaron MySQL, apuntaron ambas instancias a él y eliminaron por completo la capa de archivo compartido.
El primer incidente después de eso fue aburrido. Aburrido es el resultado correcto.

Mini-historia 2: La optimización que salió mal

Otro equipo tenía un servicio de ingestión intensivo en escrituras con SQLite. Estaban sufriendo picos de latencia. Alguien encontró un post sobre pragmas de rendimiento y aplicaron un cambio: establecer PRAGMA synchronous=OFF y PRAGMA journal_mode=MEMORY.

Los gráficos se veían increíbles. Menor IO en disco, mayor throughput, menos timeouts. El equipo celebró en voz baja porque habían aprendido a no celebrar en voz alta.
Dos meses después, un host se reinició en medio de una escritura durante un parche rutinario del kernel. Nada dramático. Operaciones normales.

Tras el reinicio, la BD empezó a lanzar errores de integridad. Faltaban datos recientes y algunas relaciones de claves foráneas eran inconsistentes.
El equipo tenía copias de seguridad, pero las restauraciones implicaban perder escrituras legítimas y recientes. Acabaron reconstruyendo datos desde logs upstream y exportaciones parciales.

Lo que salió mal no fue que existan ajustes de durabilidad; fue que el requisito real del sistema era “no perder escrituras aceptadas”.
Si tu sistema requiere eso, las optimizaciones que debilitan la durabilidad no son optimizaciones—son préstamos con interés usurero.

Migraron a MySQL con logging de rehacer y binlog sincronizado adecuados. El rendimiento fue aceptable y las fallas pasaron a ser recuperables en lugar de existenciales.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Una tercera organización usaba SQLite para un agente tipo escritorio que corría en entornos de clientes. Su arquitectura tenía sentido: un agente, una BD local, baja concurrencia. Aun así lo trataban como almacenamiento de producción porque a los clientes no les importa que sea “solo un agente”.

Implementaron tres prácticas aburridas: escrituras transaccionales con límites claros, comprobaciones periódicas de integridad y una rutina de copias de seguridad que copiaba la BD usando la API de backup online de SQLite en lugar de copiar el archivo crudo durante escrituras activas.

Un cliente tuvo un disco inestable. El agente empezó a registrar errores de IO. Debido a que las comprobaciones de integridad ya estaban en su lugar, el agente detectó la corrupción temprano,
aisló la BD, restauró desde la última copia buena y reprodujo un pequeño buffer de eventos recientes desde una cola local.

El cliente nunca presentó un ticket. Internamente, el equipo vio la alerta, abrió un incidente y lo cerró con un encogimiento de hombros.
Ese encogimiento es el sonido de un buen diseño operativo.

Lección: no migras solo porque puedas. Migras porque tus requisitos operativos cambiaron. Hasta entonces, haz el trabajo aburrido de corrección.

Broma #2: La manera más fácil de hacer SQLite “altamente disponible” es imprimir el archivo de base de datos y guardarlo en dos oficinas distintas.

Errores comunes: síntoma → causa raíz → solución

1) Síntoma: timeouts aleatorios durante picos de tráfico

Causa raíz: contención de escrituras (límite de un escritor), más reintentos que amplifican la carga.

Solución: reducir frecuencia de escrituras (batching), acortar transacciones, usar WAL, añadir busy timeout con backoff. Si múltiples escritores son fundamentales: migrar a MySQL.

2) Síntoma: aparecen errores “database is locked” tras añadir un worker en background

Causa raíz: un proceso nuevo introdujo una segunda fuente de escritura; las transacciones se solapan; lectores largos impiden el checkpoint WAL.

Solución: asegurar que los escritores se serialicen (cola de un solo escritor) o mover la carga de escritura a MySQL. Auditar jobs en background por transacciones de lectura de larga duración.

3) Síntoma: el archivo WAL crece sin control

Causa raíz: el checkpoint no puede completarse porque lectores de larga duración mantienen páginas antiguas vivas; o las configuraciones de checkpoint son demasiado laxas.

Solución: eliminar transacciones de lectura largas; ejecutar checkpoints explícitos en ventanas de baja carga; considerar migrar si muchos servicios leen concurrentemente.

4) Síntoma: después de un crash/reboot faltan datos recientes

Causa raíz: ajustes de durabilidad debilitados (synchronous=OFF, sistema de archivos inadecuado, opciones de montaje inseguras) o escrituras no envueltas en transacciones.

Solución: restaurar pragmas de durabilidad, usar transacciones, mover la BD a almacenamiento local fiable. Si necesitas garantías fuertes de durabilidad a escala: MySQL con ajustes correctos de flush/binlog.

5) Síntoma: la consulta es rápida en dev, lenta en prod

Causa raíz: dataset de dev pequeño; prod tiene sesgo; índice faltante; cambio de plan de consulta; LIKE/ORDER BY pesados sin soporte de índice.

Solución: ejecutar EXPLAIN QUERY PLAN con datos tipo prod; añadir índices compuestos; reescribir consultas. No culpes a SQLite por un ordenamiento sin índice.

6) Síntoma: la CPU de la app sube cuando corre un informe

Causa raíz: SQLite se ejecuta dentro del proceso de la app; consultas pesadas roban CPU al manejo de peticiones.

Solución: aislar la carga de reporting, ejecutarla fuera de réplica (MySQL) o mover analítica a otro almacén. Como mínimo, ejecutar informes en un proceso separado con límites de recursos.

7) Síntoma: “funcionaba hasta que lo containerizamos”

Causa raíz: archivo DB colocado en filesystem overlay o volumen de red; semántica de fsync/bloqueo cambió; IO se volvió más lento.

Solución: poner SQLite en un volumen persistente local con semánticas conocidas, o dejar de usar SQLite en un escenario multi-contenedor con escrituras y migrar a MySQL.

Listas de verificación / plan paso a paso: migrar sin heroísmos

Lista de decisión: ¿deberías migrar este trimestre?

  • ¿Tienes más de un escritor en producción (múltiples procesos, workers, cron, instancias de app)?
  • ¿La latencia de cola (p95/p99) está impulsada por esperas de bloqueo o reintentos?
  • ¿Requieres alta disponibilidad (seguir aceptando escrituras tras la falla de un nodo) en lugar de “tenemos backups”?
  • ¿Necesitas operaciones online: backups sin downtime, cambios de esquema con impacto mínimo, matar consultas, observabilidad?
  • ¿Estás poniendo la BD SQLite en almacenamiento compartido o en red?

Si respondiste “sí” a dos o más, planifica la migración. Si respondiste “sí” a almacenamiento compartido o requisitos de HA, deja de debatir y prográmala.

Plan de migración: la secuencia sensata

  1. Define requisitos de corrección: durabilidad (¿qué se puede perder?), consistencia (¿lectura después de escritura?), downtime aceptable.
  2. Inventario diferencias de esquema: tipos de datos, comportamiento de autoincrement, almacenamiento de fecha/hora, restricciones, valores por defecto.
  3. Elige estrategia de migración:
    • Corte grande: detener escrituras, exportar/importar, cambiar. Simple, necesita ventana de downtime.
    • Escritura dual: escribir en ambos, leer desde SQLite, luego cambiar lecturas, luego parar SQLite. Más difícil, menos downtime.
    • Tipo captura de cambios: normalmente overkill para SQLite a menos que ya tengas un log de eventos.
  4. Levanta MySQL con valores por defecto de producción: backups, monitorización, registro de consultas lentas, ajustes sensatos de durabilidad.
  5. Backfill de datos desde SQLite a MySQL y valida conteos y checksums.
  6. Ejecuta lecturas canary: compara resultados entre SQLite y MySQL en rutas críticas.
  7. Cambia tráfico gradualmente si es posible; si no, realiza un cutover limpio con plan de rollback.
  8. Mantén SQLite en solo lectura por un periodo definido como red de seguridad, luego archiva.

Lista operativa para MySQL (para que no “mejores” y entres en caos)

  • Backups probados mediante restauración (no solo “jobs de backup en verde”).
  • Monitorización de lag de replicas (si usas réplicas), espacio en disco, hit rate del buffer pool, consultas lentas, saturación de conexiones.
  • Pool de conexiones en la app. No abras 2.000 conexiones MySQL porque descubriste hilos.
  • Plan de migración de esquema (online cuando sea posible o ventanas programadas).
  • Plan de capacidad para crecimiento de almacenamiento (InnoDB crece y prefiere espacio libre para mantenimiento).

Preguntas frecuentes

1) ¿SQLite “no es para producción”?

Es absolutamente para producción—cuando “producción” significa embebido, nodo único y baja concurrencia de escritura. Está en producción en miles de millones de dispositivos.
El movimiento equivocado es usarlo como una base de datos compartida multi-escritor para un servicio escalado horizontalmente.

2) ¿El modo WAL hace que SQLite soporte muchos escritores?

WAL ayuda a que lectores no bloqueen a escritores y viceversa, pero no convierte a SQLite en un sistema de muchos escritores. Sigues teniendo un escritor a la vez.
WAL es una mejora de concurrencia, no un coordinador de transacciones distribuidas.

3) ¿Cuál es la señal práctica más grande para migrar?

Contención persistente de escrituras bajo carga normal—timeouts, errores de bloqueo, reintentos que inflan la latencia. Si tu producto depende de escrituras, esa es la línea.

4) ¿Puedo poner SQLite en NFS si tengo cuidado?

Puedes, y algunas personas lo hacen, y algunas reciben alertas más tarde. Los bloqueos del sistema de archivos y las semánticas de durabilidad sobre almacenamiento en red son un impuesto a la fiabilidad.
Si necesitas varias máquinas, usa un servidor de base de datos.

5) ¿MySQL siempre es más rápido que SQLite?

No. SQLite puede ser más rápido para lecturas locales simples y escrituras pequeñas porque no hay salto de red y la sobrecarga es mínima.
MySQL suele ganar cuando la concurrencia, el aislamiento, el buffering y los controles operativos importan.

6) ¿Qué tal usar SQLite como caché y MySQL como fuente de verdad?

Eso puede funcionar si tratas SQLite como desechable y reconstruible. El momento en que SQLite se convierte en “el único lugar” donde vive algo, deja de ser caché y pasa a ser tu base de datos.

7) ¿Cómo evito migrar demasiado pronto?

Demuestra el cuello de botella. Si tu problema son índices faltantes o límites de transacciones descuidados, arregla eso primero. La migración está justificada cuando la arquitectura te limita, no cuando el esquema necesita cariño.

8) ¿Cuál es el enfoque de backup seguro más simple para SQLite?

Usa la API de backup online de SQLite (vía tus bindings de lenguaje o las funciones de backup del CLI) en lugar de copiar el archivo mientras hay escrituras en curso.
Valida backups restaurando y ejecutando PRAGMA integrity_check;.

9) Si migro a MySQL, ¿qué nuevo modo de fallo me golpeará primero?

Tormentas de conexiones y un pooling mal configurado. SQLite lo ocultaba porque corre en proceso. MySQL aceptará tu load test hasta que se quede sin hilos o memoria.

10) ¿Debería usar MySQL u otra cosa (Postgres, etc.)?

Si tu elección es específicamente SQLite vs MySQL, elige MySQL cuando necesites concurrencia de nivel servidor, replicación y herramientas operativas.
Si estás haciendo una evaluación más amplia, decide según las habilidades del equipo y las restricciones operativas. El punto central sigue siendo: base de datos servidor cuando necesitas propiedades de servidor.

Conclusión: pasos prácticos siguientes

SQLite no “falla”. Los equipos le piden que haga un trabajo para el que nunca fue contratada, y luego la culpan por tener límites.
MySQL no es “mejor” en abstracto. Es mejor cuando necesitas escrituras concurrentes, patrones de HA, observabilidad y controles operativos que no requieran reinventar un equipo de bases de datos.

Si no estás seguro, no discutas. Mide. Ejecuta la guía de diagnóstico rápido, realiza las tareas anteriores y busca la firma:
esperas de bloqueo, inflación de latencia de cola y riesgo operativo alrededor de durabilidad y almacenamiento compartido.

  1. Si el problema es diseño de consultas: añade índices, arregla transacciones, vuelve a probar.
  2. Si el problema es almacenamiento: arregla IO primero; los discos malos hacen que cualquier base de datos parezca incompetente.
  3. Si el problema es concurrencia y requisitos de HA: programa la migración a MySQL y trátala como un proyecto de infraestructura, no como un refactor.

El mejor momento para migrar es antes de que estés depurando una base de datos bloqueada a las 3 a.m. mientras intentas recordar si synchronous=OFF fue “solo temporal”.

← Anterior
Ubuntu 24.04: Certificados renovados pero Nginx sigue sirviendo el antiguo — por qué y cómo solucionarlo
Siguiente →
Dos oficinas en 192.168.0.0/24: conéctalas sin renumerar

Deja un comentario