Los primeros meses son fáciles. Tu aplicación se despliega, los clientes hacen clic, los datos aterrizan en algún lugar y todo el mundo se siente productivo.
Luego empieza la diversión: un panel se vuelve lento, un incidente te llama a las 02:17 y el modelo de datos “temporal” se convierte en una obligación contractual.
PostgreSQL y MongoDB pueden ambos funcionar en producción seria. Pero fallan de maneras distintas y requieren diferentes tipos de disciplina.
La pregunta no es “cuál es mejor”, sino “cuál duele menos más adelante para los hábitos, restricciones y tolerancia al riesgo de tu equipo”.
La tesis: la flexibilidad no es gratis
El argumento de MongoDB—modelo de documentos, esquema flexible, alta velocidad de desarrollo—puede ser cierto. El argumento de PostgreSQL—modelo relacional,
consistencia fuerte, cadena de herramientas operativa madura—también puede ser cierto. La trampa es asumir que las ventajas son gratuitas.
En producción, tu “elección de base de datos” es mayormente una “elección de comportamiento operativo”. PostgreSQL tiende a recompensar la estructura:
esquema explícito, restricciones, transacciones y operaciones aburridas y repetibles. MongoDB tiende a recompensar la disciplina que debes aportar tú:
consistencia en las formas de los documentos, indexación cuidadosa e higiene operativa rigurosa alrededor de los replica sets y write concerns.
Si tienes un equipo al que le gustan los límites formales—migraciones, restricciones, tipos estrictos—Postgres dolerá menos más adelante.
Si tienes un equipo que puede mantener la estructura de documentos consistente sin que la base de datos lo imponga, y realmente te beneficias
de documentos anidados, MongoDB puede doler menos más adelante. Si eliges MongoDB porque no quieres pensar en el esquema, definitivamente pensarás
en el esquema más tarde, pero entonces lo harás durante incidentes.
Una cita que vale la pena pegar en una nota: La esperanza no es una estrategia.
— General Gordon R. Sullivan.
Las bases de datos convierten la esperanza en ruido de pager.
Hechos interesantes y contexto (por qué los valores por defecto son así)
- PostgreSQL empezó como POSTGRES en UC Berkeley en los años 80, con la intención de ser extensible; ese ADN explica tipos personalizados, extensiones y una mentalidad de “caja de herramientas”.
- MongoDB surgió a finales de los 2000 como un almacén de documentos amigable para desarrolladores cuando los equipos web estaban hartos de forzar datos con forma JSON en modelos ORM rígidos.
- El modelo MVCC de PostgreSQL (control multiversión de concurrencia) es la razón por la que las lecturas no bloquean las escrituras—pero también por la que vacuum se convierte en una tarea operativa real.
- La popularidad temprana de MongoDB se apoyó en la era “web scale” cuando el sharding sonaba a inevitabilidad, no a una decisión de diseño con aristas afiladas.
- Postgres obtuvo JSONB (almacenamiento e indexación de JSON binario) para atender las necesidades modernas sin renunciar a las fortalezas relacionales; eso desvió muchas decisiones de “usar Mongo por defecto”.
- MongoDB añadió transacciones multi-documento más tarde, lo que estrechó la brecha—pero también introdujo más matices de rendimiento y afinación en cargas transaccionales.
- La replicación lógica de Postgres maduró hasta convertirse en una herramienta práctica para migraciones, replicación parcial y actualizaciones—útil cuando “el tiempo de inactividad es inaceptable” se vuelve un requisito urgente.
- La historia operativa de MongoDB es centrada en replica-set: elecciones, read preferences y write concerns son conceptos centrales, no simples perillas de afinación opcionales.
Esto no es trivia. Explica por qué cada sistema te “empuja” hacia ciertas arquitecturas—y por qué resistir esos empujes puede salir caro.
Realidad del modelado de datos: documentos, filas y las mentiras que nos contamos
La mayor ventaja de MongoDB: localidad y agregados naturales
Cuando una “entidad” en tu dominio es naturalmente jerárquica—una orden con líneas, direcciones de envío y estados—los documentos
pueden mapear bien. Una lectura obtiene todo. Una escritura actualiza el conjunto. Eso no es solo conveniencia; son menos viajes de ida y vuelta y
menos lógica de joins.
Pero los documentos traen una pregunta permanente: ¿incrustar o referenciar? Incrusta para localidad; referencia para entidades compartidas y límites de crecimiento.
Si incrustas demasiado, los documentos se inflan y las actualizaciones reescriben muchos bytes. Si referencias demasiado, reinventas joins en el código de la aplicación,
usualmente sin seguridad transaccional entre colecciones a menos que pagues el costo de la transacción.
La mayor ventaja de PostgreSQL: invariantes aplicadas
Postgres brilla cuando la corrección importa y las relaciones importan. Llaves foráneas, restricciones únicas, check constraints y triggers no son
“burocracia empresarial”. Son cómo evitas que tus datos se conviertan en un ático embrujado lleno de objetos a medio hacer.
Postgres también te da JSONB, que te permite almacenar atributos flexibles sin renunciar a la capacidad de indexarlos y consultarlos. Este es el
punto medio práctico: mantiene lo fundamental relacional y permite una “bolsa de atributos” para la cola larga. La mayoría de los sistemas tienen una cola larga.
La mentira: “Normalizaremos más tarde” / “Limpiar documentos después”
“Más tarde” es cuando tienes más datos, más dependencias, más expectativas de clientes y menos tolerancia al tiempo de inactividad. “Más tarde” es cuando cada
limpieza se convierte en una migración en vivo con riesgo real. El trabajo de esquema es como usar hilo dental: saltártelo ahorra tiempo hasta que deja de hacerlo.
Broma #1: Tu esquema es como tus recibos de impuestos—ignorarla se siente genial hasta que alguien te audita.
Transacciones y consistencia: en qué apuesta implícitamente tu app
Esta sección es donde nacen los incidentes de producción. No porque la gente no sepa qué es ACID, sino porque suponen que su sistema
se comporta como la última base de datos que usaron.
PostgreSQL: valores por defecto fuertes, herramientas afiladas
Postgres por defecto ofrece garantías transaccionales fuertes. Si actualizas dos tablas en una transacción, o se confirman ambas o ninguna.
Las restricciones actúan en el límite de la base de datos. Puedes elegir niveles de aislamiento; también puedes dispararte en el pie con
transacciones largas que inflan el historial MVCC y bloquean el vacuum. Postgres te permite ser ingenioso. A veces no deberías ser ingenioso.
MongoDB: elige tu modelo de consistencia explícitamente
MongoDB puede ser fuertemente consistente en la práctica si usas las perillas correctas: majority write concern, read concern apropiado y preferencias de lectura razonables.
También puede ser “rápido pero sorprendente” si lees desde secondaries, permites lecturas obsoletas o aceptas escrituras que no se han replicado aún. Eso no es una falla moral; es una elección. El problema es cuando es una elección accidental.
Si tu lógica de negocio requiere “dinero movido exactamente una vez”, quieres un sistema que haga difícil violar invariantes.
Postgres hace eso por defecto. MongoDB puede hacerlo, pero debes diseñarlo: claves idempotentes, sesiones transaccionales cuando sea necesario
y configuraciones operativas que coincidan con tus requisitos de corrección.
“Formas” de rendimiento: qué se vuelve rápido, qué se vuelve raro
MongoDB: lecturas rápidas hasta que tus índices no coinciden con la realidad
MongoDB puede ser extremadamente rápido cuando las consultas se alinean con los índices y los documentos están bien formados. El dolor llega cuando los equipos
añaden nuevos patrones de consulta semanalmente y la estrategia de indexación se vuelve reactiva. Peor aún: el esquema flexible significa que una consulta puede necesitar
manejar múltiples formas y campos ausentes, lo que puede llevar a índices selectivos que no se comportan como piensas.
También conocerás el “impuesto por crecimiento de documento”. Si los documentos crecen con el tiempo—añadiendo arrays, campos anidados—las actualizaciones pueden volverse más pesadas.
La fragmentación de almacenamiento y la amplificación de escritura empiezan a aparecer. Esto no es teórico; es lo que pasa cuando “cronología de perfil”
se convierte en “cronología de perfil más feed de actividad más ajustes más todo”.
PostgreSQL: los joins están bien; los planes malos no lo están
Postgres puede manejar joins a escala, pero solo si las estadísticas están sanas y las consultas son sensatas. Cuando el rendimiento cae, suele ser por:
índices faltantes, orden de joins erróneo por estadísticas obsoletas o consultas parametrizadas que producen planes genéricos que son horribles para algunos valores.
La solución suele ser visible en EXPLAIN (ANALYZE, BUFFERS). Postgres es honesto si le preguntas de la manera correcta.
La diferencia de “forma” que perjudica a operaciones
Los problemas de MongoDB a menudo parecen “CPU al 100% en el primary + fallos de caché + retraso en la replicación”. Los problemas de Postgres suelen parecer
“I/O saturado + autovacuum retrasado + una consulta haciendo algo profundamente desafortunado”. Ambos pueden diagnosticarse rápido,
pero los modelos mentales son diferentes.
Indexación: la partida silenciosa del presupuesto
Los índices son cómo compras rendimiento con almacenamiento y coste en escrituras. Ambas bases de datos te castigan por sobreindexar. Ambas te castigan más por
subindexar. La diferencia es lo fácil que es indexarte accidentalmente hasta generar dolor operativo.
Escollos de indexación en PostgreSQL
- Añadir índices “porque la lectura es lenta” sin comprobar el coste en escrituras o el bloat.
- No usar índices parciales cuando corresponde, lo que lleva a índices masivos que sirven solo a una fracción de las consultas.
- Ignorar fillfactor y las actualizaciones HOT, aumentando bloat y presión sobre vacuum.
- Olvidar que un índice también es algo que debe vaciarse y mantenerse.
Escollos de indexación en MongoDB
- Índices compuestos que no coinciden con el orden de sort + filter, provocando escaneos.
- Indexar campos que faltan en muchos documentos, creando índices de baja selectividad.
- Dejar que índices TTL actúen como un “trabajo de borrado gratis” y luego descubrir que generan presión de escritura y retraso de replicación durante ventanas de limpieza.
- Construir índices grandes en un primary ocupado sin planificar, y luego sorprenderse cuando la latencia se dispara.
Replicación y conmutación por error: dolor predecible vs dolor sorpresivo
PostgreSQL: la replicación es directa; la conmutación por error es tu responsabilidad
La replicación física de Postgres está probada en combate. Pero la conmutación por error automática no es una única característica incorporada; es una elección del ecosistema
(Patroni, repmgr, Pacemaker, servicios gestionados). Cuando la gente dice “la conmutación por error de Postgres es difícil”, normalmente quieren decir “no decidimos,
probamos ni ensayamos cómo funciona la conmutación por error”.
La replicación de Postgres también te obliga a afrontar la retención de WAL, los replication slots y el crecimiento del disco. Ignora eso y aprenderás qué tan rápido
puede llenarse un disco a las 03:00.
MongoDB: la conmutación por error está incorporada; las semánticas son tu responsabilidad
Los replica sets eligen un primary. Eso es genial. Pero tu aplicación debe manejar errores transitorios, escrituras retryables y la realidad de que el “primary” puede moverse.
Además, tu política de read preference define si los usuarios ven datos obsoletos durante ciertos modos de fallo.
El dolor operativo de MongoDB suele llegar cuando la gente trata las elecciones como eventos raros. No lo son. Las redes fallan. Los nodos se reinician.
Se aplican actualizaciones del kernel. Si el comportamiento del cliente no se prueba contra stepdowns, no tienes alta disponibilidad; tienes optimismo.
Copias de seguridad y restauración: tu único SLA real
Las copias de seguridad no son los archivos que copias. Las copias de seguridad son las restauraciones que has probado. Todo lo demás es artesanía.
PostgreSQL
El estándar de oro es backups base más archivado de WAL (recuperación punto en el tiempo). Los volcado lógicos son aceptables para sistemas más pequeños o migraciones,
pero no son una máquina del tiempo. La pregunta operativa es: ¿puedes restaurar a un clúster nuevo, verificar la consistencia y hacer el corte sin improvisar?
MongoDB
Puedes hacer backups basados en snapshots, snapshots a nivel de sistema de archivos (con cuidado, con garantías de consistencia) o backups lógicos estilo mongodump.
La parte crítica es entender si tu backup captura una vista consistente a través de shards y replica sets (si aplica).
Los clústeres shardeados complican todo. Siempre lo hacen.
Cambios de esquema: migraciones vs “simplemente lo lanzamos”
Las migraciones en Postgres son explícitas y por ende manejables
En Postgres, los cambios de esquema son un flujo de trabajo de primera clase. Eso significa que puedes revisarlos, etaparlos y aplicarlos deliberadamente.
Aun así puedes meter la pata (bloquear DDL en horas pico es clásico), pero al menos el trabajo es visible.
Los cambios de esquema en MongoDB son implícitos y por ende furtivos
En MongoDB, los cambios de esquema suelen ocurrir como efecto secundario de desplegar nuevo código. Los documentos antiguos permanecen con formas antiguas hasta que se tocan o
se backfill. Eso puede ser una ventaja—migración gradual sin un gran momento de bloqueo DDL. También puede ser una inconsistencia de larga duración que
se filtra en analítica, índices de búsqueda y comportamiento visible al cliente.
La pregunta operativa es simple: ¿prefieres una gran migración controlada o muchas migraciones parciales pequeñas con un periodo más largo de realidad mixta?
Cualquiera puede funcionar. La realidad mixta tiende a volverse permanente a menos que impongas una fecha límite de limpieza.
Tareas prácticas con comandos: qué ejecutar, qué significa, qué hacer después
Estos no son “comandos tutoriales”. Son las cosas que ejecutas cuando un gráfico se ve mal y necesitas decidir qué hacer en los próximos 15 minutos.
Cada tarea incluye: comando, qué significa la salida y la decisión que tomas.
Tareas de PostgreSQL
1) Comprobar consultas activas y si estás bloqueado
cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select pid, usename, state, wait_event_type, wait_event, now()-query_start as age, left(query,120) as q from pg_stat_activity where state <> 'idle' order by age desc;"
pid | usename | state | wait_event_type | wait_event | age | q
------+--------+--------+-----------------+---------------+----------+------------------------------------------------------------
8421 | app | active | | | 00:02:14 | SELECT ... FROM orders JOIN customers ...
9110 | app | active | Lock | relation | 00:01:09 | ALTER TABLE orders ADD COLUMN ...
8788 | app | active | Lock | transactionid | 00:00:57 | UPDATE orders SET ...
Significado: DDL de larga ejecución esperando locks, además de consultas esperando locks de transacción.
Decisión: Si el DDL está bloqueando tráfico del negocio, cancela el DDL (o al bloqueador), reprograma con una estrategia de migración más segura.
2) Encontrar quién bloquea a quién
cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select blocked.pid as blocked_pid, blocked.query as blocked_query, blocking.pid as blocking_pid, blocking.query as blocking_query from pg_locks blocked_locks join pg_stat_activity blocked on blocked.pid=blocked_locks.pid join pg_locks blocking_locks on blocking_locks.locktype=blocked_locks.locktype and blocking_locks.database is not distinct from blocked_locks.database and blocking_locks.relation is not distinct from blocked_locks.relation and blocking_locks.page is not distinct from blocked_locks.page and blocking_locks.tuple is not distinct from blocked_locks.tuple and blocking_locks.virtualxid is not distinct from blocked_locks.virtualxid and blocking_locks.transactionid is not distinct from blocked_locks.transactionid and blocking_locks.classid is not distinct from blocked_locks.classid and blocking_locks.objid is not distinct from blocked_locks.objid and blocking_locks.objsubid is not distinct from blocked_locks.objsubid and blocking_locks.pid != blocked_locks.pid join pg_stat_activity blocking on blocking.pid=blocking_locks.pid where not blocked_locks.granted;"
blocked_pid | blocked_query | blocking_pid | blocking_query
------------+---------------------+--------------+-------------------------
8788 | UPDATE orders SET.. | 6502 | BEGIN; SELECT ...;
Significado: Una transacción que mantiene locks está bloqueando actualizaciones.
Decisión: Matar la sesión bloqueadora si es seguro, o arreglar el patrón de la app (por ejemplo, transacciones largas).
3) Comprobar el retraso de replicación
cr0x@server:~$ psql -h pg01 -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
------------------+-----------+------------+-----------+-----------+------------
pg02 | streaming | async | 00:00:02 | 00:00:03 | 00:00:05
Significado: El réplica está unos segundos detrás.
Decisión: Si el retraso crece, reduce picos de escritura, comprueba I/O en la réplica o mueve lecturas pesadas fuera del primary con cuidado.
4) Identificar consultas lentas por tiempo total
cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select queryid, calls, total_exec_time::int as total_ms, mean_exec_time::int as mean_ms, rows, left(query,120) as q from pg_stat_statements order by total_exec_time desc limit 5;"
queryid | calls | total_ms | mean_ms | rows | q
----------+-------+----------+---------+-------+------------------------------------------------------------
91233123 | 18000 | 941200 | 52 | 18000 | SELECT * FROM events WHERE user_id=$1 ORDER BY ts DESC LIMIT 50
Significado: Una consulta frecuente domina el tiempo total.
Decisión: Añadir el índice correcto, reescribir la consulta o cachear en el borde de la aplicación—en función del análisis del plan.
5) Explicar una consulta con buffers para ver el dolor de I/O
cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "explain (analyze, buffers) select * from events where user_id=42 order by ts desc limit 50;"
Limit (cost=0.43..12.77 rows=50 width=128) (actual time=0.212..24.981 rows=50 loops=1)
Buffers: shared hit=120 read=1800
-> Index Scan Backward using events_user_id_ts_idx on events (cost=0.43..4212.10 rows=17000 width=128) (actual time=0.211..24.964 rows=50 loops=1)
Index Cond: (user_id = 42)
Planning Time: 0.188 ms
Execution Time: 25.041 ms
Significado: Muchas lecturas de buffers indican I/O de disco; el índice existe pero aún así se están leyendo muchas páginas.
Decisión: Considerar un índice covering, reducir el ancho de fila o mejorar la caché (RAM) si el conjunto de trabajo es mayor que la memoria.
6) Comprobar señales de bloat y salud del vacuum
cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select relname, n_live_tup, n_dead_tup, round(100.0*n_dead_tup/greatest(n_live_tup,1),2) as dead_pct, last_vacuum, last_autovacuum from pg_stat_user_tables order by n_dead_tup desc limit 5;"
relname | n_live_tup | n_dead_tup | dead_pct | last_vacuum | last_autovacuum
---------+------------+------------+----------+---------------------+---------------------
events | 94000000 | 21000000 | 22.34 | | 2025-12-30 01:02:11
Significado: Muchos tuples muertos; autovacuum corrió, pero puede estar retrasado respecto al churn.
Decisión: Ajustar autovacuum para tablas calientes, considerar particionado e investigar transacciones largas que impiden limpieza.
7) Comprobar riesgo de WAL y replication slot
cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) as retained from pg_replication_slots;"
slot_name | active | retained
-----------+--------+----------
wal_slot | f | 128 GB
Significado: Slot inactivo reteniendo 128 GB de WAL. Riesgo de llenado de disco.
Decisión: Si el consumidor se fue, elimina el slot; si no, arregla el consumidor y aumenta disco o planes de retención.
8) Comprobar presión de checkpoints
cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select checkpoints_timed, checkpoints_req, round(100.0*checkpoints_req/greatest(checkpoints_timed+checkpoints_req,1),2) as req_pct, buffers_checkpoint, buffers_backend from pg_stat_bgwriter;"
checkpoints_timed | checkpoints_req | req_pct | buffers_checkpoint | buffers_backend
------------------+-----------------+---------+--------------------+----------------
120 | 98 | 44.96 | 81234012 | 12999876
Significado: Muchas checkpoints solicitadas; buffers de backend volcados por writers—probables picos de latencia.
Decisión: Ajustar settings de checkpoint, evaluar volumen de WAL y considerar almacenamiento más rápido o agrupar escrituras.
Tareas de MongoDB
9) Verificar salud del replica set y quién es primary
cr0x@server:~$ mongosh --host mongo01:27017 --quiet --eval 'rs.status().members.map(m => ({name:m.name,stateStr:m.stateStr,health:m.health,uptime:m.uptime,lag:m.optimeDate}))'
[
{ name: 'mongo01:27017', stateStr: 'PRIMARY', health: 1, uptime: 90233, lag: ISODate('2025-12-30T02:10:01.000Z') },
{ name: 'mongo02:27017', stateStr: 'SECONDARY', health: 1, uptime: 90110, lag: ISODate('2025-12-30T02:09:58.000Z') },
{ name: 'mongo03:27017', stateStr: 'SECONDARY', health: 1, uptime: 89987, lag: ISODate('2025-12-30T02:09:57.000Z') }
]
Significado: Clúster saludable; secondaries unos segundos detrás.
Decisión: Si la salud baja o el lag crece, deja de hacer lecturas pesadas en secondaries, comprueba disco y red y verifica settings de write concern.
10) Comprobar operaciones actuales por contención de locks o trabajo lento
cr0x@server:~$ mongosh --host mongo01:27017 --quiet --eval 'db.currentOp({active:true, secs_running: {$gte: 5}}).inprog.map(op => ({opid:op.opid,secs:op.secs_running,ns:op.ns,command:Object.keys(op.command||{}),waitingForLock:op.waitingForLock}))'
[
{ opid: 12345, secs: 22, ns: 'app.events', command: [ 'aggregate' ], waitingForLock: false },
{ opid: 12388, secs: 9, ns: 'app.orders', command: [ 'update' ], waitingForLock: true }
]
Significado: Una actualización está esperando un lock; podría ser un documento hotspot o contención a nivel de colección.
Decisión: Identificar el patrón ofensivo (contador de un solo documento, actualizaciones de arrays sin límites), luego rediseñar para reducir contención.
11) Inspeccionar el perfil de consultas lentas (cuando esté habilitado) o usar explain
cr0x@server:~$ mongosh --host mongo01:27017 --quiet --eval 'db.events.find({userId:42}).sort({ts:-1}).limit(50).explain("executionStats").executionStats'
{
nReturned: 50,
totalKeysExamined: 18050,
totalDocsExamined: 18050,
executionTimeMillis: 84
}
Significado: Se examinaron 18k documentos para devolver 50. El índice no coincide con la forma de la consulta.
Decisión: Crear un índice compuesto como {userId:1, ts:-1} y volver a comprobar estadísticas; evitar añadir índices casi duplicados.
12) Comprobar retraso de replicación más directamente
cr0x@server:~$ mongosh --host mongo01:27017 --quiet --eval 'db.adminCommand({replSetGetStatus:1}).members.map(m => ({name:m.name,state:m.stateStr,lagSeconds: (new Date()-m.optimeDate)/1000}))'
[
{ name: 'mongo01:27017', state: 'PRIMARY', lagSeconds: 0 },
{ name: 'mongo02:27017', state: 'SECONDARY', lagSeconds: 3.2 },
{ name: 'mongo03:27017', state: 'SECONDARY', lagSeconds: 4.1 }
]
Significado: Los secondaries están unos segundos detrás.
Decisión: Si el lag excede tu tolerancia, reduce la carga de escritura, aumenta IOPS, ajusta journaling/checkpoint con cuidado o escala/shardea con intención.
13) Comprobar presión de cache de WiredTiger (limitador común de rendimiento)
cr0x@server:~$ mongosh --host mongo01:27017 --quiet --eval 'var s=db.serverStatus(); ({cacheBytesUsed:s.wiredTiger.cache["bytes currently in the cache"], cacheMax:s.wiredTiger.cache["maximum bytes configured"], evicted:s.wiredTiger.cache["pages evicted by application threads"]})'
{
cacheBytesUsed: 32212254720,
cacheMax: 34359738368,
evicted: 1902231
}
Significado: Caché casi al máximo, con alta evacuación. Estás limitado por I/O o con falta de memoria.
Decisión: Añadir RAM, reducir conjunto de trabajo (índices, proyecciones) o rediseñar consultas para ser más selectivas.
14) Comprobar tamaños de índice por colección (chequeo de presupuesto)
cr0x@server:~$ mongosh --host mongo01:27017 --quiet --eval 'db.events.stats().indexSizes'
{
_id_: 2147483648,
userId_1_ts_-1: 4294967296,
type_1_ts_-1: 3221225472
}
Significado: Los índices son varios GB. El conjunto de trabajo puede no caber en la caché.
Decisión: Eliminar índices no usados, consolidar o mover consultas frías a almacenamiento analítico. Los índices no son gratis.
15) Comprobar distribución de shards (si está shardeado) para detectar hotspots
cr0x@server:~$ mongosh --host mongos01:27017 --quiet --eval 'db.getSiblingDB("config").chunks.aggregate([{$match:{ns:"app.events"}},{$group:{_id:"$shard",chunks:{$sum:1}}}]).toArray()'
[
{ _id: 'shard01', chunks: 412 },
{ _id: 'shard02', chunks: 398 },
{ _id: 'shard03', chunks: 401 }
]
Significado: El conteo de chunks parece balanceado, pero el balance no es lo mismo que balance de carga.
Decisión: Si un shard está caliente, reevalúa la clave de shard y el enrutamiento de consultas; el balance de chunks por sí solo puede ser una mentira reconfortante.
16) Validar uso de disco en Postgres rápidamente (porque el almacenamiento siempre es culpable hasta que se demuestre lo contrario)
cr0x@server:~$ df -h /var/lib/postgresql
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p2 1.8T 1.6T 150G 92% /var/lib/postgresql
Significado: Estás cerca de lleno. A Postgres no le gustan los discos llenos; todo se vuelve emocionante de la peor manera.
Decisión: Identificar crecimiento (WAL, tablas, archivos temporales), mitigar inmediatamente y añadir alertas antes de que esto vuelva a suceder.
Broma #2: Lo único que escala infinitamente es la cantidad de índices que alguien propondrá durante un incidente.
Guion de diagnóstico rápido (triage de cuellos de botella)
Cuando la latencia se dispara, no empieces debatiendo arquitectura. Empieza ubicando el cuello de botella. El objetivo es identificar
si estás limitado por CPU, I/O, locks o red, y si el problema es una consulta mala o presión sistémica.
Primero: confirma el radio del blast
- ¿Es un endpoint o todo?
- ¿Son solo escrituras, solo lecturas o ambas?
- ¿La base de datos está lenta, o la app está lenta mientras la BD está bien (agotamiento de pools de conexión, reintentos, timeouts)?
Segundo: comprueba señales de saturación
- CPU: Si la CPU de la BD está al máximo y la carga promedio sube, busca unas pocas consultas costosas, índices faltantes o agregaciones descontroladas.
- I/O: Alta latencia de lectura, alta utilización de disco, picos de evacuación de caché (WiredTiger) o lecturas de buffers (Postgres) sugieren que el conjunto de trabajo no cabe en memoria.
- Locks: Muchas sesiones esperando locks, o transacciones largas, apuntan a contención o mal momento de migraciones.
- Red: Timeouts espasmódicos, elecciones de réplica o tráfico entre AZ pueden hacerse pasar por problemas de base de datos.
Tercero: identifica al principal culpable
- Postgres: Revisa
pg_stat_activitypara esperas ypg_stat_statementspara consultas pesadas; confirma conEXPLAIN (ANALYZE, BUFFERS). - MongoDB: Revisa
currentOp, consultas lentas (profiler/métricas) yexplain("executionStats"); verifica evacuación de caché y retraso de replicación.
Cuarto: elige la mitigación más segura
- Cancelar/matar la consulta o job peor si es claramente patológico.
- Escalar temporalmente (CPU/RAM/IOPS) si necesitas aire para respirar.
- Reducir carga: limitar tasa, deshabilitar endpoints pesados, pausar jobs por lotes.
- Hacer el cambio de índice más pequeño que arregle el patrón de consulta dominante (y programar el seguimiento adecuado).
Quinto: escribe el “por qué” mientras está fresco
No confíes en el tú del futuro. Captura la consulta, el plan, el tipo de espera y la mitigación. La diferencia entre un equipo que mejora y un equipo
que revive la misma caída es si puedes recrear el modo de fallo a propósito.
Tres micro-historias corporativas desde las trincheras
1) Incidente causado por una suposición errónea: “Es seguro leer desde secondaries”
Una empresa SaaS mediana usaba MongoDB con un replica set estándar. El equipo de aplicaciones quiso reducir la carga en el primary, así que cambiaron las lecturas de
ciertos endpoints “no críticos” a secondaries usando una read preference. Los endpoints eran “solo dashboards” y los dashboards “no son producción”.
Todo el mundo ha dicho esa frase. Todos han estado equivocados.
El incidente empezó como una queja de cliente: “Mis números van hacia atrás.” No fue solo un problema de UX; desencadenó alertas automáticas y hizo que los tickets de soporte se dispararan.
Los dashboards alimentaban otros sistemas: flujos de renovación, límites de uso y forecasting interno. Una lectura obsoleta se convirtió en una entrada de lógica de negocio,
que luego provocó acciones críticas.
La causa raíz no fue que MongoDB “perdiera datos”. Hizo lo que se configuró para hacer: servir lecturas desde secondaries que pueden retrasarse. Durante un periodo de alta escritura, el lag creció.
Los dashboards leían datos antiguos y luego una actualización leía datos más nuevos, y los usuarios vieron viajes en el tiempo. Nadie había documentado la ventana de obsolescencia aceptable y nadie había probado el comportamiento durante el lag.
La solución no fue heroica. Volvieron la read preference al primary para esos endpoints e introdujeron cache explícita con TTL claro y semántica “los datos pueden tener hasta N minutos”.
También añadieron monitorización del lag de replicación con umbrales de alerta ligados a la tolerancia del negocio.
Resultado: menos sorpresas y los dashboards dejaron de causar peleas.
2) Optimización que salió mal: “Desnormalizar todo en un documento”
Otra compañía usaba MongoDB para perfiles de usuario. Una revisión de rendimiento mostró demasiados viajes para ensamblar la “vista de usuario” para la app:
info de perfil, preferencias, estado de suscripción y una lista de eventos recientes. Alguien propuso la optimización obvia: incrustar todo en el documento de usuario y “actualizarlo en escrituras”.
Una lectura, listo.
Funcionó en staging. Incluso funcionó en producción por un tiempo. Luego la app añadió más “eventos recientes”, luego más historial, luego ajustes por característica.
El documento de usuario creció constantemente. Las actualizaciones empezaron a tocar documentos más grandes. La latencia subió lentamente y luego se disparó. El lag de replicación aumentó
en tráfico pico y las elecciones fueron más frecuentes porque el primary estaba bajo presión sostenida.
El verdadero reves no fue solo el tamaño. Fue la amplificación de escritura y la contención. El documento caliente de un solo usuario fue actualizado por múltiples procesos concurrentes
(eventos, facturación, feature flags), creando un embudo serial. Algunas actualizaciones reintentaron tras errores transitorios, aumentando la carga.
El sistema se volvió “lecturas rápidas, todo lo demás lento”, que es una manera elegante de decir “pager”.
Lo deshicieron hacia un híbrido: campos de perfil centrales permanecieron incrustados, pero arrays de rápido cambio y sin límite se movieron a colecciones separadas con indexación clara.
También añadieron un enfoque de stream de eventos append-only para “eventos recientes” en lugar de reescribir repetidamente un array creciente.
Las lecturas se volvieron un poco más complejas, pero el sistema dejó de autodestruirse.
3) Práctica aburrida pero correcta que salvó el día: “Ensayar restauraciones como simulacros de incendio”
Una organización regulada ejecutaba Postgres para datos transaccionales. No eran glamorosos. Tenían ventanas de cambio, runbooks y un “ensayo de restauración” semanal
a un entorno de staging que reflejaba la producción lo suficiente como para ser molesto. Los ingenieros se quejaban del tiempo perdido. Los managers del costo. Seguridad se quejaba de todo. Normal.
Un día un problema a nivel de almacenamiento corrompió un volumen en una réplica. La conmutación por error fue limpia, pero el equipo descubrió que sus “copias conocidas buenas”
faltaban una pieza pequeña pero crítica: un cambio reciente en la lógica de retención del archivo WAL. No lo descubrieron durante el incidente. Lo descubrieron porque el ensayo de restauración de la semana anterior ya había fallado y se había arreglado. Tenían un camino probado, un RPO verificado y un corte documentado.
El incidente aún dolió, porque los incidentes siempre duelen. Pero se mantuvo dentro de la tolerancia del negocio. El postmortem no fue “no teníamos backups”.
Fue “ensayamos restauraciones, así que las copias de seguridad eran reales.” Esa diferencia es la línea entre un apagón y un evento de carrera profesional.
Errores comunes: síntomas → causa raíz → solución
1) Síntoma: La CPU de Postgres está bien, pero todo está lento y los discos están calientes
Causa raíz: Tormenta de cache misses + indexación pobre + escaneos de tablas grandes, a menudo empeorado por estadísticas obsoletas o retraso del autovacuum.
Solución: Identificar consultas principales con pg_stat_statements, ejecutar EXPLAIN (ANALYZE, BUFFERS), añadir índices dirigidos y ajustar autovacuum para tablas calientes.
2) Síntoma: Bloat “aleatorio” en Postgres y crecimiento de almacenamiento, luego colapso de rendimiento
Causa raíz: Transacciones de larga duración impiden la limpieza de vacuum, creando acumulación de tuples muertos e index bloat.
Solución: Encontrar transacciones antiguas en pg_stat_activity, aplicar timeouts de transacción, rediseñar jobs por lotes y considerar particionado.
3) Síntoma: El primary de MongoDB pega la CPU en pico, el lag de replicación crece y luego elecciones
Causa raíz: Conjunto de trabajo que no cabe en caché + consultas ineficientes que escanean demasiado + patrones de escritura que afectan documentos calientes.
Solución: Usar explain("executionStats"), corregir índices compuestos, reducir crecimiento de documentos y añadir RAM/IOPS cuando esté justificado.
4) Síntoma: Las lecturas de MongoDB son “inconsistentes” entre peticiones
Causa raíz: Read preference apunta a secondaries y existe lag, o write concern no es majority y ocurre un rollback tras la conmutación por error.
Solución: Alinear read/write concerns con las necesidades de corrección; evitar lecturas secundarias para cualquier cosa que alimente lógica de negocio a menos que la obsolescencia sea explícitamente aceptable.
5) Síntoma: La conmutación por error de Postgres funcionó, pero los errores en la app aumentan por minutos
Causa raíz: Manejo de conexiones de cliente y comportamiento de actualización de DNS/endpoint no ajustados; los pools de conexión no se recuperan limpiamente.
Solución: Usar una estrategia de proxy/endpoint estable, aplicar lógica de reintento segura y probar con pools parecidos a producción.
6) Síntoma: Un shard de MongoDB parece “balanceado” pero uno se está sobrecargando
Causa raíz: La clave de shard enruta consultas calientes a un shard; el conteo de chunks balanceados no representa la distribución de tráfico.
Solución: Revisar la clave de shard basada en patrones de consulta; validar con métricas por shard y muestreo objetivo de enrutamiento de consultas.
7) Síntoma: Disco de Postgres se llena rápido aunque las tablas no crecieron mucho
Causa raíz: Retención de WAL por replication slots o archivado mal configurado, o archivos temporales descontrolados por sorts/hash joins.
Solución: Comprobar tamaño retenido por replication slots, pipeline de archivado y uso de archivos temporales; añadir alertas de disco con umbrales reales de holgura.
8) Síntoma: La “flexibilidad de esquema” se convierte en caos analítico
Causa raíz: Múltiples formas de documentos a lo largo del tiempo sin limpieza; los sistemas downstream no pueden confiar en que existan campos o que los tipos coincidan.
Solución: Establecer contratos de esquema en el límite de la app, backfillear datos antiguos según un calendario y aplicar reglas de validación cuando sea posible.
Listas de verificación / plan paso a paso
Elegir Postgres sin arrepentirte
- Diseña invariantes primero: ¿Qué nunca debe ocurrir? Codifícalo con constraints (unique, foreign keys, check constraints).
- Planifica el vacuum: Identifica las tablas más calientes, ajusta umbrales de autovacuum y monitoriza tuples muertos.
- Usa JSONB intencionalmente: Mantén lo fundamental relacional; almacena atributos de cola larga en JSONB con índices GIN dirigidos cuando sea necesario.
- Haz las migraciones aburridas: Evita locks largos; prefiere patrones de backfill + swap; prueba con tamaños de datos parecidos a producción.
- Define backup/restore: Backup base + archivado WAL; ensaya restauraciones; documenta RPO/RTO.
- Decide HA: Elige un enfoque de conmutación por error, pruébalo trimestralmente y haz que el comportamiento de conexión de la app sea compatible.
Elegir MongoDB sin acumular “deuda por esquema flexible”
- Escribe un contrato de esquema de todos modos: Define campos requeridos, tipos y estrategia de versionado para documentos.
- Incrusta con límite: Evita arrays sin límites y documentos que crezcan constantemente; prefiere colecciones append-only para historial de eventos.
- Indexa a partir de patrones de consulta: Para cada endpoint crítico: campos de filtro, orden de sort y proyecciones; crea índices compuestos en consecuencia.
- Establece read/write concerns deliberadamente: Decide la obsolescencia y durabilidad que toleras; no lo dejes a valores por defecto y sensaciones.
- Ensaya elecciones: Prueba el comportamiento de reintentos del cliente durante stepdowns; verifica que los timeouts sean realistas y las operaciones idempotentes.
- Presupuesta caché: Monitoriza evacuación de WiredTiger; dimensiona RAM para el conjunto de trabajo, no para la esperanza.
Enfoque híbrido que suele ganar: Postgres + JSONB (y disciplina)
- Poner la verdad transaccional en tablas relacionales con constraints.
- Usar JSONB para atributos que evolucionan y campos opcionales escasos.
- Indexar solo lo que se consulta; aceptar que no todos los campos JSON merecen índice.
- Mantener la analítica separada si los patrones de consulta se vuelven incompatibles con OLTP.
Preguntas frecuentes
1) ¿Deberían las startups elegir MongoDB por velocidad?
Elige por lo que tu equipo puede operar. Si no tienes disciplina fuerte sobre la forma de los documentos y la indexación, Postgres será más rápido en la única forma que importa: menos sorpresas a las 2 a. m.
2) ¿Postgres es “lento a escala” porque los joins son caros?
No. Los planes malos son caros. Con índices y estadísticas correctas, Postgres puede manejar grandes cargas de joins. Cuando falla, suele ser porque la forma de la consulta cambió y nadie revisó indexación y vacuum.
3) ¿Las transacciones de MongoDB son “tan buenas como las de Postgres” ahora?
Pueden proporcionar garantías fuertes, pero las pagas en complejidad y a veces en rendimiento, especialmente si apelas mucho a transacciones multi-documento. Si necesitas transacciones en todas partes, Postgres es la apuesta más simple.
4) ¿Puede Postgres manejar esquemas flexibles como MongoDB?
Postgres puede almacenar y consultar JSONB de forma efectiva, y a menudo es suficiente. Pero no es un pase libre: aún necesitas estructura, y las consultas pesadas sobre JSON pueden convertirse en un tema de indexación y bloat. Usa JSONB para la cola larga, no como filosofía total.
5) ¿Cuándo gana claramente MongoDB?
Cuando tus objetos de dominio son naturalmente con forma de documento, los accedes mayoritariamente como agregados completos y puedes mantener la estructura de documentos consistente. También cuando el escalado horizontal vía sharding es un requisito conocido y estás listo para diseñarlo desde el día uno.
6) ¿Cuándo gana claramente Postgres?
Cuando necesitas corrección estricta, consultas relacionales complejas, restricciones como guardianes y una caja de herramientas operativa madura. Si construyes facturación, inventario, permisos o algo que involucre a abogados, Postgres es la elección más tranquila.
7) ¿Cuál es la razón más común de “elegimos MongoDB y nos arrepentimos”?
Tratar “sin esquema” como “sin estructura”. La deuda aparece como documentos inconsistentes, rendimiento de consultas impredecible y pipelines analíticos que no confían en los datos. MongoDB no causa esto; los equipos sí—por no imponer contratos.
8) ¿Cuál es la razón más común de “elegimos Postgres y nos arrepentimos”?
Subestimar el trabajo operativo alrededor de vacuum, bloat y bloqueo en migraciones—o tratarlo como juguete hasta que es crítico. Postgres es estable, pero espera que hagas mantenimiento rutinario y planificación de capacidad.
9) ¿Deberíamos ejecutar ambos?
Solo si tienes una separación clara de responsabilidades y la madurez operativa para dos sistemas. Ejecutar dos bases de datos “porque cada una es mejor en algo” es válido. Ejecutar dos porque no pudiste decidir es cómo duplicas tu carga en on-call.
10) ¿Servicio gestionado o autogestionado?
Si la disponibilidad importa y tu equipo es pequeño, lo gestionado suele ganar. Autogestionar puede ser genial cuando necesitas control profundo y tienes personal que disfruta de actualizaciones del kernel y conversaciones sobre page cache.
Siguientes pasos que realmente puedes tomar
Si estás eligiendo entre PostgreSQL y MongoDB para un sistema nuevo, no empieces por la ideología. Empieza por los modos de fallo y los hábitos operativos.
Luego decide qué puedes imponer con tecnología frente a lo que esperas que los humanos recuerden.
- Escribe tus invariantes: qué nunca debe ocurrir (cobros dobles, registros huérfanos, lecturas obsoletas que alimentan decisiones).
- Lista tus 10 consultas principales: forma, filtros, ordenamientos, latencia esperada y expectativas de crecimiento.
- Elige tu postura de consistencia: define obsolescencia y durabilidad aceptables; en MongoDB codifícalo en read/write concerns; en Postgres, encuéntralo en transacciones y constraints.
- Decide tu historia de backups: cómo restaurar, cuánto tarda, quién lo ejecuta y con qué frecuencia ensayas.
- Ejecuta una prueba de carga con tamaño de datos parecido a producción: no para obtener un número de benchmark, sino para sacar a la luz patrones de consulta que se vuelven minas operativas.
- Elige la opción que “duele menos más adelante”: la que coincida con cómo tu equipo realmente se comporta un martes por la tarde y en un incidente de sábado por la noche.
Mi preferencia con sesgo: si no tienes una razón fuerte para el modelo de documentos de MongoDB (y un plan para mantener los documentos consistentes), usa PostgreSQL.
No es perfecto, pero es imperfectamente predecible—que es lo que quieres cuando interviene tu pager.