La mayoría de los fallos en bases de datos no son causados por un error novedoso. Son causados por una suposición que no sabías que habías hecho—porque el valor predeterminado de la base de datos te hizo sentir seguro. Despliegas código que “funciona” en staging, y luego producción te enseña una nueva definición de la palabra “funciona”.
MariaDB y PostgreSQL son ambos excelentes. También tienen opiniones opuestas. Los valores predeterminados de MariaDB a menudo intentan mantener la aplicación en ejecución, aunque los datos queden un poco… interpretativos. Los predeterminados de PostgreSQL son más propensos a detener el tren en la estación y hacerte explicar. Esa diferencia no es académica. Cambia la frecuencia de incidentes, el tiempo de depuración y qué tipo de errores puede cometer tu equipo.
La tesis: perdonar vs castigar
En producción, “perdonar” no significa “amistoso”. Significa que la base de datos intentará interpretar tu intención, y esa interpretación puede diferir de lo que tus auditores, tus analistas o tu yo futuro querían.
MariaDB (especialmente en el terreno de compatibilidad con MySQL) tiene una larga tradición de ser permisivo: coerciones de tipos, aceptar valores de fecha raros según modos, truncar silenciosamente y, en general, intentar no romper las aplicaciones. PostgreSQL tiende a ser más estricto: lanza errores donde existe ambigüedad, exige conversiones explícitas y te obliga a enfrentar las restricciones desde temprano. Uno mantiene la aplicación en pie. El otro mantiene los datos honestos.
Ambos enfoques pueden ser correctos. El truco es elegir el modo de fallo que prefieres:
- Vibra predeterminada de MariaDB: “Almacenaremos algo; no despiertes a nadie.” Bueno cuando la disponibilidad es lo principal y tienes validación fuerte en los extremos. Peligroso cuando la corrección de los datos es crucial y no la tienes.
- Vibra predeterminada de PostgreSQL: “Esto es ambiguo; corrige tu código.” Bueno cuando importa la corrección y quieres que los errores se manifiesten pronto. Peligroso cuando no puedes tolerar errores de escritura y tu aplicación no está diseñada para reintentos adecuados.
Broma 1: MariaDB a veces se siente como una base de datos que dice “claro” como un barman cansado dice “claro”. Te arrepentirás por la mañana.
Si construyes sistemas que afectan dinero, inventario, control de acceso, cumplimiento o cualquier cosa que termine en una reunión con Legal: prefiere “castigar los errores temprano”. Si ejecutas una canalización de ingesta de eventos de alto volumen con validación sólida en los bordes: los predeterminados permisivos pueden estar bien, siempre que los hagas explícitos y observables.
Hechos interesantes e historia (las partes que aún importan)
- MariaDB existe por el miedo a la gravedad de adquisiciones: se creó tras la adquisición de MySQL (Sun, luego Oracle), para mantener un fork liderado por la comunidad con la compatibilidad como promesa central.
- Las raíces de PostgreSQL son académicas pero su cultura es conservadora: desciende de la investigación POSTGRES; el proyecto históricamente priorizó la corrección y los estándares sobre el “lo que funcione”.
- Los comportamientos permisivos de MySQL moldearon un ecosistema: ORMs y bases de código de aplicaciones aprendieron a confiar en coerciones silenciosas. Portar esas apps a Postgres suele sacar a la luz “bugs” que siempre estuvieron ahí.
- InnoDB se convirtió en motor por defecto en MySQL porque MyISAM perdía discusiones: transacciones y recuperación tras fallos dejan de ser opcionales una vez has tenido tu primer incidente real.
- La MVCC de PostgreSQL es central en su historia operativa: evita el bloqueo lector/escritor en muchos casos, pero “lo paga” con requisitos de vacuum y posible bloat si ignoras el mantenimiento.
- El ecosistema de MariaDB se dividió con el tiempo: las características y los predeterminados divergieron entre MariaDB, MySQL y Percona Server; las recetas operativas no siempre son portables aun cuando la sintaxis SQL lo sea.
- La cultura de extensiones de Postgres es una superpotencia: capacidades operativas clave suelen llegar como extensiones (para estadísticas, indexación, herramientas de particionado). Eso es flexibilidad, pero también más perillas que gestionar.
- El cumplimiento del estándar SQL es una herramienta práctica, no un trofeo: Postgres se acerca más; eso cambia cuán predecible es la semántica de tus consultas durante migraciones y entre controladores.
Predeterminados que deciden tu destino
Modos SQL y coerciones “útiles”
Si hay una razón por la que MariaDB se siente permisivo, es que a menudo aceptará tu entrada y la remodelará a algo almacenable. A veces te avisa; otras veces no lo notarás porque tu librería cliente no muestra advertencias. Probablemente tu monitorización tampoco las rastrea.
MariaDB: el comportamiento está fuertemente influenciado por sql_mode. Sin modos estrictos, los inserts que desbordan, truncan o contienen valores inválidos pueden tener éxito con advertencias. En modos estrictos, muchos de esos casos se convierten en errores duros. La base de datos puede ser indulgente o punitiva; el problema es que muchos entornos no hacen esa elección explícitamente.
PostgreSQL: tiende a lanzar errores en conversiones inválidas, valores fuera de rango, timestamps inválidos, etc. Esto es operacionalmente “ruidoso”, pero también evita que malos datos se conviertan en un incidente silencioso y a largo plazo que aparezca en analíticas dentro de seis meses.
Qué cambia esto en la práctica:
- En MariaDB, debes tratar
sql_modecomo un contrato de producción y versionarlo como código. - En Postgres, debes tratar los reintentos de la aplicación y el manejo de errores como un contrato de producción y probarlos bajo concurrencia realista.
Transacciones e aislamiento: lo que obtienes gratis
Los niveles de aislamiento por defecto no son trivia. Son la diferencia entre “nuestros contadores a veces son raros” y “tenemos un incidente de conciliación financiera”.
MariaDB (InnoDB): históricamente predetermina REPEATABLE READ. Eso reduce algunas anomalías pero introduce otras (como gap locks y comportamientos de bloqueo más sorprendentes). Puede hacer que ciertos patrones de escritura sean proclives a deadlocks bajo concurrencia de maneras que los equipos no anticipan.
PostgreSQL: predetermina READ COMMITTED. Es sensato para muchas cargas de trabajo y reduce algunas sorpresas de bloqueo, pero permite lecturas no repetibles a menos que uses un aislamiento más fuerte o bloqueo explícito.
Implicación operativa: ninguno de los predeterminados es “seguro” en el sentido moral. Son seguros para diferentes tipos de suposiciones de desarrolladores. Si tu app asume “lo leí y se mantiene igual hasta que hago commit”, Postgres puede violar eso a menos que lo pidas. Si tu app asume “el bloqueo es simple”, InnoDB te castigará con un deadlock que sólo ocurre en pico de tráfico.
Conjuntos de caracteres y collations: la corrupción silenciosa
Los problemas de encoding y collation rara vez te despiertan a las 2 a.m. Te despertarán durante una migración, o durante una fusión de datos por adquisición, o cuando empiezas a atender usuarios en un idioma nuevo. Eso es peor, porque ahora el problema es político además de técnico.
MariaDB: los despliegues históricamente variaron: latin1 fue común, utf8 (que en contextos antiguos de MySQL/MariaDB podía significar “UTF-8 de 3 bytes”) causó sorpresas, y los predeterminados más nuevos tienden hacia utf8mb4 según la versión y configuración. Las collations difieren, y algunas collations han cambiado comportamiento entre versiones.
PostgreSQL: la codificación se establece al crear la base de datos y suele ser UTF-8 en despliegues modernos, pero las collations dependen de la configuración del SO/ICU. Eso es excelente hasta que restauras en una imagen de SO distinta y el ordenamiento cambia sutilmente.
Mi fuerte consejo: elige UTF-8 de extremo a extremo, y en ambos sistemas haz explícitas las collations para el ordenamiento/búsqueda visible al usuario. “Collation por defecto” no es una estrategia; es una dependencia no probada.
Restricciones, claves foráneas y cómo aparecen los errores
Las restricciones son cómo transformas el comportamiento de la base de datos de “mejor esfuerzo” a “contrato”. Si no defines el contrato, igual tienes uno—solo que no sabes cuál es.
PostgreSQL: las restricciones son de primera clase y se usan comúnmente. Existen restricciones diferidas y son útiles cuando sabes lo que haces. Postgres hará cumplir tus reglas y rechazará las violaciones de manera ruidosa.
MariaDB: soporta claves foráneas en InnoDB, pero muchos ecosistemas MariaDB/MySQL históricamente las usaron poco, a menudo por historia con MyISAM, preocupaciones de replicación o temor al overhead de escritura. El resultado: integridad reforzada en el código de la aplicación—hasta que no lo está.
Si te mudas de MariaDB a Postgres, a menudo descubrirás que tus datos ya violan las restricciones que siempre supusiste verdaderas. Postgres no está “siendo difícil”. Está haciendo el trabajo que no le pediste a MariaDB.
Autovacuum vs purge: el mantenimiento es política
La MVCC de PostgreSQL significa que las filas no desaparecen al borrarlas; se convierten en tuplas muertas y deben ser vacuumadas. Autovacuum se ejecuta por defecto, pero está ajustado para lo “promedio” y tu carga raramente es promedio. Si lo ignoras, el rendimiento se degrada lentamente y luego de golpe, y el primer síntoma suele ser “disco lleno” o “consultas inexplicablemente lentas”.
En InnoDB, tienes dinámicas de mantenimiento diferentes: undo logs, hilos de purge y longitud de lista de historial. No es “sin vacuum”, es “plomería distinta”. En ambos sistemas, los predeterminados buscan seguridad y generalidad, no tu patrón de escritura específico.
Aquí es donde la división perdonar/castigar se invierte. Postgres mantendrá tus lecturas consistentes, pero te castigará después si no vigilas el vacuum. MariaDB/InnoDB puede seguir andando, pero puede castigarte con parones largos, lag de purge o problemas de replicación cuando la limpieza interna no puede mantenerse al día.
Predeterminados de replicación: consistencia vs disponibilidad
La replicación es donde los valores predeterminados se vuelven política: ¿quieres confirmar escrituras antes de que sean durables en una réplica? ¿Quieres que el primario espere? ¿Quieres lecturas desde réplicas que pueden estar desactualizadas?
Replicación en MariaDB: a menudo empieza siendo asíncrona. Es fácil de configurar, fácil de malinterpretar y extremadamente buena produciendo postmortems de “perdimos datos en el failover” si no la diseñas explícitamente.
Replicación de streaming en PostgreSQL: también suele ser asíncrona por defecto, pero la replicación síncrona es un patrón común y bien soportado. La base de datos te obliga a ser explícito sobre si quieres semánticas de “sin pérdida de datos”, y te hará pagar por ello en latencia.
Los predeterminados no te salvan de la física. Solo eligen quién se sorprende primero: tú o tus clientes.
Guion de diagnóstico rápido
Cuando la latencia sube o los errores aumentan, necesitas una secuencia que funcione bajo presión. No una discusión filosófica. Este es el orden que tiende a encontrar el cuello de botella más rápido en MariaDB y Postgres.
Primero: confirma qué tipo de dolor tienes (CPU, IO, bloqueos o saturación)
- ¿Las consultas son lentas o están esperando? “Esperando” suele significar bloqueos, IO o saturación del pool de conexiones.
- ¿La CPU de la BD está al máximo? Piensa en mal plan, índice faltante, deriva de estadísticas o demasiados workers concurrentes.
- ¿Está el IO al máximo? Piensa en bloat/vacuum, checkpoints, buffer pool demasiado pequeño, lecturas aleatorias o vecinos ruidosos.
- ¿Están las conexiones al máximo? Piensa en mala configuración del pool, clientes desbocados o consultas lentas reteniendo conexiones.
Segundo: identifica al principal culpable de espera/bloqueo
- PostgreSQL: mira
pg_stat_activityy eventos de espera; encuentra bloqueadores y transacciones largas. - MariaDB: mira
SHOW PROCESSLIST, el estado de InnoDB y vistas de transacciones/bloqueos.
Tercero: valida la salud de mantenimiento del motor de almacenamiento
- PostgreSQL: progreso de autovacuum, tuplas muertas, sospecha de bloat, presión de checkpoints.
- MariaDB/InnoDB: longitud de lista de historial, lag de purge, tasa de aciertos del buffer pool, ajustes de capacidad IO.
Cuarto: revisa replicación y backups (porque la respuesta al incidente empeora si no puedes hacer failover)
- Lag y errores de replicación.
- Tendencias de espacio en disco y crecimiento de WAL/binlog.
- Frescura de backups y confianza en restauraciones.
Idea parafraseada (atribuida): Gene Kim suele impulsar la verdad operativa de que la retroalimentación rápida vence a los heroísmos; acorta el ciclo y evitas las sorpresas a medianoche.
Tareas prácticas con comandos (qué significa la salida, qué decisión tomar)
Estas son las comprobaciones básicas que ejecutas durante incidentes, trabajos de rendimiento y migraciones. Cada tarea incluye un comando, salida de ejemplo, qué significa y la decisión que impulsa.
Task 1: Identify MariaDB SQL mode (strictness and coercion risk)
cr0x@server:~$ mysql -uroot -p -e "SELECT @@GLOBAL.sql_mode, @@SESSION.sql_mode\G"
Enter password:
*************************** 1. row ***************************
@@GLOBAL.sql_mode: STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
@@SESSION.sql_mode: STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
Qué significa: el modo estricto está habilitado, así que las truncaciones/valores inválidos tienen más probabilidades de producir errores en lugar de advertencias.
Decisión: si faltan modos estrictos en producción, alinéalos con staging, actualiza el manejo de errores de la app y programa limpieza de datos antes de activar la estrictitud.
Task 2: Check MariaDB warnings after a suspicious insert (silent data damage detector)
cr0x@server:~$ mysql -uroot -p -e "INSERT INTO t(amount) VALUES ('999999999999'); SHOW WARNINGS;"
Enter password:
Level Code Message
Warning 1264 Out of range value for column 'amount' at row 1
Qué significa: el insert tuvo éxito pero el valor fue coercionado/truncado según el tipo de columna y el modo.
Decisión: si ves advertencias en rutas críticas, o bien endurece sql_mode o corrige la validación en la aplicación y comienza a recolectar advertencias en logs/métricas.
Task 3: Confirm PostgreSQL isolation level (debugging “it changed under me” reads)
cr0x@server:~$ psql -U postgres -d appdb -c "SHOW default_transaction_isolation;"
default_transaction_isolation
-----------------------------
read committed
(1 row)
Qué significa: cada sentencia ve una instantánea fresca; dentro de una transacción, sentencias posteriores pueden ver cambios committeados por otras sesiones.
Decisión: si la app asume lecturas repetibles, corrige la app o usa explícitamente REPEATABLE READ para la transacción crítica.
Task 4: Find Postgres sessions waiting on locks (fast path to “who is blocking?”)
cr0x@server:~$ psql -U postgres -d appdb -c "SELECT pid, usename, state, wait_event_type, wait_event, now()-query_start AS age, left(query,120) FROM pg_stat_activity WHERE wait_event_type IS NOT NULL ORDER BY age DESC LIMIT 10;"
pid | usename | state | wait_event_type | wait_event | age | left
------+--------+--------+-----------------+---------------+----------+-----------------------------------------------------------
8421 | app | active | Lock | transactionid | 00:03:12 | UPDATE orders SET status='paid' WHERE id=$1
8399 | app | active | Lock | relation | 00:01:48 | ALTER TABLE orders ADD COLUMN notes text
(2 rows)
Qué significa: el UPDATE está esperando un lock por ID de transacción; el ALTER espera un lock de relación. DDL puede bloquear DML.
Decisión: identifica el bloqueador a continuación (Task 5). Si es un DDL accidental en pico, termínalo o pospónlo con un plan de migración adecuado.
Task 5: Identify the blocking query in Postgres (turn “mystery” into a name)
cr0x@server:~$ psql -U postgres -d appdb -c "SELECT blocked.pid AS blocked_pid, blocker.pid AS blocker_pid, now()-blocker.query_start AS blocker_age, left(blocker.query,120) AS blocker_query FROM pg_locks blocked JOIN pg_locks blocker ON blocker.locktype=blocked.locktype AND blocker.database IS NOT DISTINCT FROM blocked.database AND blocker.relation IS NOT DISTINCT FROM blocked.relation AND blocker.page IS NOT DISTINCT FROM blocked.page AND blocker.tuple IS NOT DISTINCT FROM blocked.tuple AND blocker.virtualxid IS NOT DISTINCT FROM blocked.virtualxid AND blocker.transactionid IS NOT DISTINCT FROM blocked.transactionid AND blocker.classid IS NOT DISTINCT FROM blocked.classid AND blocker.objid IS NOT DISTINCT FROM blocked.objid AND blocker.objsubid IS NOT DISTINCT FROM blocked.objsubid AND blocker.pid!=blocked.pid JOIN pg_stat_activity blocked_act ON blocked_act.pid=blocked.pid JOIN pg_stat_activity blocker ON blocker.pid=blocker.pid WHERE NOT blocked.granted AND blocker.granted LIMIT 5;"
blocked_pid | blocker_pid | blocker_age | blocker_query
------------+------------+-------------+---------------------------------------------------------
8421 | 8302 | 00:06:40 | BEGIN; SELECT * FROM orders WHERE id=$1 FOR UPDATE;
(1 row)
Qué significa: una transacción larga que mantiene un lock de fila está bloqueando actualizaciones.
Decisión: arregla el camino de la app (falta de commit/rollback), añade timeouts y considera terminar el proceso bloqueador si es seguro y el incidente lo exige.
Task 6: MariaDB quick lock/transaction clue via processlist
cr0x@server:~$ mysql -uroot -p -e "SHOW FULL PROCESSLIST;"
Enter password:
Id User Host db Command Time State Info
21 app 10.0.2.15 appdb Query 187 Waiting for table metadata lock ALTER TABLE orders ADD COLUMN notes TEXT
34 app 10.0.2.16 appdb Query 183 Updating UPDATE orders SET status='paid' WHERE id=?
Qué significa: un DDL está esperando un metadata lock; puede estar bloqueando a otros según el timing.
Decisión: deja de hacer DDL online de forma ingenua. Usa un enfoque de migración que evite locks de metadata largos y prográmalo.
Task 7: Check InnoDB engine health quickly (purge lag and deadlock hints)
cr0x@server:~$ mysql -uroot -p -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
Enter password:
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:
=====================================
2025-12-30 01:42:11 0x7f4a4c1f9700 INNODB MONITOR OUTPUT
=====================================
...
History list length 148732
...
LATEST DETECTED DEADLOCK
------------------------
*** (1) TRANSACTION:
TRANSACTION 923847, ACTIVE 3 sec starting index read
...
Qué significa: una gran longitud de lista de historial sugiere que el purge está retrasado, a menudo por transacciones de larga duración. La sección de deadlock muestra patrones.
Decisión: busca transacciones largas, corrige el alcance de las transacciones en la app y ajusta purge/capacidad IO solo después de eliminar transacciones largas.
Task 8: PostgreSQL autovacuum health by table (bloat early warning)
cr0x@server:~$ psql -U postgres -d appdb -c "SELECT relname, n_live_tup, n_dead_tup, last_autovacuum, vacuum_count, autovacuum_count FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10;"
relname | n_live_tup | n_dead_tup | last_autovacuum | vacuum_count | autovacuum_count
------------+------------+------------+------------------------+--------------+------------------
events | 81234921 | 23999110 | 2025-12-29 22:11:05+00 | 0 | 148
orders | 392110 | 88112 | 2025-12-29 20:03:12+00 | 2 | 67
(2 rows)
Qué significa: muchas tuplas muertas en events y autovacuums frecuentes. Puede estar al día—o apenas.
Decisión: si las tuplas muertas se mantienen altas, ajusta umbrales de autovacuum por tabla, considera particionar y revisa transacciones largas que impidan la limpieza.
Task 9: Postgres “are we checkpoint thrashing?” sanity check
cr0x@server:~$ psql -U postgres -d appdb -c "SELECT checkpoints_timed, checkpoints_req, checkpoint_write_time, checkpoint_sync_time, buffers_checkpoint FROM pg_stat_bgwriter;"
checkpoints_timed | checkpoints_req | checkpoint_write_time | checkpoint_sync_time | buffers_checkpoint
------------------+-----------------+-----------------------+----------------------+-------------------
214 | 987 | 8241132 | 119883 | 18933211
(1 row)
Qué significa: muchas solicitudes de checkpoint en relación con los temporizados: el sistema está forzando checkpoints, a menudo por volumen de WAL y ajustes.
Decisión: ajusta parámetros de checkpoint/WAL y/o reduce la amplificación de escritura (índices, bloat). Verifica también la latencia del almacenamiento; discos lentos convierten el checkpointing en un outage visible.
Task 10: MariaDB buffer pool hit rate and dirty pages (is the cache working?)
cr0x@server:~$ mysql -uroot -p -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_dirty';"
Enter password:
Variable_name Value
Innodb_buffer_pool_read_requests 9134829123
Innodb_buffer_pool_reads 18239481
Variable_name Value
Innodb_buffer_pool_pages_dirty 48211
Qué significa: si Innodb_buffer_pool_reads es alto respecto a las requests, estás fallando en caché y haciendo lecturas de disco. Las páginas sucias muestran presión de escritura.
Decisión: si los misses de caché dominan y tienes RAM, aumenta el buffer pool. Si las páginas sucias se mantienen altas y el IO está saturado, revisa ajustes de flush y patrones de escritura.
Task 11: Identify top SQL in Postgres via pg_stat_statements (the “who’s burning CPU?” list)
cr0x@server:~$ psql -U postgres -d appdb -c "SELECT calls, total_exec_time::int AS total_ms, mean_exec_time::numeric(10,2) AS mean_ms, rows, left(query,100) FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 5;"
calls | total_ms | mean_ms | rows | left
-------+----------+---------+--------+----------------------------------------------------
89122 | 9123412 | 102.38 | 89122 | SELECT * FROM events WHERE tenant_id=$1 AND ts>=$2 ORDER BY ts DESC LIMIT 100
12211 | 3421901 | 280.21 | 12211 | UPDATE orders SET status=$1, updated_at=now() WHERE id=$2
(2 rows)
Qué significa: tienes una consulta “top talker” que consume tiempo de ejecución.
Decisión: ejecuta EXPLAIN (ANALYZE, BUFFERS) en la consulta principal, añade/ajusta índices o cambia la forma de la consulta. Revisa también si el ORDER BY fuerza un sort y derrame a disco.
Task 12: Identify top SQL in MariaDB using the slow query log (the boring, effective hammer)
cr0x@server:~$ sudo tail -n 20 /var/log/mysql/mysql-slow.log
# Time: 2025-12-30T01:38:44.112233Z
# User@Host: app[app] @ 10.0.2.15 []
# Query_time: 4.982 Lock_time: 0.001 Rows_sent: 100 Rows_examined: 81234921
SET timestamp=1735522724;
SELECT * FROM events WHERE tenant_id=42 ORDER BY ts DESC LIMIT 100;
Qué significa: comportamiento de escaneo completo (Rows_examined enorme) y un ORDER BY que probablemente fuerza trabajo extra.
Decisión: añade un índice compuesto (por ejemplo, (tenant_id, ts)), valida el plan con EXPLAIN y mantén el slow log en producción con umbrales sensatos.
Task 13: Postgres disk usage by table (find the real storage hogs)
cr0x@server:~$ psql -U postgres -d appdb -c "SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) AS total_size FROM pg_catalog.pg_statio_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 10;"
relname | total_size
---------+-----------
events | 412 GB
orders | 38 GB
(2 rows)
Qué significa: events domina el almacenamiento. Es la tabla que dominará vacuum, comportamiento de caché y tiempo de backup.
Decisión: considera particionado, políticas de retención y revisión de índices. No afines toda la base de datos cuando una tabla es todo el clima.
Task 14: MariaDB replication lag check (failover readiness)
cr0x@server:~$ mysql -uroot -p -e "SHOW SLAVE STATUS\G" | egrep 'Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running|Last_SQL_Error'
Enter password:
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 87
Last_SQL_Error:
Qué significa: la replicación funciona pero está 87 segundos atrasada. Si haces failover ahora, pierdes hasta 87 segundos de escrituras reconocidas.
Decisión: si tu RPO es cercano a cero, no finjas que esto está bien. Arregla la causa del lag (IO, transacciones largas, escrituras grandes) o adopta patrones síncronos/semisíncronos donde corresponda.
Task 15: Postgres replication status and lag (primary visibility)
cr0x@server:~$ psql -U postgres -d appdb -c "SELECT application_name, state, sync_state, write_lag, flush_lag, replay_lag FROM pg_stat_replication;"
application_name | state | sync_state | write_lag | flush_lag | replay_lag
------------------+-----------+------------+-----------+-----------+-----------
replica1 | streaming | async | 00:00:02 | 00:00:03 | 00:00:05
(1 row)
Qué significa: la réplica está ligeramente atrasada; es asíncrona, así que puede perderse algo en failover.
Decisión: decide si async es aceptable. Si no, implementa replicación síncrona para la ruta de datos crítica y presupuestarás el coste en latencia.
Task 16: Confirm Postgres long transactions (vacuum and bloat killer)
cr0x@server:~$ psql -U postgres -d appdb -c "SELECT pid, usename, now()-xact_start AS xact_age, state, left(query,120) FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_age DESC LIMIT 10;"
pid | usename | xact_age | state | left
------+--------+----------+--------+----------------------------------------------------------
8302 | app | 01:12:44 | idle | BEGIN; SELECT * FROM orders WHERE id=$1 FOR UPDATE;
(1 row)
Qué significa: una sesión idle_in_transaction está manteniendo locks e impidiendo que vacuum avance.
Decisión: termínala si es necesario, luego corrige la app (configuración del pool de conexiones, alcance de la transacción) y establece idle_in_transaction_session_timeout.
Tres mini-historias corporativas desde el frente
Mini-historia 1: El incidente causado por una suposición errónea (los predeterminados permissivos ocultaron datos malos)
Un marketplace mediano ejecutó MariaDB durante años. El equipo de la app estaba orgulloso de su “resiliencia”. Si un servicio upstream enviaba datos ligeramente malformados, la canalización de ingest seguía. No había errores visibles para clientes. Dashboards verdes. Todos dormían.
Entonces finanzas preguntó algo simple: “¿Por qué los reembolsos no coinciden con los chargebacks del último trimestre?” Los números estaban cerca, pero no lo suficiente como para ignorarlo. La investigación fue lenta y humillante porque no hubo un “despliegue malo” único. La mala práctica estaba esparcida durante meses.
La causa raíz fue una suposición: “Los timestamps inválidos fallarán los inserts”. En su entorno MariaDB, el modo SQL estricto había divergido entre clústeres. Algunos clústeres aceptaban ciertos valores de fecha inválidos con advertencias; otros los rechazaban. La canalización no verificaba advertencias. Almacenó valores coercionados que parecían fechas legítimas pero ordenaban mal. Algunos reportes downstream trataron esas fechas como verdad y produjeron desajustes sutiles.
La corrección no fue heroica. Estandarizaron sql_mode, añadieron validación upstream y escribieron un job de reparación de datos. La corrección cultural fue más difícil: dejaron de tratar “sin errores” como “correcto”. Empezaron a tratar las advertencias como un presupuesto de defectos visible que podían reducir.
Mini-historia 2: La optimización que salió mal (los predeterminados punitivos sacaron a la luz un fallo de diseño de la app)
Una compañía SaaS migró un camino caliente de MariaDB a PostgreSQL. Su razón era sólida: mejores opciones de indexado, mejor planificación de consultas para consultas tipo analítica y menos trampas de tipos. La migración fue bien en pruebas. Luego la llevaron a producción y sufrieron fallos esporádicos de escritura.
Los fallos no eran aleatorios. Correlacionaban con tráfico pico. La aplicación usaba un patrón que “usualmente funcionaba” en MariaDB: leer una fila, hacer lógica de negocio, actualizarla, repetir. Bajo los comportamientos predeterminados de MariaDB y la forma en que su código manejaba conflictos, obtenían “last write wins” más a menudo de lo que se daban cuenta. No era correcto, pero mantenía la UX en movimiento.
En Postgres, endurecieron restricciones y usaron transacciones de forma más rigurosa. Ahora los conflictos se manifestaban como errores. La base de datos no los castigaba por deporte; se negaba a mentir. Sin embargo la app no estaba construida para reintentos seguros de transacciones. Reintentaba solicitudes completas, duplicando efectos secundarios y produciendo rarezas visibles para el usuario.
Intentaron “optimizar” aumentando el tamaño del pool de conexiones y quitando algunos locks en el código. Eso lo empeoró: más concurrencia aumentó la tasa de conflictos, y el pool saturó la CPU con cambio de contexto y contención de locks. La consulta más rápida sigue siendo lenta cuando compite con 2.000 hermanas.
Se recuperaron haciendo lo aburrido: claves de idempotencia, reintentos de transacción adecuados y una política clara sobre qué operaciones requieren serialización. El rendimiento mejoró después de mejorar la corrección. Ese orden no es opcional.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día (los predeterminados no importan si puedes ver)
Una compañía relacionada con pagos ejecutaba MariaDB y Postgres en distintas líneas de producto. Tenían una regla simple: cada base de datos tenía un “manifiesto de predeterminados” en control de versiones—parámetros de arranque, modos SQL, expectativas de aislamiento, encoding/collation y los SLO operacionales que implicaban.
Esto no era trabajo sexy. No ganaba premios de arquitectura. Hizo, sin embargo, que las migraciones fueran ingeniería predecible en lugar de arqueología. Cuando un equipo quería clonar un entorno, no “copiaban lo que prod parecía”. Aplicaban el manifiesto y luego lo validaban con un script que ejecutaba consultas de saneamiento.
Un día, una actualización de la imagen del SO cambió el comportamiento de localización en una réplica de Postgres. El ordenamiento cambió sutilmente. Normalmente eso habría sido un incidente a cámara lenta: usuarios quejan del orden de resultados, soporte abre tickets, ingeniería discute relevancia. En su lugar, la validación del manifiesto detectó una discrepancia de collation inmediatamente después de aprovisionar. Sin impacto en producción. Arreglaron la imagen y siguieron.
Broma 2: El bug de base de datos más caro es el que espera educadamente hasta que estás en medio de una ventana de migración.
Errores comunes: síntoma → causa raíz → solución
1) “Los datos se ven mal, pero no hubo error”
Síntoma: discrepancias en analítica, fechas extrañas, cadenas truncadas, valores numéricos redondeados inesperadamente.
Causa raíz: MariaDB corriendo sin modos SQL estrictos; advertencias ignoradas; casts implícitos y truncamientos aceptados.
Solución: establece un sql_mode estricto consistentemente, muestra advertencias en logs de la aplicación, añade validación en la ingestión y ejecuta una auditoría única de datos por daños de coerción.
2) “Postgres lanza errores en producción tras la migración”
Síntoma: fallos de insert/update para valores que antes “funcionaban”, violaciones inesperadas de restricciones, errores de cast.
Causa raíz: el sistema antiguo aceptaba entradas ambiguas; Postgres las rechaza. La app dependía del comportamiento permisivo.
Solución: corrige tipos y validaciones en la aplicación, añade casts explícitos donde corresponda y haz de los reintentos/idempotencia parte del diseño.
3) “Deadlocks aleatorios en MariaDB en pico”
Síntoma: errores de deadlock bajo actualizaciones concurrentes; el mismo camino de código suele funcionar fuera de pico.
Causa raíz: comportamiento de locks de InnoDB bajo REPEATABLE READ, gap locks, orden inconsistente de actualizaciones y transacciones largas.
Solución: reduce el alcance de las transacciones, accede filas en orden consistente, añade índices adecuados para evitar escaneos por rango y considera cambios de aislamiento solo después de arreglar consultas/índices.
4) “Postgres se hace más lento con semanas; el disco no deja de crecer”
Síntoma: latencia en aumento, uso de disco creciente, picos de actividad de vacuum, ocasionales tormentas de IO.
Causa raíz: autovacuum no da abasto, transacciones largas impiden limpieza, bloat en tablas/índices.
Solución: termina/limita transacciones largas, ajusta autovacuum por tablas calientes, implementa particionado/retención y programa vacuum/reindex manual solo con evidencia.
5) “Ocurrió un failover y perdimos escrituras recientes”
Síntoma: tras failover, algunas transacciones recientes desaparecen; usuarios reportan actualizaciones faltantes.
Causa raíz: replicación asíncrona con lag no nulo; el failover se trató como “seguro” sin medir RPO.
Solución: mide lag de replicación continuamente, define RPO aceptable, adopta replicación síncrona donde sea necesario y ajusta la UX de la app para consistencia eventual cuando esté permitida.
6) “Tormentas de conexiones: la BD está arriba pero todo hace timeout”
Síntoma: CPU de la BD moderada, pero los timeouts en la app se disparan; el conteo de conexiones al máximo.
Causa raíz: mala configuración del pool, consultas lentas reteniendo conexiones o bucles de reintento que amplifican la carga (especialmente en Postgres donde los errores son más comunes bajo estrictitud).
Solución: limita la concurrencia, usa un pooler apropiado (y configúralo), arregla consultas lentas y añade jitter/backoff a los reintentos.
Listas de verificación / plan paso a paso
Lista A: Si ejecutas MariaDB y quieres seguridad “como Postgres”
- Estandariza
sql_modeen todos los clústeres y sesiones. Hazlo parte del aprovisionamiento, no de la tradición oral. - Convierte advertencias en señales: recógelas en logs y alerta cuando suban.
- Usa InnoDB en todas partes (si aún no lo haces). Si algo sigue en MyISAM, trátalo como deuda técnica con interés.
- Añade restricciones y claves foráneas donde el dominio es estricto (dinero, derechos, inventario). Deja que la base de datos diga “no”.
- Mide deadlocks y esperas por locks. Corrige alcance de transacciones e índices antes de ajustar perillas mágicas.
- Define encoding/collation explícitamente. Prefiere UTF-8 (
utf8mb4) y prueba migraciones con datos multilingües reales. - Haz visible el lag de replicación y vincúlalo a la política de failover. No “auto-failover” hacia pérdida de datos sin una decisión.
Lista B: Si ejecutas Postgres y quieres uptime “como MariaDB” sin perder corrección
- Diseña reintentos de la aplicación para fallos de serialización y errores transitorios. Pruébalos bajo concurrencia.
- Establece timeouts de sentencia sensatos y
idle_in_transaction_session_timeout. - Instala y usa
pg_stat_statementsy sigue las consultas top como parte de la operación estándar. - ajusta autovacuum para las tablas que importan; los predeterminados son un punto de partida, no una promesa.
- Vigila checkpoints y volumen de WAL; asegúrate de que la latencia del almacenamiento sea conocida y aceptable.
- Usa replicación síncrona solo donde el negocio lo requiera; de lo contrario sé honesto sobre el RPO.
- Para tablas grandes, planifica particionado y retención pronto. El bloat es más fácil de prevenir que de curar.
Lista C: Plan de saneidad para migraciones (MariaDB ↔ Postgres)
- Audita el uso de tipos: fechas, timestamps, booleanos y precisión numérica. Lista cada cast implícito de que depende tu app.
- Congela predeterminados: SQL mode, expectativas de aislamiento, encoding/collation, manejo de zona horaria.
- Ejecuta un escaneo de calidad de datos: fechas inválidas, valores truncados, registros huérfanos (FK faltantes), claves duplicadas donde se asumía unicidad.
- Ensaya el corte con concurrencia parecida a producción e inyección de fallos (conexiones terminadas, deadlocks, tormentas de reintentos).
- Define criterios de rollback y valida backups con una prueba real de restauración, no solo un registro de “backup exitoso”.
- Post-cutover, observa los gráficos aburridos: lag de replicación, tasa de checkpoints, tuplas muertas/lista de historial y consultas top lentas.
Preguntas frecuentes
1) ¿MariaDB es “inseguro” por defecto?
No. Pero puede ser permisivo de maneras que permiten que entren malos datos silenciosamente si no configuras modos SQL estrictos y no vigilas advertencias. Puedes hacerlo estricto; solo debes hacerlo deliberadamente.
2) ¿PostgreSQL es “mejor” porque es más estricto?
Más estricto significa que tus errores salen a la luz antes. Eso suele ser mejor para sistemas donde importa la corrección. Pero también significa que necesitas manejo robusto de errores y lógica de reintentos, o cambiarás problemas silenciosos por outages ruidosos.
3) ¿Qué base de datos tiene un aislamiento de transacción predeterminado más seguro?
Ninguna es universalmente más segura. REPEATABLE READ de MariaDB puede reducir algunas anomalías pero aumentar sorpresas de locking. READ COMMITTED de Postgres es más simple pero permite lecturas no repetibles a menos que pidas aislamiento más fuerte.
4) ¿Por qué las migraciones de MariaDB a Postgres “encuentran” problemas de datos?
Porque Postgres aplica tipos y restricciones con más rigor y no coerciona silenciosamente tanto. La migración actúa como suero de la verdad para tus suposiciones.
5) ¿Cuál es la mayor trampa operativa en los predeterminados de Postgres?
Asumir que autovacuum siempre dará abasto. Usualmente lo hace—hasta que tienes una tabla caliente, transacciones largas o una carga de escritura intensa. Entonces se vuelve un problema de rendimiento y disco.
6) ¿Cuál es la mayor trampa operativa en los predeterminados de MariaDB?
Asumir que “se insertó” significa “se almacenó lo que quería”. Sin modos estrictos y visibilidad de advertencias, puedes acumular datos incorrectos mientras todo parece saludable.
7) ¿Puedo lograr failover sin pérdida de datos con cualquiera de los dos?
Sí, pero debes pagarlo. Necesitas semánticas de replicación síncrona (o garantías equivalentes) y un modelo de aplicación que tolere la latencia añadida. La replicación asíncrona es una decisión de RPO, no una comodidad por defecto.
8) ¿Debería confiar en los valores predeterminados de la base de datos?
Confía en los predeterminados solo cuando los hayas documentado, validado y monitorizado el comportamiento que implican. De lo contrario estás confiando en el valor que se instaló por casualidad.
9) Si solo puedo hacer una mejora este mes, ¿cuál debería ser?
Haz explícitos y observables tus predeterminados: modos SQL / expectativas de aislamiento, encoding/collation y un dashboard “consultas top + esperas por locks + lag de replicación”. Evita tanto la corrupción lenta como los outages sorpresivos.
Siguientes pasos que puedes hacer esta semana
- Escribe tus predeterminados actuales (MariaDB
sql_mode, aislamiento de Postgres, encoding/collation, zona horaria) y súbelos a control de versiones como “contrato de runtime”. - Ejecuta las consultas de diagnóstico rápido en un periodo tranquilo, no durante un incidente. Las líneas base son cómo reconoces lo “raro”.
- Elige una guardia de corrección y hazla cumplir: modos estrictos en MariaDB, o timeouts y transacciones reintentables en Postgres.
- Haz visible el mantenimiento: estadísticas de tuplas muertas/autovacuum en Postgres; longitud de lista de historial de InnoDB y deadlocks en MariaDB.
- Decide tu verdad de replicación: mide lag, define RPO aceptable y diseña failover en consecuencia. No dejes que el valor predeterminado decida durante un outage.
Si no tomas nada más: los valores predeterminados de la base de datos son parte de tu API de producción. Trátalos como código, porque se comportan como código—solo que sin pruebas unitarias.