MariaDB vs SQLite: Por qué ‘rápido en local’ puede ser lento en producción

¿Te fue útil?

Hiciste una prueba rápida en tu portátil y SQLite pareció una bala. Luego lo desplegaste,
lo pusiste detrás de un servidor web, añadiste unos workers, y el rendimiento se convirtió en un informe de incidente en cámara lenta.
Esa frase de “SQLite es rápido” no estaba equivocada: simplemente estaba incompleta.

Producción no es tu portátil. En producción hay concurrencia, vecinos ruidosos, discos reales, latencia real,
backups, failovers y la encantadora costumbre de castigar cualquier suposición que no hayas documentado.
Por eso “rápido en local” se convierte en “lento en prod”, y por qué MariaDB suele comportarse mejor bajo presión
incluso cuando al principio parece más pesado.

La diferencia real: arquitectura, no sintaxis SQL

SQLite es una biblioteca que enlazas dentro de tu aplicación. MariaDB es un servidor al que te conectas.
Esa sola frase explica la mayoría de las sorpresas de rendimiento.

Con SQLite, tu “base de datos” es un archivo y el motor DB se ejecuta dentro de tu proceso. Las lecturas pueden ser absurdamente rápidas
porque te evitas saltos de red, handshakes de autenticación, cambios de contexto entre cliente y servidor,
y a menudo aciertas la caché de páginas del SO. En una máquina de desarrollador tranquila con un solo usuario, es casi injusto.

MariaDB es un proceso separado (a menudo en un host distinto). Paga una sobrecarga que puedes medir: TCP, gestión de conexiones,
parseo de consultas, planificación de hilos. Pero a cambio te ofrece propiedades que también mides: control de concurrencia robusto, buffering maduro y flushing en background,
patrones de E/S afinados, instrumentación y perillas operativas diseñadas para la realidad multi-tenant.

Si tu carga es “una app pequeña, un escritor, muchas lecturas, personal de ops mínimo”, SQLite puede ser la elección correcta.
Si tu carga es “tráfico web, ráfagas, múltiples workers, trabajos en background, migraciones y alguien pidiendo HA”,
MariaDB deja de ser opcional y pasa a ser higiene básica.

Dos métricas de rendimiento que la gente confunde (y luego sufre)

  • Latencia: cuánto tarda una solicitud. “Mi consulta se siente lenta”.
  • Throughput: cuántas solicitudes completas por segundo bajo concurrencia. “El sistema colapsa a 20 RPS”.

SQLite puede verse genial en pruebas de latencia y luego caer en throughput tan pronto como añades escritores concurrentes.
MariaDB puede parecer más lento en una prueba de hilo único y luego conservar la dignidad cuando el gráfico de tráfico se dispara.

Una cita, porque sigue siendo cierta

Everything fails, all the time. — Werner Vogels

Si diseñas pensando que el archivo siempre estará disponible, el lock siempre será uncontendido y el disco siempre será rápido,
has diseñado para una demo.

Hechos e historia interesantes (lo que explica el presente)

  1. El diseño “sin servidor” de SQLite fue una elección deliberada: es un motor de base de datos embebido, no un daemon, lo que lo hace ideal para apps y appliances.
  2. SQLite almacena toda la base de datos en un único archivo (más archivos secundarios como WAL o el journal), lo que es conveniente operativamente pero sensible al rendimiento.
  3. SQLite usa bloqueo de escritura a nivel de base de datos (con matices bajo WAL), lo que simplifica la corrección pero limita escrituras concurrentes.
  4. El modo WAL (Write-Ahead Logging) en SQLite mejora drásticamente la concurrencia lectura/escritura al separar lectores del escritor—hasta cierto punto.
  5. MariaDB es un fork de MySQL, creado cuando Oracle adquirió Sun; mantuvo vivo el ecosistema MySQL con un modelo de gobernanza distinto.
  6. InnoDB se convirtió en el motor por defecto porque proporcionó transacciones ACID y recuperación ante fallos de una forma que escalaba mejor que motores antiguos.
  7. El buffer pool de InnoDB es una de las mayores razones por las que MariaDB puede superar en cargas reales: es una caché diseñada con comportamiento más inteligente que “lo que haya cacheado el SO”.
  8. SQLite está en todas partes—dispositivos móviles, navegadores, sistemas embebidos—porque la historia de despliegue (una biblioteca, un archivo) es imbatible cuando la carga encaja.

Por qué SQLite suele ser fulminante en local

Las pruebas locales son el escenario de mejores condiciones para SQLite. Tu app y la base de datos comparten memoria y CPU, y el archivo vive en un
SSD local rápido. La caché de páginas del SO hace la mayor parte del trabajo después del calentamiento. Y como probablemente ejecutas un solo proceso,
evitas la parte más cara del diseño de SQLite: la coordinación.

1) Sin red

MariaDB necesita un socket (TCP o Unix), un protocolo y un hilo de servidor que responda. Incluso cuando es eficiente, no es gratis.
SQLite son llamadas a funciones. Por eso se siente ágil.

2) “Está cacheado” (tú solo no lo sabías)

Cuando ejecutas un benchmark dos veces y la segunda ejecución es más rápida, probablemente estés midiendo la caché de páginas del SO, no la base de datos.
SQLite lee el archivo. El kernel cachea páginas. Tu segunda ejecución es a velocidad de memoria. Enhorabuena: mediste RAM.

3) Escritor único, sin contención

Muchos entornos locales ejecutan un solo proceso. SQLite brilla ahí. Un escritor único puede hacer mucho trabajo por segundo, especialmente
cuando agrupas transacciones y no haces fsync en cada pequeña escritura.

4) Menor overhead de consulta

El planificador y motor de ejecución de SQLite son ligeros. Está optimizado para bajo overhead. Eso puede vencer a una base de datos servidor en
microbenchmarks, especialmente cuando el conjunto de datos cabe en caché y no hay estrés de concurrencia.

Broma #1: SQLite en dev es como un carro de la compra con ruedas perfectas—silencioso, suave y moviendo solo las compras de una persona.

Qué cambia en producción: concurrencia, almacenamiento y modos de fallo

Las cargas de producción no son educadas. Son multihilo, ráfagas y llenas de puntos de sincronización accidentales.
También se ejecutan sobre infraestructuras con peculiaridades: sistemas de archivos en red, capas de overlay en contenedores, IOPS limitados, vecinos ruidosos
y trabajos de backup que aparecen como por arte de magia en el peor momento.

La concurrencia convierte “rápido” en “bloqueado”

La concurrencia de SQLite es donde la mayoría de las historias de “funciona bien en local” mueren. El modo de fallo común no es “SQLite es lento”.
Es “SQLite está esperando.”

  • Varios escritores compiten por el mismo bloqueo a nivel de base de datos.
  • Transacciones de lectura de larga duración pueden impedir que los checkpoints WAL se completen, haciendo crecer el WAL y provocando paradas.
  • Los timeouts de busy están mal configurados, provocando errores inmediatos database is locked o largas esperas.

El almacenamiento no es solo “velocidad de disco”

La durabilidad de SQLite depende de las semánticas del sistema de archivos. Si el sistema de archivos es local, POSIX-ish y se comporta, estás bien.
Si el archivo vive en NFS, SMB, un sistema distribuido con bloqueo “suficientemente cercano”, o un driver de almacenamiento de contenedores con
comportamiento de fsync sorprendente, puedes obtener desde un acantilado de rendimiento hasta riesgo de corrupción.

MariaDB también depende del sistema de archivos, pero sus patrones de I/O y perillas de configuración están diseñados para manejar realidades feas:
group commit, flushing en background, doublewrite, adaptive flushing y defaults sensatos para recuperación tras fallos.

Configuraciones de durabilidad: siempre pagas en algún lugar

El benchmark de “rápido en local” a menudo desactiva silenciosamente la durabilidad. SQLite puede ejecutarse con synchronous=OFF o
NORMAL y verse increíble. Pero si en producción necesitas no perder datos durante una pérdida de energía o caída de nodo,
volverás a activar la durabilidad y descubrirás dónde se fue el tiempo: fsync.

MariaDB tiene la misma verdad, pero la amortigua de forma distinta vía redo logs de InnoDB y group commit. SQLite suele pagar más
directamente por transacción a menos que agrapes.

Observabilidad es una característica de rendimiento

Cuando algo es lento a las 2 a.m., “¿cómo vemos qué está pasando?” se vuelve la única pregunta. MariaDB tiene instrumentación madura: slow query log, performance schema (en MySQL; MariaDB tiene herramientas similares), estado del motor, estadísticas del buffer pool e información de conexiones/hilos. SQLite puede instrumentarse, pero depende mayormente de ti: métricas a nivel de aplicación,
tracing y registro cuidadoso alrededor de las transacciones.

Por qué MariaDB (InnoDB) suele ganar en producción

La ventaja de MariaDB no es que sea mágicamente más rápido en SQL. Es que está construido para acceso concurrente, durabilidad controlada
y comportamiento predecible bajo carga. SQLite está diseñado para ser embebido y correcto con una pequeña huella.
Diferentes objetivos, resultados distintos.

InnoDB maneja la concurrencia como si esperara que los humanos tomaran malas decisiones

InnoDB usa bloqueo a nivel de fila (y MVCC) para muchas cargas, permitiendo que escritores concurrentes avancen sin bloquearse
entre sí tan a menudo. El bloqueo de escritura de SQLite es más grueso. WAL ayuda a que las lecturas convivan con escrituras, pero no con múltiples escritores
de la manera en que lo hace un motor OLTP de servidor.

Buffer pool vs caché del SO: misma idea, distinto control

SQLite depende en gran medida de la caché de páginas del SO. MariaDB tiene el buffer pool de InnoDB, una caché gestionada con heurísticas internas
y visibilidad. Puedes dimensionarlo, monitorizar tasas de acierto y razonar sobre él.

La caché del SO sigue siendo buena, pero en producción quieres que la base de datos se comporte de forma consistente incluso cuando el kernel decide
que prefiere cachear otra cosa (como tus archivos de log durante una ráfaga de errores).

Durabilidad basada en log estructurado y group commit

InnoDB convierte escrituras aleatorias en escrituras más secuenciales del log, hace flush estratégicos y agrupa commits entre sesiones.
Eso importa en discos reales y volúmenes en la nube donde IOPS y latencia son realidades facturadas, no números teóricos.

Características operativas que afectan al rendimiento indirectamente

  • Manejo de conexiones: pooling, thread cache, max connections. SQLite no tiene conexiones de la misma manera, pero el modelo de proceso de tu app se vuelve el cuello de botella.
  • Replicación: no es gratis, pero permite escalar lecturas y ventanas de mantenimiento más seguras.
  • Cambios de esquema online: todavía complicados, pero posibles con las herramientas y patrones adecuados. Las migraciones en SQLite pueden bloquear el mundo si no tienes cuidado.
  • Backups: MariaDB soporta snapshots consistentes y estrategias de streaming; los backups de SQLite son posibles pero requieren manejo cuidadoso del WAL/journal y de las copias de archivo.

Broma #2: Hacer benchmark de SQLite en tu portátil y declararte victorioso es como probar un puente con una bicicleta y decir que está “listo para camiones”.

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

Estos son los cheques que hago cuando alguien dice “SQLite fue rápido en dev” o “MariaDB es lento” y espera que adivine.
Cada tarea incluye: un comando, qué significa la salida y la decisión que tomas a partir de ello.

Task 1: Identifica tu sistema de archivos y opciones de montaje (a SQLite le importa mucho)

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

Significado: ext4 local con opciones típicas. Buena línea de base para SQLite. Si ves nfs, cifs o algo exótico, trátalo como señal de alarma.

Decisión: Si el archivo DB está en almacenamiento en red, muévelo a disco local o cambia a MariaDB/Postgres. SQLite en NFS es la forma segura de ganarte un incidente.

Task 2: Revisa la latencia del disco bajo carga real (tu “DB lenta” puede ser almacenamiento lento)

cr0x@server:~$ iostat -xz 1 3
Linux 6.5.0 (server)  12/30/25  _x86_64_  (8 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.31    0.00    4.22    8.55    0.00   74.92

Device            r/s     rkB/s   rrqm/s  %rrqm r_await rareq-sz     w/s     wkB/s   wrqm/s  %wrqm w_await wareq-sz  aqu-sz  %util
nvme0n1         55.0   4200.0     1.0   1.79    1.20    76.36    90.0    8600.0     3.0   3.23   18.50    95.56    1.80  92.00

Significado: Alto w_await (18.5ms) con alto %util sugiere que el dispositivo está saturado en escrituras. SQLite y MariaDB sufren, pero SQLite puede bloquearse más cuando la frecuencia de fsync es alta.

Decisión: Reduce la frecuencia de sync mediante batching, afina las opciones de durabilidad (con cuidado), o aprovisiona almacenamiento más rápido / más IOPS. Si no puedes controlar las escrituras, migra a MariaDB y deja que amortigüe commits.

Task 3: Confirma el modo journal y ajustes sync de SQLite (el truco usual de “rápido en local”)

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

Significado: wal es bueno para la concurrencia lectura/escritura. synchronous=2 significa FULL. Durable, más lento.

Decisión: Si ves paradas en escrituras, intenta agrupar transacciones antes de tocar synchronous. Si tu negocio tolera perder unos segundos en un crash, considera synchronous=NORMAL con WAL—pero documenta el riesgo.

Task 4: Comprueba si el WAL está creciendo (a menudo causado por lectores largos)

cr0x@server:~$ ls -lh /var/lib/app/app.db*
-rw-r----- 1 app app 1.2G Dec 30 01:58 /var/lib/app/app.db
-rw-r----- 1 app app 3.8G Dec 30 02:10 /var/lib/app/app.db-wal
-rw-r----- 1 app app  32K Dec 30 01:59 /var/lib/app/app.db-shm

Significado: El WAL es más grande que el archivo DB. No es automáticamente incorrecto, pero grita “checkpoint no está ocurriendo” o “transacciones de lectura de larga duración”.

Decisión: Identifica lectores largos; añade estrategia de checkpointing; asegúrate de que las conexiones cierren transacciones con prontitud. Si tu app mantiene transacciones de lectura minutos, arregla eso primero.

Task 5: Encuentra transacciones SQLite de larga duración (nivel aplicación, pero puedes inferir)

cr0x@server:~$ lsof /var/lib/app/app.db | head
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
api      1221  app    9u   REG  259,2 1288490188  77 /var/lib/app/app.db
worker   1304  app   11u   REG  259,2 1288490188  77 /var/lib/app/app.db

Significado: Procesos que mantienen el DB abierto. No es prueba de transacciones abiertas, pero te dice quién está en la sala.

Decisión: Instrumenta la app: registra duraciones de begin/commit, añade tracing alrededor del alcance de la transacción y verifica que ningún handler deje una transacción abierta durante llamadas de red.

Task 6: Busca síntomas de contención de locks en los logs

cr0x@server:~$ journalctl -u app-api -n 30 --no-pager | sed -n '1,8p'
Dec 30 02:12:01 server app-api[1221]: ERROR db: sqlite busy: database is locked
Dec 30 02:12:01 server app-api[1221]: WARN  db: retrying transaction attempt=1
Dec 30 02:12:02 server app-api[1221]: ERROR db: sqlite busy: database is locked
Dec 30 02:12:02 server app-api[1221]: WARN  db: retrying transaction attempt=2

Significado: Errores busy/locked. Esto es colapso de throughput: los workers pasan tiempo reintentando en lugar de hacer trabajo.

Decisión: Reduce escritores concurrentes, introduce una cola de escritura o mueve la carga de escritura a MariaDB. SQLite te está diciendo que no es un motor para concurrencia de escrituras.

Task 7: Valida el busy timeout de SQLite (evita fallos instantáneos, pero no ocultes el problema)

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

Significado: Sin busy timeout; los locks fallan inmediatamente.

Decisión: Configura un busy timeout sensato en la aplicación (p. ej., 2000–5000ms) y añade reintentos con jitter. Pero considera esto un parche; aún arregla la contención.

Task 8: Comprueba que MariaDB use InnoDB y no esté haciendo algo raro

cr0x@server:~$ mariadb -e "SHOW VARIABLES LIKE 'default_storage_engine';"
+------------------------+--------+
| Variable_name          | Value  |
+------------------------+--------+
| default_storage_engine | InnoDB |
+------------------------+--------+

Significado: InnoDB es el motor por defecto, como debe ser para OLTP en producción.

Decisión: Si ves otro engine, detente y corrígelo primero. Afinar un motor equivocado es teatro de rendimiento.

Task 9: Revisa el tamaño del buffer pool de MariaDB (error común en producción)

cr0x@server:~$ mariadb -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"
+-------------------------+-----------+
| Variable_name           | Value     |
+-------------------------+-----------+
| innodb_buffer_pool_size | 134217728 |
+-------------------------+-----------+

Significado: 128MB de buffer pool. Bien para bases minúsculas, trágico para algo real.

Decisión: Aumenta el tamaño (a menudo 60–75% de la RAM en un host DB dedicado). Luego verifica tasa de aciertos y presión de memoria. No dejes sin memoria al SO ni a otros servicios.

Task 10: Detecta presión de flushing en disco de MariaDB (impuesto de fsync)

cr0x@server:~$ mariadb -e "SHOW GLOBAL STATUS LIKE 'Innodb_data_fsyncs'; SHOW GLOBAL STATUS LIKE 'Innodb_os_log_fsyncs';"
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Innodb_data_fsyncs| 98321 |
+-------------------+-------+
+---------------------+-------+
| Variable_name       | Value |
+---------------------+-------+
| Innodb_os_log_fsyncs| 22110 |
+---------------------+-------+

Significado: Los contadores de fsyncs pueden indicar cuánto estás golpeando el almacenamiento. Por sí solos no son “malos”, pero picos correlacionados con latencia son sospechosos.

Decisión: Si la latencia se alinea con picos de fsync, revisa innodb_flush_log_at_trx_commit y la latencia de almacenamiento. No cambies la durabilidad a la ligera; prefiere arreglar I/O y agrupar escrituras.

Task 11: Encuentra consultas lentas en MariaDB (no adivines)

cr0x@server:~$ mariadb -e "SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'long_query_time';"
+----------------+-------+
| Variable_name  | Value |
+----------------+-------+
| slow_query_log | ON    |
+----------------+-------+
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| long_query_time | 1.000 |
+-----------------+-------+

Significado: Slow query log activado; el umbral es 1s.

Decisión: Usa el slow log para atacar a los verdaderos culpables. Si no lo tienes activado en producción, estás depurando a ciegas.

Task 12: Inspecciona esperas y locks actuales en MariaDB (localiza contención)

cr0x@server:~$ mariadb -e "SHOW FULL PROCESSLIST;" | head
Id	User	Host	db	Command	Time	State	Info
48	app	10.0.2.41:51244	prod	Query	12	Waiting for table metadata lock	ALTER TABLE events ADD COLUMN x INT
51	app	10.0.2.40:49810	prod	Query	11	Sending data	SELECT * FROM events WHERE created_at > NOW() - INTERVAL 1 DAY

Significado: Un lock de metadata por un ALTER bloquea consultas. Este es un clásico slowdown en producción que no tiene nada que ver con “la BD es lenta” y todo que ver con la estrategia de DDL online.

Decisión: Deja de hacer cambios de esquema bloqueantes en horas pico. Usa métodos de cambio de esquema online o programa ventanas de mantenimiento.

Task 13: Valida que no estés ejecutando SQLite en un filesystem overlay por accidente

cr0x@server:~$ stat -f -c "%T" /var/lib/app/app.db
ext2/ext3

Significado: Está en familia ext (ext4 se muestra igual). Si ves overlayfs, podrías estar dentro de la capa writable de un contenedor con semánticas de fsync desagradables.

Decisión: Pon el archivo DB en un volumen persistente real, no en la capa writable del contenedor.

Task 14: Revisa límites de descriptores abiertos (SQLite + muchos workers = sorpresa)

cr0x@server:~$ ulimit -n
1024

Significado: Límite de FD bajo. Bajo carga podrías agotar FDs y maldiagnosticar como “BD lenta” porque todo reintenta.

Decisión: Aumenta límites para el servicio y verifica con settings de systemd. Luego re-testea bajo carga.

Task 15: Chequeo rápido de throttling de CPU (la nube puede hacer eso)

cr0x@server:~$ mpstat 1 3
Linux 6.5.0 (server)  12/30/25  _x86_64_  (8 CPU)

01:20:11 PM  all  %usr %nice %sys %iowait %irq %soft %steal %idle
01:20:12 PM  all  18.0  0.0  6.0   2.0    0.0  1.0   0.0   73.0
01:20:13 PM  all  19.0  0.0  6.0   3.0    0.0  1.0   0.0   71.0

Significado: No hay steal obvio aquí. Si %steal es alto, tu “problema de BD” podría ser que el hypervisor te está robando CPU.

Decisión: Si steal es alto, cambia de tipo de instancia o host, o aísla la BD. Afinar consultas no arreglará una CPU que no posees.

Task 16: Confirma ajustes de durabilidad en MariaDB (no ejecutes “modo benchmark” por accidente)

cr0x@server:~$ mariadb -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     |
+---------------+-------+

Significado: Ajustes totalmente durables. Esto cuesta latencia en escrituras, pero es lo que muchos entornos de producción realmente necesitan.

Decisión: Si cambias esto, hazlo con aprobación explícita del riesgo. Si no, compra IOPS o rediseña los patrones de escritura.

Guía rápida de diagnóstico

Cuando el rendimiento cae, no tienes tiempo para filosofía. Necesitas un triage de 10 minutos que acote el campo.
Aquí está la guía que uso cuando la pregunta es “SQLite vs MariaDB—¿qué está lento y por qué?”

Primero: ¿Está esperando por locks o por I/O?

  • SQLite: busca en logs database is locked; revisa tamaño del WAL; comprueba busy timeout; identifica cuántos escritores existen.
  • MariaDB: revisa SHOW FULL PROCESSLIST por esperas de lock; revisa estado de InnoDB si es necesario; consulta el slow query log para outliers.
  • Host: ejecuta iostat -xz y busca alto await y alto %util.

Segundo: ¿Se está saturando algún recurso?

  • CPU: alto %usr/%sys, bajo iowait. Planificación de consultas, parseo JSON o cifrado pueden dominar.
  • Disco: alto iowait, alto await/util. Estás limitado por IOPS o latencia.
  • Red: MariaDB en host separado: revisa RTT y pérdidas de paquetes. Una “consulta rápida” sobre una red lenta sigue siendo lenta.
  • Hilos/workers: demasiados workers de app pueden destrozar SQLite vía contención; demasiadas conexiones a MariaDB pueden añadir cambio de contexto y presión de memoria.

Tercero: Valida la forma de la carga (la pregunta que decide la base de datos)

  • ¿Cuántos escritores concurrentes en pico?
  • ¿Las escrituras son pequeñas y frecuentes (peor caso), o agrupadas (mejor caso)?
  • ¿Ejecutas transacciones de lectura largas (reportes, exportes, analítica) contra la misma BD que OLTP?
  • ¿Necesitas HA/replicación/backups con downtime mínimo?

Si tienes múltiples escritores y no puedes serializarlos barato, deja de discutir con SQLite. Usa MariaDB (o Postgres).
Si son mayoritariamente lecturas y escrituras ocasionales que puedes agrupar, SQLite aún puede ganar.

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

1) Síntoma: “Picos aleatorios de latencia y luego todo caduca”

Causa raíz: Contención en el lock de escritor de SQLite; reintentos de la app amplifican la carga (efecto manada).

Solución: Reduce concurrencia de escritores; implementa una cola de escritor único; agrupa escrituras; habilita WAL; configura busy timeout con reintentos con jitter. Si la concurrencia de escritura es inherente, migra a MariaDB.

2) Síntoma: “El archivo WAL de SQLite crece sin control”

Causa raíz: Transacciones de lectura de larga duración impiden checkpoints; o checkpoints deshabilitados/mal configurados.

Solución: Asegura que los lectores hagan commit rápido; evita envolver grandes trabajos de reporte en una sola transacción; ejecuta checkpoints periódicos; separa analytics de OLTP.

3) Síntoma: “Era rápido hasta que pusimos la BD en almacenamiento compartido”

Causa raíz: Bloqueo y semánticas de fsync del filesystem en red; latencia y coherencia de locks matan a SQLite.

Solución: Mantén SQLite solo en disco local; de lo contrario cambia a MariaDB con almacenamiento apropiado. No “tunées” alrededor de NFS con esperanza.

4) Síntoma: “MariaDB es lenta, pero la CPU está baja y las consultas parecen simples”

Causa raíz: Buffer pool de InnoDB demasiado pequeño; todo está leyendo del disco.

Solución: Aumenta innodb_buffer_pool_size; verifica el working set; añade índices. Mide de nuevo.

5) Síntoma: “MariaDB se ralentiza durante cambios de esquema”

Causa raíz: Locks de metadata o ALTER bloqueante; transacciones de larga duración impiden la finalización del DDL.

Solución: Usa estrategias de cambio de esquema online; acorta transacciones; ejecuta DDL fuera de horas pico; asegúrate de que tu tooling de migración es seguro para producción.

6) Síntoma: “SQLite ‘funciona’ pero perdemos datos tras crashes”

Causa raíz: Ajustes de durabilidad relajados (synchronous=OFF), o almacenamiento subyacente que miente sobre flushing.

Solución: Restaura ajustes durables; verifica semánticas del filesystem y del caché del dispositivo; si necesitas garantías fuertes, migra a MariaDB con configuración durable y hardware apropiado.

7) Síntoma: “Alta latencia p99 solo en producción”

Causa raíz: Producción tiene concurrencia en ráfagas, caches fríos, jobs en background, backups, vecinos ruidosos y contención real de I/O.

Solución: Ejecuta pruebas de carga con concurrencia; añade cargas background realistas; mide p99, no promedios; añade observabilidad y alertas antes de escalar.

Tres mini-historias corporativas desde las trincheras

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

Una compañía mediana construyó un scheduler interno: tablas de cola, pool de workers, reintentos, lo habitual.
Empezó como un binario único con un SQLite embebido. Era rápido, barato y desplegable a cualquier parte.
El equipo lo amaba porque no requería coordinación con el equipo de bases de datos.

Luego el producto se hizo popular internamente y el equipo escaló los workers horizontalmente. Mismo volumen compartido, mismo archivo SQLite,
ahora montado en varios contenedores. La suposición fue “es solo un archivo; múltiples pods pueden leer y escribirlo.”
Durante una semana incluso pareció bien—hasta el lunes por la mañana.

El lunes trajo una ráfaga de tráfico más un backlog de trabajos programados. La contención de locks se transformó en cascada:
los workers reintentaban instantáneamente, lo que aumentó la presión de locks, lo que aumentó el volumen de reintentos. El sistema no estaba “lento”;
estaba mayormente esperando, y cuando hacía trabajo, martillaba la capa de almacenamiento con pequeñas transacciones intensivas en fsync.

La solución no fue un pragma ingenioso. Movieron la cola de trabajos a MariaDB, mantuvieron SQLite para caché local donde correspondía,
y añadieron una regla simple: las bases embebidas son componentes de nodo único a menos que se demuestre lo contrario.
El informe del incidente terminó con una frase que todo SRE reconoce: “Asumimos que el sistema de archivos se comportaba como un disco local.”

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

Otra organización tenía un servicio con muchas escrituras que recolectaba eventos de dispositivos de borde. Usaban MariaDB y tenían un problema doloroso de latencia en picos. Alguien miró ajustes de durabilidad y encontró ganancias fáciles: relajar fsync, aumentar throughput, desplegar.
Los gráficos de benchmark se vieron preciosos.

Dos meses después, un host se cayó. No todo el clúster—solo una máquina. El servicio falló por failover bien, pero descubrieron
una laguna de eventos perdidos. No fue enorme, pero suficiente para romper reconciliaciones y disparar alertas visibles al cliente. De repente a todos les importaba “unos segundos de datos”.

El postmortem fue incómodo porque la optimización funcionó exactamente como se diseñó. El fallo no era misterioso.
Era el clásico intercambio: menos fsyncs, más rendimiento, garantías más débiles. El error real fue tratar esa compensación como una decisión puramente de ingeniería en lugar de una decisión de producto con aceptación explícita de riesgo.

El plan de recuperación fue aburrido: restaurar ajustes durables, agrupar inserts correctamente, usar inserts multi-fila y aprovisionar
almacenamiento que pudiera manejar la tasa de escritura sin hacer trampa. El rendimiento mejoró de nuevo—esta vez sin apostar.

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

Un servicio pequeño relacionado con pagos ejecutaba MariaDB con una rutina operativa estricta: slow query log activado, revisión semanal de los mayores ofensores y la costumbre de ejecutar migraciones en staging con volumen de datos parecido a producción.
No era glamuroso. Tampoco llenaba slides.

Un desarrollador introdujo un nuevo endpoint que hacía join de dos tablas por una columna sin índice. En dev fue instantáneo porque el dataset era pequeño y todo vivía en caché. En staging con un snapshot real, fue obviamente malo: el plan era un scan completo y las estimaciones de filas eran horribles.

Como el equipo tenía la práctica de revisar planes y mirar el slow log, lo atraparon antes de liberar.
Añadieron el índice, reescribieron la consulta para que fuera sargable y el incidente nunca ocurrió.
La herramienta de rendimiento más efectiva en producción sigue siendo: “miramos los logs como adultos”.

Listas de verificación / plan paso a paso

Checklist de decisión: ¿debería esta carga ser SQLite o MariaDB?

  • Elige SQLite cuando:
    • Nodo único, disco local, sin filesystem compartido.
    • Mayormente lecturas, baja tasa de escrituras, o las escrituras pueden agruparse.
    • Quieres cero overhead operativo y puedes tolerar concurrencia limitada.
    • Puedes aceptar “escalar verticalmente” más que “escalar horizontalmente”.
  • Elige MariaDB cuando:
    • Es normal tener múltiples escritores concurrentes, no excepcional.
    • Necesitas HA/replicación, mantenimiento online y comportamiento predecible bajo carga.
    • Necesitas herramientas operativas y visibilidad sin instrumentarlo todo tú mismo.
    • Esperas crecimiento, múltiples servicios o patrones de acceso compartidos.

Paso a paso: hacer que SQLite se comporte (cuando es la herramienta adecuada)

  1. Mantén el archivo DB en disco local, no en NFS/SMB/overlay layers.
  2. Habilita modo WAL para cargas mayoritariamente de lectura con escrituras ocasionales.
  3. Agrupa escrituras: envuelve múltiples inserts/updates en una sola transacción.
  4. Configura busy timeout e implementa reintentos con jitter para evitar efectos manada.
  5. Mantén transacciones cortas; no las mantengas abiertas durante llamadas de red o cómputos largos.
  6. Vigila el crecimiento del WAL; haz checkpoints intencionales y evita lectores largos.
  7. Sé explícito sobre la durabilidad y documenta qué puedes perder en caso de crash.

Paso a paso: hacer MariaDB rápida (sin apagar la seguridad)

  1. Tamaño del buffer pool para que quepa el working set, dentro de las limitaciones de memoria.
  2. Habilita y usa el slow query log; arregla consultas, no vibras.
  3. Añade índices adecuados; revisa planes antes y después.
  4. Agrupa escrituras (multi-row inserts, prepared statements).
  5. Controla el número de conexiones con pooling; evita miles de conexiones concurrentes a menos que disfrutes de cambio de contexto.
  6. Aprovisiona IOPS y mide latencia de disco; no finjas que volúmenes gp2-like son magia.
  7. Planifica cambios de esquema para evitar tormentas de metadata locks.

Preguntas frecuentes

1) ¿SQLite es “más lento” que MariaDB?

No de forma universal. SQLite puede ser más rápido para cargas de un solo proceso, locales, con datasets pequeños o medianos y baja contención de escrituras.
MariaDB suele ser más rápido (y más estable) bajo escritores concurrentes y acceso multiusuario.

2) ¿Por qué SQLite se siente instantáneo en mi portátil?

Estás midiendo el overhead de llamada a función más la caché del SO en un SSD local con contención mínima. Es el mejor escenario.
Producción añade concurrencia, presión de I/O y costos reales de durabilidad.

3) ¿El modo WAL hace que SQLite sea “apto para producción”?

WAL mejora la concurrencia lectura/escritura, especialmente muchos lectores con un escritor. No convierte a SQLite en un servidor OLTP multi-escritor.
Si necesitas muchos escritores concurrentes, WAL no te salvará.

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

Puedes, pero apuestas tu uptime a semánticas de bloqueo y flush que no controlas. Si necesitas almacenamiento compartido,
usa una base de datos servidor. El bloqueo por archivo de SQLite no es un proyecto divertido de sistemas distribuidos.

5) ¿Por qué MariaDB es más lenta en mi benchmark de un solo hilo?

Porque estás incluyendo overhead cliente/servidor y probablemente no estás saturando lo que MariaDB está diseñado para manejar: concurrencia.
Una prueba justa usa concurrencia realista, datos realistas y mide p95/p99 además de throughput.

6) ¿Cuál es el killer de rendimiento más común de SQLite en producción?

Demasiados escritores concurrentes, normalmente por escalar workers de la app sin una estrategia de coordinación de escrituras.
El síntoma son errores de lock o largos stalls, no siempre CPU alta.

7) ¿Cuál es el killer de rendimiento más común de MariaDB en producción?

Índices malos y buffer pool de InnoDB subdimensionado, seguido de cerca por latencia de disco y cambios de esquema mal planificados.
MariaDB es indulgente, pero no es psíquica.

8) ¿Puedo mantener SQLite y aún escalar la app horizontalmente?

Sí, si evitas escrituras compartidas. Patrones comunes: cada nodo tiene su propio SQLite para caché; o canalizas escrituras a través de un servicio de escritor único.
Si cada nodo necesita escribir el mismo archivo DB, eso es un problema de base de datos servidor.

9) ¿Debería apagar fsync/durabilidad para hacerlo rápido?

Solo con aceptación explícita del riesgo de pérdida de datos, y solo después de haber arreglado el batching y el aprovisionamiento de I/O.
Deshabilitar la durabilidad por defecto es cómo conviertes problemas de rendimiento en problemas de integridad.

10) Si migro de SQLite a MariaDB, ¿qué duele usualmente?

Bordes de compatibilidad de consultas, suposiciones de aislamiento de transacciones y tareas operativas (backups, usuarios, cambios de esquema).
La recompensa es concurrencia predecible y herramientas. Presupuesta tiempo para migraciones adecuadas y afinado de planes de consulta.

Siguientes pasos que puedes hacer esta semana

Si estás ejecutando SQLite en producción y ves lentitud:

  • Confirma que el archivo DB está en disco local y no en un filesystem compartido/red/overlay.
  • Habilita WAL (si procede), configura busy timeout y agrupa escrituras.
  • Mide la contención de locks explícitamente (registra reintentos, sigue duraciones de transacción).
  • Decide si intentas hacer OLTP multi-escritor con una BD de un solo archivo. Si sí, detente y migra.

Si estás ejecutando MariaDB y ves lentitud:

  • Activa el registro de consultas lentas (o verifica que esté activo) y repara a los mayores ofensores.
  • Dimensiona correctamente el InnoDB buffer pool y verifica que no estés leyendo todo desde disco en cada petición.
  • Revisa la latencia de almacenamiento y la presión de fsync; compra IOPS antes que superstición.
  • Audita prácticas de cambios de esquema para no infligir outages por metadata locks.

La lección central: la velocidad local prueba que tu SQL funciona. La velocidad en producción prueba que tu diseño de sistema funciona.
Trátalas como hitos separados y dormirás mejor.

← Anterior
Pruebas de calidad del VPN de oficina: medir latencia, jitter y pérdida para demostrar problemas del ISP
Siguiente →
La cola de correo crece — Encuentra al culpable antes de que el correo se detenga

Deja un comentario