Despliegas un servicio, todo parece bien en staging, y luego llega el tráfico de producción con un regalo cruel: latencia.
No del tipo “necesitamos consultas más rápidas”. Del tipo “¿por qué pagamos un servidor de base de datos solo para esperar a que responda?”.
A veces la base de datos más rápida es la que no tienes que consultar por un socket. A veces el aburrido fichero en disco,
junto a tu aplicación, supera discretamente a un respetable clúster de MySQL —al menos para la carga de trabajo que realmente tienes.
La premisa de la «velocidad gratis»: dónde SQLite gana sin esforzarse
SQLite es una librería. MySQL es un servicio. Esa sola frase explica el 70% de las capturas de pantalla de “SQLite fue más rápido”
que la gente exhibe como bandera de victoria.
Si ejecutas SQLite en proceso, tu app llama a una librería, que lee un fichero local (o la caché de páginas) y devuelve filas. No hay TCP.
No hay drama de pool de conexiones. No hay planificación de hilos del servidor. No hay handshake de autenticación. No hay salto por proxy. No hay espera detrás de otros clientes.
Es el equivalente en bases de datos de ir andando a la nevera en vez de pedir comida y discutir con el intercomunicador.
Esto es lo que llamo “velocidad gratis”: velocidad que obtienes al eliminar piezas móviles, no por ser ingenioso.
Tu plan de consulta puede tener la misma complejidad; tus datos pueden ser del mismo tamaño; tu almacenamiento puede incluso ser el mismo disco.
Pero la longitud del trayecto—instrucciones de CPU, cambios de contexto, syscalls, despertadores—se acorta.
La trampa es pensar que eso significa que SQLite es “mejor” en general. No lo es. Es mejor en escenarios específicos y comunes:
aplicaciones local-first, cargas en el edge, servicios con muchas lecturas y pocas escrituras concurrentes, y sistemas donde la simplicidad operativa
es una característica, no una ocurrencia tardía.
Primer chiste corto (porque este tema lo merece): SQLite tiene una historia operativa fantástica—principalmente porque se niega a tener operaciones.
Dos modelos mentales: servidor de base de datos vs archivo de base de datos
MySQL: dominio de fallo separado, perímetro de rendimiento separado
MySQL es un servicio de red con su propio proceso, memoria, hilos y planificador de E/S. Esa separación es una superpotencia:
aisla el trabajo de la base de datos de los fallos de la aplicación, permite que muchos clientes se conecten y soporta patrones avanzados de replicación
y clustering.
Pero la separación tiene coste. Cada petición atraviesa una frontera: biblioteca cliente → socket → red del kernel → servidor → motor de almacenamiento.
Cada frontera introduce sobrecarga y puntos adicionales de encolamiento. Bajo carga, el encolamiento domina.
Por eso tu “SELECT simple por clave primaria” puede pasar de submilisegundos a decenas de milisegundos sin que la consulta cambie.
SQLite: la base de datos es un fichero, el servidor es tu proceso
SQLite corre dentro del espacio de tu proceso. Lee y escribe un único fichero de base de datos (más los ficheros de journal/WAL opcionales),
usando bloqueos del SO para coordinar concurrencia. Depende mucho de la caché de páginas del SO y se beneficia de la localidad.
El detalle clave: en muchas cargas, los datos ya están en memoria (caché de páginas). SQLite puede acceder a esas páginas sin hacer un viaje.
Cuando benchmarkeas “SELECT 1 fila,” estás midiendo llamadas a funciones y aciertos de caché—no la sobrecarga del protocolo cliente-servidor.
¿Qué estás eligiendo realmente?
Estás eligiendo cuánto quieres pagar por acceso compartido, acceso remoto y concurrencia de múltiples escritores.
MySQL está optimizado para “muchos clientes, muchos escritores, autoridad central.”
SQLite está optimizado para “una aplicación posee los datos, las lecturas son frecuentes, las escrituras están coordinadas.”
Si estás construyendo una app web con cientos de transacciones de escritura concurrentes desde muchos servidores de aplicación, SQLite no es una ganga.
Si construyes un servicio con principalmente lecturas, una tasa de escrituras moderada y ganas de eliminar infra,
SQLite puede ser increíblemente eficaz.
Hechos e historia que cambian cómo razonar sobre ambos
- SQLite (2000) fue diseñado por D. Richard Hipp para ser embebido, sin servidor y autocontenido, priorizando fiabilidad y portabilidad sobre expansión de características.
- El enfoque de «dominio público» de SQLite (prácticamente sin fricción de licencias) ayudó a que se integrara en todas partes: navegadores, teléfonos, routers, impresoras y apps de escritorio.
- MySQL (mediados de los 90) surgió en una era donde “base de datos” significaba un proceso servidor separado y donde el hosting compartido demandaba acceso multi-tenant.
- InnoDB se convirtió en el motor por defecto de MySQL porque aportó recuperación ante fallos, transacciones y bloqueo a nivel de fila a un mundo que había aprendido a temer los bloqueos de tabla.
- SQLite añadió el modo WAL para mejorar drásticamente la concurrencia de lecturas separando lectores de escritores, un punto de inflexión para cargas de trabajo reales en aplicaciones.
- SQLite usa un diseño compacto de B-tree optimizado para almacenamiento local y rendimiento predecible, por eso se comporta tan bien en dispositivos pequeños.
- La replicación de MySQL moldeó patrones operativos modernos (réplicas de lectura, failover, binlogs), pero esos beneficios vienen con responsabilidad operativa.
- La cultura de pruebas de SQLite es famosa: gran cobertura automatizada y fuzzing agresivo han hecho que sea uno de los componentes más probados en batalla que ya usas.
Cargas de trabajo: formas exactas donde SQLite vence a MySQL (y donde no)
SQLite gana cuando la “base de datos” es mayormente un índice local
Piensa: un ejecutor de tareas que sigue el estado de trabajos, un servicio que cachea respuestas de API, una herramienta CLI que guarda metadatos, una app de escritorio,
un colector en el edge que bufferiza eventos, o un servicio web de un solo nodo con un patrón de lecturas claro.
En estos escenarios, las fortalezas de MySQL—arbitraje entre múltiples clientes, acceso remoto, alta concurrencia—están infrautilizadas.
Estás pagando un taxi para cruzar la calle.
SQLite gana cuando las lecturas dominan y las escrituras están coordinadas
SQLite puede atender muchos lectores concurrentes de forma eficiente, especialmente en modo WAL. Las escrituras, sin embargo, se serializan a nivel de base de datos.
Si tu tasa de escrituras es baja o puedes canalizarlas a través de un trabajador (o un líder), puedes obtener un rendimiento excelente con baja latencia.
MySQL gana cuando necesitas concurrencia sostenida de múltiples escritores
MySQL con InnoDB está diseñado para escrituras concurrentes. Bloqueos a nivel de fila, MVCC, flushing en segundo plano y pools de búfer independientes son
ingeniería pensada para el mundo de “muchos escritores”. Si tu sistema tiene escrituras frecuentes desde muchas instancias de app y no puedes coordinarlas,
SQLite se convierte en un generador de contención de bloqueos.
MySQL gana cuando necesitas acceso remoto y propiedad compartida
Si varios servicios o equipos necesitan acceder al mismo conjunto de datos, centralizar la base de datos suele ser la decisión organizativa correcta,
no solo técnica. Querrás control de acceso, auditoría, backups y comportamiento multi-cliente predecible.
SQLite gana cuando necesitas la simplicidad de «embarcarlo con la app»
Enviar SQLite es como enviar una librería. Enviar MySQL es enviar un ecosistema: configuración, actualizaciones, backups, monitorización,
gestión de usuarios y el ocasional postmortem de «por qué está haciendo swap».
Segundo chiste corto: La migración de base de datos más fácil es la que nunca haces, por eso la gente sigue guardando SQLite como una vieja pero fiable sudadera.
El presupuesto de latencia: tu consulta es inocente, el trayecto es culpable
En producción, la mayor parte de la “lentitud de la base de datos” no es el plan de consulta. Es todo lo demás:
churn de conexiones, planificación de hilos, esperas de bloqueo, esperas de fsync, vecinos ruidosos y latencia de cola en la red.
SQLite elimina las capas de red y planificación del servidor. Esa es la velocidad gratis. Si tu carga cabe en RAM
(o está mayormente en la caché del SO), y si las escrituras son moderadas, la latencia mediana y la cola pueden mejorar drásticamente.
MySQL también puede ser extremadamente rápido—pero necesita ajuste competente y condiciones estables. SQLite necesita menos ayuda para ser decente,
porque hay menos cosas que afinar.
Qué estás realmente midiendo en benchmarks
Cuando alguien publica un benchmark diciendo “SQLite es 3× más rápido que MySQL”, pregunta:
- ¿Usan sockets localhost o red real?
- ¿Reutilizan conexiones o reconectan en cada consulta?
- ¿MySQL está haciendo fsync en cada transacción mientras SQLite no lo hace (o viceversa)?
- ¿El conjunto de datos está en caché para uno y no para el otro?
- ¿Están midiendo mono-hilo o concurrencia real?
- ¿Están usando modo WAL y ajustes de synchronous sensatos?
Si el benchmark no responde esas preguntas, es una historia, no evidencia.
Durabilidad y semántica ante fallos: qué significa “seguro” realmente
La primera pregunta seria que hacen los SRE no es “qué tan rápido”, es “qué pasa cuando el host cae en mitad de una escritura.”
Ambos, MySQL y SQLite, pueden ser durables. Ambos también pueden configurarse para dispararse en el pie.
Mandos de durabilidad de SQLite: modo de journal y synchronous
La durabilidad de SQLite está gobernada principalmente por el modo de journaling (DELETE, TRUNCATE, PERSIST, MEMORY, WAL)
y PRAGMA synchronous (OFF, NORMAL, FULL, EXTRA). El modo WAL normalmente mejora la concurrencia de lectura y el rendimiento de escritura
al anexar a un fichero WAL y hacer checkpoints más adelante.
La verdad incómoda: muchas apps «benchmarkean» SQLite con synchronous=OFF y luego se sorprenden cuando una pérdida de energía causa corrupción.
Eso no es un problema de la base de datos, es un problema de decisión.
Mandos de durabilidad de MySQL: innodb_flush_log_at_trx_commit y compañía
La durabilidad de MySQL vive en InnoDB: redo logs, doublewrite buffer, vaciado del buffer pool. El mando famoso es
innodb_flush_log_at_trx_commit. Pónlo en 1 y haces fsync en cada commit (durable, más lento).
Pónlo en 2 o 0 y cambias durabilidad por rendimiento.
Ambos sistemas te permiten elegir. La clave es elegir conscientemente, documentarlo y probar el comportamiento ante fallos.
Una idea de fiabilidad para recordar
Idea parafraseada de John Allspaw: la fiabilidad viene de diseñar para el fallo y aprender de él, no de fingir que el fallo no ocurrirá.
Concurrencia: bloqueos, MVCC y por qué “múltiples escritores” es una elección de vida
SQLite: un escritor a la vez, por diseño
SQLite permite múltiples lectores y un escritor. El modo WAL mejora la historia de lectores porque los lectores pueden seguir leyendo la instantánea antigua mientras un escritor anexa al WAL.
Pero si tienes varios escritores concurrentes, se encolan.
El resultado no es “se rompe.” El resultado son picos de latencia, timeouts por ocupado y, ocasionalmente, el tipo de estampida
donde todos reintentan a la vez y lo empeoran.
MySQL/InnoDB: diseñado para escritores concurrentes, pero no es magia
InnoDB ofrece bloqueos a nivel de fila y MVCC para permitir transacciones concurrentes. Pero la contención existe:
filas calientes, índices secundarios calientes, bloqueos por auto-incremento (según configuración), bloqueos de metadatos y presión en el buffer pool.
MySQL puede manejar alta concurrencia—hasta que tu esquema o patrón de consulta lo convierta en un simulador de esperas de bloqueo.
No obtienes concurrencia gratis; la obtienes con índices cuidados y diseño de transacciones.
Estrategias de coordinación que hacen viable a SQLite
- Arquitectura de escritor único: canaliza escrituras a través de un proceso o hilo. Los lectores pueden ser muchos.
- Escrituras por lotes: menos transacciones, commits más grandes (con moderación).
- Usar WAL + busy_timeout: reduce fallos espurios bajo contención ligera.
- Mantener las transacciones cortas: “hacer trabajo y luego escribir” es mejor que “escribir mientras piensas.”
Sobrecarga operativa: el impuesto oculto que añade MySQL (y que SQLite no tiene)
MySQL no es “difícil”, pero es un sistema. Necesita parches, backups, privilegios, gestión de replicación,
aprovisionamiento de disco, monitorización, alertas y humanos que recuerden qué hicieron hace seis meses.
El modelo operativo de SQLite es: copia el fichero; monitoriza el disco; verifica que no lo estés corrompiendo; y no dejes que diez escritores pelean en un pasillo.
Esa simplicidad vale dinero.
El otro lado: SQLite empuja la responsabilidad al límite de la aplicación. Ubicación del fichero, semánticas del sistema de ficheros, almacenamiento en contenedores
y consistencia de backups son ahora tu problema. Si lo tratas como una burbuja mágica, te tratará como un amateur.
Tres mini-historias corporativas desde las trincheras
Mini-historia 1: El incidente causado por una suposición errónea
Un equipo de producto construyó un pequeño servicio interno de dashboard. Era intensivo en lecturas y servía principalmente resultados analíticos cacheados.
Eligieron SQLite para evitar desplegar MySQL. Razonable.
Luego el servicio fue “actualizado” para soportar anotaciones de usuarios. Las escrituras eran pequeñas, pero ocurrían en ráfagas:
cada mañana, decenas de personas abrían el dashboard al mismo tiempo, creaban notas y actualizaban etiquetas.
El equipo asumió que “escrituras pequeñas” significaba “sin problema.”
El primer lunes después del lanzamiento, el servicio empezó a devolver 500s intermitentes. Los logs de la app mostraban “database is locked.”
El on-call hizo lo que hacen los on-calls: aumentó los reintentos. La tasa de error empeoró porque ahora todos los clientes reintentaban al mismo tiempo,
convirtiendo efectivamente “un escritor” en “una cola con megáfono.”
La solución no fue abandonar SQLite. La solución fue tratarlo como lo que es: una base de datos de escritor único. Introdujeron una cola de escritura
(un trabajador background que hacía las transacciones), acortaron el alcance de las transacciones, habilitaron WAL y pusieron un busy timeout sensato.
La tasa de error cayó a cero y la latencia se normalizó.
La suposición errónea no fue “SQLite es rápido.” La suposición errónea fue “la concurrencia de escritura no importa si las escrituras son pequeñas.”
La concurrencia no comparte tus sentimientos.
Mini-historia 2: La optimización que salió mal
Otro equipo ejecutaba MySQL para una store de sesiones con tasas moderadas de lectura/escritura. Perseguían la latencia p99 y notaron esperas de fsync.
Alguien sugirió bajar la durabilidad porque “las sesiones son efímeras.” Cambiaron el comportamiento de flush de InnoDB para reducir la presión de fsync.
La latencia mejoró de inmediato. El cambio se celebró.
Dos semanas después, un reinicio de host durante mantenimiento hizo desaparecer un conjunto de escrituras recientes de sesiones. Usuarios fueron desconectados,
carritos se perdieron y soporte al cliente recibió un ejercicio no planificado.
El postmortem no fue dramático. Fue aburrido, que es peor. El equipo había redefinido silenciosamente el significado de “commit.”
Optimizaron para benchmarks y se olvidaron de optimizar la experiencia de usuario.
Revirtieron el cambio de durabilidad y arreglaron la latencia de la forma correcta: logs de redo más grandes, mejor dimensionamiento del buffer pool
y batching de transacciones en la capa de aplicación. MySQL volvió a ser estable.
La lección: la durabilidad es parte de tu producto, no solo de la configuración de la base de datos.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día
Una pequeña flota de colectores edge bufferizaba telemetría localmente y subía lotes. Usaban SQLite en cada dispositivo.
Las escrituras eran frecuentes pero coordinadas: un proceso de ingestión escribía; los uploaders leían.
El equipo hizo algo dolorosamente poco sexy: probaron pérdida de energía y condiciones de disco lleno.
Durante un despliegue, un bug hizo explotar los reintentos de upload. Los dispositivos empezaron a llenar discos.
El proceso de ingestión comenzó a fallar escrituras con “disk I/O error.” Esto podría haberse convertido en pérdida silenciosa de datos,
porque las flotas edge son excelentes en fallar silenciosamente.
Pero tenían dos guardarraíles: (1) monitorización de espacio en disco con un corte duro que pausaba la ingestión antes del agotamiento total,
y (2) una comprobación periódica de integridad de la BD SQLite que corría en ventanas de baja carga.
Cuando el incidente ocurrió, los dispositivos dejaron de ingerir antes de corromper la base de datos, enviaron una señal clara de salud
y se recuperaron automáticamente una vez arreglado el bug de reintentos. No hubo ediciones manuales heroicas de ficheros de base de datos.
No hubo corrupción misteriosa.
Práctica aburrida, salvó el día: pruebas proactivas de fallos más contrapresión simple y explícita.
Tareas prácticas: comandos, salidas y decisiones (12+)
A continuación hay tareas reales que ejecutaría en producción o en un entorno de staging que realmente se parezca a producción.
Cada una incluye: comando, qué significa la salida y la decisión que impulsa.
Tarea 1: Confirmar modo de journal y nivel synchronous de SQLite
cr0x@server:~$ sqlite3 /var/lib/myapp/app.db "PRAGMA journal_mode; PRAGMA synchronous;"
wal
2
Significado: El modo WAL está activado. synchronous=2 significa FULL (enfocado en durabilidad).
Dependiendo de la compilación, los valores numéricos mapean a OFF/NORMAL/FULL/EXTRA.
Decisión: Si ves picos de latencia en escrituras, considera NORMAL para una durabilidad aceptable en muchos casos,
pero solo si tu producto puede tolerar perder la última transacción en una pérdida de energía. Documenta el trade-off.
Tarea 2: Comprobar contención de bloqueo en SQLite vía busy_timeout y prueba rápida de escritura
cr0x@server:~$ sqlite3 /var/lib/myapp/app.db "PRAGMA busy_timeout=5000; BEGIN IMMEDIATE; SELECT 'got write lock'; COMMIT;"
got write lock
Significado: El proceso adquirió el bloqueo de escritura rápidamente. Si se queda parado o da error, ya tienes un escritor activo.
Decisión: Si esto frecuentemente espera, tu arquitectura necesita una cola de escritor único o reducir el alcance de transacción.
No lo “arregles” añadiendo reintentos por todos lados.
Tarea 3: Observar patrones activos de acceso a SQLite (bloqueos de fichero y escritores)
cr0x@server:~$ sudo lsof /var/lib/myapp/app.db | head
myapp 1187 appuser 12u REG 259,0 52428800 1048577 /var/lib/myapp/app.db
myapp 1187 appuser 13u REG 259,0 8388608 1048578 /var/lib/myapp/app.db-wal
myapp 1187 appuser 14u REG 259,0 32768 1048579 /var/lib/myapp/app.db-shm
Significado: Existen y están abiertos los ficheros WAL y SHM. Eso es esperado en modo WAL.
Muchos procesos manteniendo el fichero abierto puede insinuar riesgo de múltiples escritores.
Decisión: Si ves muchos PIDs diferentes abriendo la BD para escrituras, rediseña para que solo un componente escriba.
Tarea 4: Medir crecimiento de la BD y del WAL de SQLite (presión de checkpoint)
cr0x@server:~$ ls -lh /var/lib/myapp/app.db /var/lib/myapp/app.db-wal
-rw------- 1 appuser appuser 48M Dec 30 09:41 /var/lib/myapp/app.db
-rw------- 1 appuser appuser 512M Dec 30 09:43 /var/lib/myapp/app.db-wal
Significado: El WAL es mucho más grande que la BD principal. Puede que no se estén realizando checkpoints (o están bloqueados por lectores largos).
Decisión: Investiga lecturas de larga duración; considera checkpointing manual o afinado; asegura que los lectores no mantienen snapshots eternamente.
El crecimiento del WAL puede convertirse en presión de disco y ralentizar checkpoints.
Tarea 5: Forzar e inspeccionar el resultado de un checkpoint SQLite
cr0x@server:~$ sqlite3 /var/lib/myapp/app.db "PRAGMA wal_checkpoint(TRUNCATE);"
0|0|0
Significado: Los tres números son (busy, log, checkpointed) páginas. Todos ceros a menudo significa nada que hacer o ya checkpointeado.
Si “busy” es distinto de cero, el checkpoint no pudo proceder por lectores activos.
Decisión: Si las páginas busy persisten, encuentra y arregla lectores de larga duración (conexiones con fugas, consultas en streaming).
Tarea 6: Ejecutar una comprobación rápida de integridad de SQLite (captura corrupción temprano)
cr0x@server:~$ sqlite3 /var/lib/myapp/app.db "PRAGMA quick_check;"
ok
Significado: La integridad estructural está OK. Si ves cualquier otra cosa, trátalo como urgente.
Decisión: Si aparece corrupción: detén las escrituras, haz snapshot del fichero y restaura desde un backup conocido bueno.
Luego investiga almacenamiento, comportamiento ante pérdida de energía y ajustes PRAGMA inseguros.
Tarea 7: Comprobar opciones de montaje del sistema de ficheros (las semánticas de fsync importan)
cr0x@server:~$ findmnt -no SOURCE,FSTYPE,OPTIONS /var/lib/myapp
/dev/nvme0n1p2 ext4 rw,relatime,errors=remount-ro
Significado: Estás en ext4 con opciones típicas. Si ves opciones exóticas (como deshabilitar barreras),
las suposiciones de durabilidad pueden romperse.
Decisión: Si necesitas durabilidad estricta, mantén opciones de montaje conservadoras y evita flags de “rendimiento” que no puedas explicar en un postmortem.
Tarea 8: Ver si el disco es tu verdadero cuello de botella (iostat)
cr0x@server:~$ iostat -x 1 3
Linux 6.5.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 rMB/s wMB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
nvme0n1 120.0 400.0 3.2 18.5 86.0 5.2 11.8 2.1 14.7 0.6 31.0
Significado: Un iowait no trivial y await de escritura más alto sugiere presión por fsync/flush.
Decisión: Si la latencia de disco es alta, ninguna optimización de consultas te salvará. Reduce la frecuencia de sync con cuidado (si está permitido),
agrupa escrituras o muévete a almacenamiento más rápido.
Tarea 9: Comprobar salud del servidor MySQL y pistas inmediatas de contención
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_running'; SHOW GLOBAL STATUS LIKE 'Questions';"
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| Threads_running | 64 |
+-----------------+-------+
+---------------+----------+
| Variable_name | Value |
+---------------+----------+
| Questions | 12893412 |
+---------------+----------+
Significado: Muchos hilos en ejecución pueden indicar saturación de CPU, esperas de bloqueo o una estampida.
Decisión: Si Threads_running es alto y la latencia también, revisa esperas de bloqueo y consultas lentas antes de añadir más workers de app.
Tarea 10: Identificar esperas de bloqueo en InnoDB
cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:
=====================================
2025-12-30 09:48:12 0x7f2c1c1fe700 INNODB MONITOR OUTPUT
=====================================
...
LATEST DETECTED DEADLOCK
------------------------
...
TRANSACTIONS
------------
Trx id counter 1829341
Purge done for trx's n:o < 1829200 undo n:o < 0 state: running
History list length 1234
...
Significado: Esta salida te dice si estás detectando deadlocks, acumulando history list length (undo),
o atascado en bloqueos.
Decisión: Si ves deadlocks o una lista de historial enorme, acorta transacciones y añade índices para reducir el alcance del bloqueo.
Si el “LATEST DETECTED DEADLOCK” se repite, arregla el patrón de la aplicación, no la base de datos.
Tarea 11: Comprobar la configuración de durabilidad de MySQL que afecta el comportamiento de fsync
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';"
+------------------------------+-------+
| Variable_name | Value |
+------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1 |
+------------------------------+-------+
Significado: Valor 1 significa que el redo log se vacía a disco en cada commit (durable).
Decisión: Si la latencia está dominada por fsync y puedes tolerar pérdida mínima de datos, podrías elegir 2.
Si no puedes tolerarlo, mantén 1 y arregla rendimiento en otro lugar (batching, almacenamiento, esquema).
Tarea 12: Inspeccionar churn de conexiones (un asesino silencioso de latencia en MySQL)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Connections'; SHOW GLOBAL STATUS LIKE 'Aborted_connects';"
+---------------+--------+
| Variable_name | Value |
+---------------+--------+
| Connections | 904221 |
+---------------+--------+
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| Aborted_connects | 1203 |
+------------------+-------+
Significado: Conexiones muy altas respecto al QPS estable a menudo significa que te estás conectando con demasiada frecuencia.
Aborted_connects insinúa problemas de auth/red o límites de recursos.
Decisión: Si las conexiones están churneando: arregla pooling, aumenta timeouts y deja de hacer “conectar por petición.”
Si no puedes arreglarlo pronto, el modelo in-process de SQLite puede realmente superarte para la misma carga.
Tarea 13: Verificar si MySQL lee desde disco o caché (presión en buffer pool)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';"
+-------------------------+----------+
| Variable_name | Value |
+-------------------------+----------+
| Innodb_buffer_pool_reads| 498221 |
+-------------------------+----------+
+-----------------------------------+-----------+
| Variable_name | Value |
+-----------------------------------+-----------+
| Innodb_buffer_pool_read_requests | 289223112 |
+-----------------------------------+-----------+
Significado: Buffer pool reads son lecturas físicas; read requests son lecturas lógicas. Una proporción baja es buena.
Un aumento de Innodb_buffer_pool_reads significa que estás fallando en caché y golpeando disco.
Decisión: Si fallas en caché: aumenta buffer pool, reduce el working set, añade índices o mueve datos calientes a otro lugar.
Si tu working set es pequeño, SQLite con caché del SO puede ser más simple y rápido.
Tarea 14: Comprobar dónde vive tu fichero SQLite (trampas con contenedores y almacenamiento en red)
cr0x@server:~$ df -T /var/lib/myapp/app.db
Filesystem Type 1K-blocks Used Available Use% Mounted on
/dev/nvme0n1p2 ext4 205113320 80422344 114123456 42% /
Significado: La BD está en ext4 local. Bien. Si está en NFS o en un overlay raro, tus garantías de bloqueo y fsync pueden volverse interesantes.
Decisión: Mantén SQLite en almacenamiento local a menos que entiendas profundamente las semánticas de tu filesystem en red y las hayas probado bajo fallo.
Guion de diagnóstico rápido: qué comprobar primero/segundo/tercero
Cuando “la base de datos está lenta”, tu trabajo es encontrar qué cola se está formando. No empieces reescribiendo SQL.
Empieza localizando la sala de espera.
Primero: ¿es latencia por cruzar fronteras?
- MySQL: comprueba churn de conexiones, dimensionado de pool, DNS, overhead de TLS, saltos de proxy.
- SQLite: comprueba si la BD está en disco local y si accidentalmente la pusiste en almacenamiento en red o en un volumen contendido.
Si estás haciendo connect-per-request a MySQL, ese es tu cuello de botella hasta que se demuestre lo contrario.
Si SQLite está en un montaje de red lento, ese es tu cuello de botella hasta que se demuestre lo contrario.
Segundo: ¿es contención de bloqueos?
- MySQL: inspecciona el estado de InnoDB por esperas de bloqueo/deadlocks; busca filas calientes y transacciones largas.
- SQLite: busca “database is locked,” lectores de larga duración que bloquean checkpoints y múltiples escritores.
La contención aparece como latencia en picos y timeouts mientras la CPU puede verse “bien”. Eso es encolamiento clásico.
Tercero: ¿es presión de flush/ fsync del disco?
- Ambos: comprueba iowait, disk await y si fuerzas sync en cada transacción.
- SQLite: inspecciona crecimiento del WAL; comportamiento de checkpoint; ajustes de synchronous.
- MySQL: vigila flush del redo log y misses del buffer pool.
Si el disco es lento, la base de datos es lenta. No hay club de debate requerido.
Finalmente: ahora sí, examina los planes de consulta
Los planes importan, pero en muchos incidentes reales, la “consulta mala” es la que mantiene un bloqueo demasiado tiempo o causa churn en la caché.
Arregla la espera primero. Luego optimiza el SQL.
Errores comunes: síntoma → causa raíz → solución
1) SQLite devuelve “database is locked” bajo carga
Síntoma: fallos esporádicos o esperas largas en escrituras, a menudo durante picos de tráfico.
Causa raíz: múltiples escritores concurrentes; transacciones largas; falta de busy_timeout; mala configuración de WAL; checkpoints bloqueados por lectores largos.
Solución: canalizar escrituras a través de un escritor único; habilitar WAL; establecer busy_timeout; mantener transacciones cortas; asegurar que los lectores cierren rápido; afinar checkpointing.
2) Benchmarks de SQLite lucen geniales, en producción se pierden datos tras un fallo
Síntoma: corrupción o escrituras recientes faltantes tras pérdida de energía/reboot.
Causa raíz: ajustes de durabilidad inseguros (por ejemplo, synchronous=OFF
Solución: usar WAL con un nivel de synchronous sensato; mantener SQLite en almacenamiento local; probar fallo/pérdida de energía; implementar backups y checks de integridad.
3) MySQL está “lento” pero la CPU está baja y los discos están bien
Síntoma: alta latencia de consultas con baja utilización de recursos.
Causa raíz: esperas de bloqueo, estampidas de conexiones o encolamiento en el servidor por demasiados hilos.
Solución: reducir churn de conexiones; arreglar pooling; inspeccionar esperas de bloqueo y deadlocks en InnoDB; acortar transacciones; añadir índices necesarios.
4) MySQL es rápido en benchmarks, luego doloroso en producción
Síntoma: p50 excelente, p99 terrible; paradas periódicas.
Causa raíz: misses en buffer pool, ráfagas de fsync, flushing en background o replicación que causa backpressure a nivel de aplicación.
Solución: dimensionar buffer pool; evitar índices calientes; agrupar escrituras; monitorizar comportamiento de redo y flush; asegurar que las réplicas no estén sobrecargadas si confías en ellas.
5) SQLite en Kubernetes se comporta de forma impredecible
Síntoma: latencia extraña, errores de bloqueo o datos que desaparecen tras reprogramación.
Causa raíz: fichero de BD almacenado en FS efímero de contenedor, capas overlay o un volume con semánticas de bloqueo inesperadas.
Solución: usar un volumen persistente con semánticas probadas; mantener la BD local en el nodo cuando sea posible; tratar el rescheduling de pods como un escenario de fallo y planear para ello.
6) “Podemos poner SQLite en NFS para que todos los pods lo compartan”
Síntoma: riesgos de corrupción, comportamiento raro de bloqueos, colapso de rendimiento.
Causa raíz: semánticas del filesystem en red, comportamiento del gestor de bloqueos y garantías de fsync que no coinciden con las suposiciones de SQLite.
Solución: no lo hagas. Si necesitas acceso compartido entre nodos, usa un servidor de base de datos (MySQL/Postgres) o una arquitectura local-first replicada con sincronización explícita.
Listas de verificación / plan paso a paso
Lista de decisión: ¿debería esta carga usar SQLite?
- ¿El conjunto de datos es propiedad de una sola aplicación? Si múltiples servicios independientes deben escribir, prefiere MySQL.
- ¿La concurrencia de escrituras es baja o coordinable? Si sí, SQLite sigue siendo una opción.
- ¿Puedes mantener la BD en almacenamiento local? Si no, piénsalo mucho; a SQLite no le gustan los almacenamientos “sorpresa”.
- ¿El working set es pequeño y caliente? SQLite más caché del SO puede ser brutalmente rápido.
- ¿Necesitas replicación, failover y acceso remoto? Si sí, MySQL gana salvo que construyas esas capas tú mismo.
- ¿La simplicidad operativa es un requisito principal? SQLite te da menos controles y menos alertas de pager.
Plan de despliegue de SQLite en producción (aburrido, correcto, repetible)
- Coloca la BD en un filesystem persistente y local con semánticas conocidas (ext4/xfs en discos reales).
- Habilita WAL y establece un busy timeout en el arranque de la aplicación.
- Decide la durabilidad (
synchronous) explícitamente y escríbelo en tu runbook. - Mantén transacciones cortas; no mantengas una transacción mientras haces llamadas de red.
- Implementa
PRAGMA quick_checkperiódico durante ventanas de baja carga. - Haz backups con un método consistente (p. ej., API de backup online de SQLite o snapshots controlados) y prueba restauraciones.
- Monitoriza crecimiento del WAL y espacio libre en disco; implementa contrapresión antes de llegar a disco lleno.
- Diseña para patrones many-reader, single-writer; añade una cola de escritura si es necesario.
Plan «detener la hemorragia» para MySQL cuando sospechas que está más lento de lo debido
- Verifica pooling de conexiones y reduce churn; chequea la tasa de crecimiento de Connections.
- Revisa esperas de bloqueo/deadlocks; encuentra filas calientes y transacciones largas.
- Comprueba latencia de disco e iowait; los stalls por fsync pueden dominar el p99.
- Inspecciona misses del buffer pool; si estás leyendo constantemente desde disco, ya estás en desventaja.
- Sólo entonces: afina índices y planes de consulta para las consultas realmente importantes.
Preguntas frecuentes
1) ¿Es SQLite “más rápido” que MySQL?
A veces, sí—especialmente para cargas intensivas en lecturas donde la sobrecarga de comunicación cliente-servidor domina.
Pero MySQL puede superar a SQLite bajo escrituras concurrentes intensas y cargas multi-cliente complejas.
2) ¿Cuándo gana SQLite a MySQL en producción real?
Cuando la BD es local, el working set está caliente, las lecturas dominan y las escrituras están coordinadas (escritor único o baja contención).
También cuando quieres eliminar infra y simplificar operaciones.
3) ¿Puedo usar SQLite para una app web?
Sí, si ejecutas una sola instancia o puedes enrutar las escrituras a un único escritor y servir principalmente lecturas.
Si tienes múltiples servidores de app sin estado escribiendo concurrentemente, SQLite eventualmente te castigará con contención de bloqueos.
4) ¿Es el modo WAL siempre la respuesta correcta para SQLite?
Generalmente para lecturas concurrentes, sí. WAL mejora el comportamiento lector/escritor. Pero introduce consideraciones de checkpoint y ficheros extra.
Debes monitorizar el crecimiento del WAL y asegurar que los lectores no mantengan snapshots para siempre.
5) ¿SQLite es seguro en sistemas de ficheros en red?
Trata “seguro” como “probado seguro bajo tu filesystem, opciones de montaje y modos de fallo exactos”. En la práctica, los filesystems en red compartidos son fuente frecuente de problemas.
Si necesitas acceso compartido entre nodos, usa MySQL (u otra BD servidor) en lugar de intentar que SQLite actúe como tal.
6) ¿Y los backups para SQLite?
Backupear un fichero es fácil. Backupearlo de forma consistente mientras la app escribe es el verdadero requisito.
Usa el enfoque de backup online de SQLite o detén las escrituras brevemente. Luego prueba restauraciones; un backup que no has restaurado es un rumor.
7) ¿Y las migraciones: empezar con SQLite y luego pasar a MySQL?
Es una estrategia válida si lo diseñas: mantén SQL portable cuando puedas, evita rarezas específicas de SQLite y construye un pipeline de migración temprano.
No esperes a que todo arda para inventar la exportación de datos.
8) ¿Por qué MySQL a veces tiene peor latencia tail que SQLite?
Porque tiene más puntos de encolamiento: red, planificación de hilos, esperas de bloqueo, misses del buffer pool, ráfagas de fsync, efectos de replicación.
El camino más simple de SQLite puede producir un p99 más agradable—hasta que aparezca la contención de escritura.
9) ¿Puedo escalar SQLite con réplicas?
No en el sentido de MySQL. Puedes replicar el fichero o transmitir cambios, pero entonces estás construyendo un sistema distribuido.
Si necesitas replicación y failover sencillos, MySQL es la opción madura.
10) Si SQLite es tan bueno, ¿por qué no lo usa todo el mundo para todo?
Porque “todo” incluye acceso multi-tenant, muchos escritores concurrentes, clientes remotos y primitivas operativas fuertes como replicación.
SQLite es un bisturí, no una navaja suiza.
Siguientes pasos que realmente puedes hacer esta semana
Si estás decidiendo entre MySQL y SQLite—o intentando rescatar un sistema que eligió mal—haz lo siguiente en orden:
- Escribe la forma de tu carga: proporción lectura/escritura, picos de escritores concurrentes, tamaño del dataset, requisitos de durabilidad, topología de despliegue.
- Mide los costes de frontera: churn de conexiones y latencia de red para MySQL; ubicación de almacenamiento y contención de bloqueos para SQLite.
- Ejecuta un benchmark realista: mismo dataset, caché caliente y fría, concurrencia real y ajustes de durabilidad parecidos a producción.
- Elige la arquitectura más simple que cumpla los requisitos: si SQLite los cumple, disfruta de la velocidad gratis y menos piezas móviles.
- Si necesitas MySQL, comprométete a operarlo bien: pooling, monitorización, backups y un esquema que respete la concurrencia.
El objetivo no es ganar una discusión de bases de datos. El objetivo es desplegar un sistema rápido porque es sensato—y fiable porque es honesto sobre el fallo.