Si alguna vez has visto a un CEO actualizar un panel mientras tu teléfono de guardia empieza a calentarse, conoces este dolor: el tráfico de reporting no se degrada suavemente. Llega de golpe, pide “solo una segmentación más”, y entonces tu base de datos empieza a hacer ruidos desconocidos.
Los equipos aman MongoDB por entregar funciones de producto rápido. Luego aparece el reporting. De repente estás depurando pipelines de agregación a las 2 a.m., y todos desarrollan una afectuosa nostalgia por los JOINs de SQL de los que antes se quejaban.
Por qué los equipos vuelven a SQL (y por qué no es hipocresía)
Reporting y analítica son otro deporte diferente al OLTP. Tu base de datos de producto está construida para lecturas y escrituras pequeñas y previsibles: obtener usuario, actualizar carrito, insertar pedido. El reporting está construido para escaneos grandes, agrupaciones pesadas, filtros amplios y “compara este mes con el anterior en seis dimensiones”. También está pensado para humanos con zonas horarias, opiniones y Excel.
MongoDB puede hacer analítica, especialmente con el framework de agregación, pero muchos equipos descubren la misma verdad incómoda: las stores de documentos se optimizan para patrones de acceso definidos por la aplicación, no por el analista. Los analistas no piensan en “documentos”; piensan en relaciones y dimensiones. Harán preguntas que el diseño documental original resiste activamente.
Los sistemas SQL (MySQL, Postgres, SQL Server, etc.) tienen una larga y dura historia de ser moldeados para reporting. Esa historia importa. Optimizadores, estrategias de JOIN, semánticas estables, herramientas maduras: esto es tejido cicatricial operativo acumulado. MongoDB está mejorando, pero cuando tu VP quiere un gráfico de cohortes de retención a mediodía, quieres caminos aburridos, legibles y bien trillados.
Además: el reporting tiene política. La gente quiere números consistentes entre equipos. Eso implica definiciones consistentes. Los esquemas y vistas SQL convierten definiciones en artefactos compartidos y revisables. MongoDB a menudo las dispersa en fragmentos de código de aplicación y pipelines repartidos por repositorios como purpurina—para siempre.
Un chiste corto, como premio: MongoDB es “sin esquema” como la habitación de un adolescente es “sin muebles”. Técnicamente cierto, operacionalmente caro.
Contexto histórico y datos interesantes (lo que la gente olvida)
A continuación algunos hechos concretos y fragmentos de la línea temporal que explican por qué SQL sigue “ganando de vuelta” las cargas de reporting. No son sensaciones: es historia real.
- La estandarización de SQL empezó en los años 80. Eso significa décadas de herramientas: JDBC/ODBC, conectores BI, planificadores de consultas y un modelo mental compartido entre empresas.
- MySQL se lanzó a mediados de los 90 y se convirtió en la base de datos web por defecto para una generación. Los patrones de reporting se aprendieron en producción de la manera difícil: réplicas, tablas resumen, rollups y particionado.
- MongoDB apareció alrededor de 2009 durante una ola de sistemas “NoSQL” pensados para escalar horizontalmente y moverse rápido. Muchos fueron diseñados principalmente para cargas de aplicación, no para analítica ad-hoc.
- El auge de JSON (y luego JSON en SQL) difuminó la línea. Los sistemas SQL adoptaron columnas y funciones JSON; MongoDB adoptó capacidades de consulta y agregación más ricas. La convergencia ocurrió, pero no por igual.
- “Schema-on-read” se popularizó en big data (pensamiento de la era Hadoop). Funcionó para procesamiento por lotes, pero el reporting operativo quiere consultas interactivas y rápidas. Schema-on-read rara vez es gratis.
- El almacenamiento columnar y los data warehouses cambiaron expectativas. Los equipos se acostumbraron a dashboards que escanean millones de filas rápidamente. Una vez que pruebas eso, una store documental haciendo group-by amplios puede sentirse como pedalear un camión.
- Las herramientas BI evolucionaron alrededor de SQL. Incluso cuando las herramientas soportan MongoDB, las funcionalidades “camino dorado” (capas semánticas, caché, lineage, permisos) son casi siempre SQL-primero.
- Los patrones de replicación son más antiguos que la mayoría de microservicios. La replicación y escalado de MySQL están profundamente comprendidos operativamente, incluidos modos de falla y playbooks de recuperación.
Todo esto suma una regla operativa simple: el éxito en reporting es menos sobre “si la base de datos puede ejecutar la consulta” y más sobre “si tu organización puede operar la base de datos bajo demanda humana impredecible”. Los sistemas SQL llevan más tiempo en esa arena.
Realidad de la carga de trabajo de reporting: lo que los dashboards hacen a las bases de datos
Las consultas de reporting son rudas. No usan tus claves primarias cuidadosamente diseñadas. No respetan tu caché. Aman los rangos temporales. Quieren agrupar por columnas que no indizaste porque no existían cuando lanzaste la v1.
Las tres cosas que hacen los dashboards y que rompen las suposiciones de “base de datos de aplicación”
- Provocan fan-out: una carga de página dispara muchas consultas (filtros, totales, gráficos, drilldowns). Multiplica por una avalancha del lunes por la mañana.
- Sesgan los patrones de acceso: en lugar de “obtener documento por id”, obtienes “escanear los últimos 90 días y calcular una distribución”.
- Exigen consistencia en las definiciones: “usuario activo”, “ingresos”, “churn”, “conversión” significan cosas distintas hasta que las fijas. Esquemas SQL, vistas y lógica almacenada ayudan a imponer definiciones compartidas.
La analítica no es solo velocidad de consulta
La velocidad importa, pero también:
- Previsibilidad: rendimiento de consulta estable según valores de parámetros y crecimiento de datos.
- Explicabilidad: necesitas mostrar cómo se calculó el número, no solo entregarlo.
- Aislamiento operativo: el reporting no debería tumbar tu flujo de checkout.
- Gobernanza de esquemas: si tus “events” tienen 19 formas, alguien va a calcular mal el “DAU”.
Hay una razón por la que muchas empresas maduras ejecutan reporting desde réplicas, data warehouses o data marts separados. Aprendieron que mezclar OLTP y analítica ad-hoc es como dejar entrar clientes a tu cocina durante la hora punta.
Por qué MySQL tiende a ganar para reporting
1) Los JOINs son una característica, no una falla moral
El reporting es relacional por naturaleza: pedidos se relacionan con clientes, clientes con segmentos, segmentos con campañas, campañas con gasto. Incluso si la app almacena “snapshot del cliente” dentro de un documento de pedido, los analistas seguirán pidiendo atributos a nivel cliente, segmentaciones actualizadas y correcciones. Los JOINs son cómo conectas hechos y dimensiones sin reescribir la historia.
El motor de JOIN, el optimizador y el modelo de indexación de MySQL están construidos alrededor de esto. Cuando escribes una consulta que une cinco tablas con predicados selectivos, MySQL tiene una posibilidad real de hacer algo sensato. En MongoDB, a menudo o denormalizas (y lo pagas después) o usas $lookup (y entonces redescubres los joins, pero con menos protecciones y a veces más dolor de memoria).
2) Los planes de consulta son inspeccionables y operativamente accionables
En MySQL, EXPLAIN, histogramas y trazas del optimizador dan pistas estructuradas. Puedes preguntar “¿por qué eligió este índice?”, “¿por qué hizo un sort?”, “¿por qué escaneó?” y obtener una respuesta que mapea a patrones de afinamiento conocidos.
Eso importa en la respuesta a incidentes. Cuando los dashboards derriten producción, no quieres debates filosóficos. Quieres ver el plan, arreglar el índice, añadir un índice covering, reescribir la consulta o mover la carga. Rápido.
3) El ecosistema es implacablemente moldeado por SQL
Herramientas BI, herramientas de gobernanza, patrones de control de acceso, auditoría, logging de consultas, e incluso “cómo enseñar a nuevos empleados a hacer reporting” son más simples cuando la capa de almacenamiento es SQL. Tu equipo de finanzas no quiere aprender pipelines de agregación. Quieren una vista llamada revenue_daily.
4) Réplicas, particionado y tablas resumen son aburridas y efectivas
El reporting en MySQL normalmente termina con alguna combinación de:
- Réplicas de lectura para el tráfico de dashboards
- Tablas de hechos particionadas (a menudo por fecha)
- Tablas pre-agregadas para métricas calientes
- Rollups materializados (sí, estás construyendo un mini-warehouse)
Nada de esto es tendencia. Sin embargo, es cómo mantienes el producto funcionando mientras la organización hace preguntas.
Dónde MongoDB es realmente bueno (y dónde no lo es)
MongoDB brilla cuando la forma del documento coincide con la forma de la consulta
Si tu reporting es “muestra el perfil del usuario con preferencias embebidas y actividad reciente” y construiste documentos exactamente para eso, MongoDB puede ser rápido y agradable. Las agregaciones sobre una sola colección con filtros selectivos e índices correctos también pueden rendir bien.
MongoDB lucha cuando el reporting se vuelve cross-entity y evolutivo
El modo de fallo se parece a esto:
- Los datos se denormalizan para velocidad.
- Las definiciones evolucionan (“activo” cambia, la segmentación cambia, llegan correcciones).
- Ahora tienes que rellenar campos embebidos, conciliar inconsistencias y explicar qué versión de la verdad usa cada gráfico.
- Alguien introduce pipelines de
$lookuppara “unir” colecciones. - Los picos de memoria aparecen, los resultados intermedios estallan y el clúster se comporta como si fuera alérgico a tu CFO.
El reporting en MongoDB puede estar bien si lo tratas como fuente, no como motor de reporting
Una arquitectura muy común y estable es: MongoDB para casos de uso tipo OLTP del producto → streaming/ETL hacia un sistema SQL o warehouse para reporting. Eso no es un insulto a MongoDB. Es especialización. Es como no ejecutar cargas del plano de control de Kubernetes en los mismos nodos que tus jobs batch de Spark a menos que disfrutes del caos.
Joins vs embebido: el impuesto del reporting que siempre pagas eventualmente
El embebido es seductor. Una lectura, todo lo que necesitas, sin joins. Genial—hasta que tu negocio quiere historia y consistencia. En el momento en que embebes atributos mutables (tier del cliente, dirección, segmento) en documentos tipo hecho (pedidos, eventos), estás eligiendo un modelo de versionado. Usualmente es “lo que era en el momento del write”. A veces eso es correcto. Con frecuencia no lo es.
Las dimensiones mutables son donde la desnormalización NoSQL duele
El reporting quiere poder preguntar:
- “¿Cuál fue el ingreso por segmento actual?”
- “¿Cuál fue el ingreso por segmento en el momento de la compra?”
- “¿Qué pasa si reclasificamos estos clientes?”
En SQL, esto es una decisión de esquema: dimensiones lentamente cambiantes, tablas snapshot o historial tipo 2. En MongoDB, los equipos suelen hacerlo a martillazos almacenando múltiples campos, rellenando atrás o escribiendo pipelines de conciliación ad-hoc. Puede funcionar, pero es menos explícito y más fácil de errar.
Segundo chiste corto (y terminamos):
La desnormalización es como empacar para un viaje usando todas las camisetas a la vez. Ahorrarás espacio en la maleta hasta que necesites cambiarte.
Planes de consulta, índices y por qué “funcionó en staging” es una mentira
Los datos de staging son corteses. Los datos de producción son creativos. Las consultas analíticas amplifican esa creatividad porque tocan más filas, más rangos y más rincones raros.
MySQL: modos de fallo previsibles
En incidentes de reporting en MySQL, los culpables habituales son:
- Falta de índices compuestos para patrones comunes de filtro+agrupación
- Ordenación de grandes conjuntos debido a
ORDER BYen columnas no indexadas - Errores en el orden de joins cuando las estadísticas están obsoletas o sesgadas
- Spills a tablas temporales (tablas temporales en disco) durante agregaciones
- Lag de réplicas cuando los dashboards golpean las réplicas
La buena noticia: estos problemas son diagnosticables con herramientas estándar y típicamente solucionables sin rediseñar todo el modelo de datos.
MongoDB: acantilados de rendimiento por agregación y memoria
MongoDB puede comportarse bien hasta que no. Acantilados típicos:
$groupcon claves de alta cardinalidad crea un estado en memoria masivo$lookuphace que los joins exploten resultados intermedios- El uso de índices está bien para el primer
$match, luego entras en tierra de pipeline - Las consultas se vuelven más lentas a medida que los documentos se vuelven más variados (documentos anchos, arrays)
- Las claves de sharding optimizadas para OLTP no coinciden con los filtros de reporting
Una cita sobre fiabilidad (idea parafraseada)
Idea parafraseada, atribuida a Richard Cook: “El éxito en las operaciones a menudo oculta el trabajo que hace que la falla no suceda”.
El reporting es donde el trabajo oculto se vuelve visible. Si quieres dashboards que no te avergüencen, necesitas diseño explícito: esquemas, índices, aislamiento y mantenimiento rutinario.
Tres mini-historias corporativas desde las trincheras
Mini-historia 1: El incidente causado por una suposición equivocada
La Compañía A ejecutaba su producto central en MongoDB. Los pedidos eran documentos con snapshots embebidos del cliente: nombre, email, tier, región. La suposición era simple: “Siempre querremos la info del cliente tal como era cuando ocurrió el pedido”. Eso era cierto para recibos y soporte al cliente, así que todos asintieron y siguieron.
Entonces el negocio lanzó un proyecto de reclasificación por cumplimiento. Las regiones cambiaron. Los clientes se remapearon. Finanzas quiso ingresos por región actual para los últimos ocho trimestres para cuadrar la nueva contabilidad. El equipo de dashboards construyó un pipeline de agregación sobre la colección de pedidos y agrupó por el campo de región embebido. Devolvió números. Estaban mal. Mal en silencio.
Cuando la discrepancia salió a la luz, ingeniería intentó “arreglar los datos” rellenando campos embebidos de región en pedidos históricos. Eso llevó días, estresó el clúster y rompió la suposición original para soporte: el recibo ahora mostraba una región en la que el cliente no vivía al comprar. Dos verdades, un campo.
La solución eventual no fue glamorosa: normalizar dimensiones en una tienda de reporting SQL, mantener los hechos de pedido inmutables y modelar la región como una dimensión lentamente cambiante con joins “as-of” explícitos. La capa de reporting tuvo dos vistas: “region_at_purchase” y “region_current”. El dashboard dejó de mentir. La gente dejó de gritar. Así es la adultez en sistemas de datos.
Mini-historia 2: La optimización que salió mal
La Compañía B usaba MySQL y sufrió latencia en reporting. Alguien propuso una optimización: “Precomputaremos todo cada noche en una tabla ancha para que los dashboards nunca hagan joins”. Sonó eficiente. También sonó a algo que dice un equipo cansado después de meses de paginación.
La tabla ancha funcionó unas dos semanas. Luego un lanzamiento de producto introdujo atributos nuevos. El ETL ganó lógica condicional. Las columnas se multiplicaron. La tabla se volvió tan ancha que la mayoría de consultas leían mucho más dato del necesario. El buffer pool de InnoDB churnó. El lag de replicación empeoró porque el job nocturno escribió una gran cantidad de datos en una ventana corta.
Peor: como el job corría de noche, el negocio empezó a esperar “números de hoy” a las 10 a.m. Obtuvieron los de ayer. Los equipos empezaron a correr backfills parciales durante el día. Eso chocó con el tráfico pico y creó una ventana diaria previsible de incidentes. La “optimización” se convirtió en un calendario operativo de dolor.
La recuperación fue volver a lo básico: mantener tablas de hechos y dimensiones normalizadas, construir un pequeño conjunto de tablas resumen dirigidas a métricas realmente calientes y refrescarlas incrementalmente (cada hora o en streaming) con lógica idempotente. Los dashboards se aceleraron y el sistema dejó de producir sorpresas diarias.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día
La Compañía C tenía MongoDB (producto) y MySQL (reporting). La práctica que los salvó no fue una arquitectura elegante. Fue una rutina semanal: revisar el slow query log, revisar los gráficos de lag de réplicas y ejecutar una pequeña suite de “consultas principales de dashboards” contra datos similares a producción con EXPLAIN/explain() capturados.
Era aburrido. También creó un lenguaje compartido entre analistas de datos y SRE. Los analistas aprendieron a formular preguntas que no requerían escanear el universo. SRE aprendió qué métricas eran críticas para el negocio y merecían tablas resumen o réplicas dedicadas.
Cuando la compañía tuvo un pico de tráfico por un lanzamiento de asociación inesperado, la carga de reporting aumentó bruscamente. El ingeniero on-call ya conocía las dos peores consultas, los índices que requerían y los throttles seguros. Activaron una réplica de solo lectura para dashboards y limitaron la tasa de un endpoint “descargar CSV” que tenía la costumbre de hacer full table scans.
Sin heroísmos. Sin all-hands. Solo operaciones predecibles construidas desde un hábito aburrido. El lanzamiento de la asociación fue una historia de ingresos, no de caída.
Tareas prácticas (comandos, salidas y decisiones)
Estas son tareas reales que puedes ejecutar hoy. Cada una incluye: comando, qué significa la salida y la decisión que tomas a partir de ella. Úsalas como una lista de comprobación activa, no como trivia.
Task 1: MySQL — identificar los patrones de consulta que consumen más tiempo
cr0x@server:~$ sudo pt-query-digest /var/log/mysql/mysql-slow.log | head -n 40
# 120s user time, 2.1s system time, 28.50M rss, 120.00M vsz
# Query_time distribution
# 1us
# 10ms
# 100ms ####
# 1s ##########
# 10s #####
# Profile
# Rank Query ID Response time Calls R/Call V/M Item
# ==== ================== ============= ===== ======= ===== ============
# 1 0xA1B2C3D4E5F6A7B8 420.1234 1200 0.3501 0.02 SELECT orders...
# 2 0x1122334455667788 180.0000 200 0.9000 0.01 SELECT users...
Significado: Obtienes qué formas de consulta cuestan más tiempo total, no solo la más lenta individual. El dolor de reporting suele ser “una consulta moderadamente lenta llamada muchas veces”.
Decisión: Arregla el rango #1–#3 primero: añade índices, reescribe o muévelas a una réplica/tabla resumen. Ignora la consulta unicornio rara hasta que la manada deje de estamparse.
Task 2: MySQL — comprobar el lag de réplicas antes de culpar a la consulta
cr0x@server:~$ mysql -e "SHOW SLAVE STATUS\G" | egrep "Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running"
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 187
Significado: Tu réplica de reporting está ~3 minutos por detrás. Los dashboards pueden mostrar números “incorrectos” y los analistas llamarán a esto un bug de datos.
Decisión: Si el lag es inaceptable, reduce la presión de escritura (ETL por lotes), aumenta la capacidad de la réplica o enruta algunos dashboards a resultados cacheados. No afines una consulta en una réplica que está ahogándose en apply lag.
Task 3: MySQL — verificar qué está haciendo realmente el optimizador
cr0x@server:~$ mysql -e "EXPLAIN FORMAT=tree SELECT c.segment, DATE(o.created_at) d, SUM(o.total) s FROM orders o JOIN customers c ON c.id=o.customer_id WHERE o.created_at >= '2025-01-01' AND o.created_at < '2025-02-01' GROUP BY c.segment, d;"
EXPLAIN: -> Aggregate using temporary table (cost=...)
-> Nested loop inner join (cost=...)
-> Index range scan on orders using idx_orders_created_at (cost=...)
-> Single-row index lookup on customers using PRIMARY (id=o.customer_id)
Significado: La consulta usa un range scan en orders.created_at, luego PK lookups a customers, y después una tabla temporal para el agrupamiento.
Decisión: Si la tabla temporal es enorme, considera un índice compuesto que soporte patrones de group-by (o una tabla resumen). Si el range scan es demasiado amplio, particiona por fecha o ajusta los predicados.
Task 4: MySQL — averiguar si las tablas temporales están derramando a disco
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Created_tmp%';"
+-------------------------+----------+
| Variable_name | Value |
+-------------------------+----------+
| Created_tmp_disk_tables | 48219 |
| Created_tmp_tables | 51200 |
+-------------------------+----------+
Significado: Una gran porción de tablas temporales están yendo a disco, a menudo por grandes sorts/group-bys.
Decisión: Afina consultas/índices para reducir tablas temporales, o ajusta límites de memoria con cuidado. Para reporting, prefiere pre-aggregación en lugar de “más espacio temp” como estrategia.
Task 5: MySQL — comprobar la presión del buffer pool (¿estás cacheando algo?)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';"
+--------------------------+-----------+
| Variable_name | Value |
+--------------------------+-----------+
| Innodb_buffer_pool_reads | 9812234 |
+--------------------------+-----------+
+----------------------------------+-------------+
| Variable_name | Value |
+----------------------------------+-------------+
| Innodb_buffer_pool_read_requests | 2048123456 |
+----------------------------------+-------------+
Significado: Muchas lecturas lógicas se sirven desde memoria; aún ocurren lecturas físicas. Si Innodb_buffer_pool_reads sube rápidamente durante uso de dashboards, estás thrashing.
Decisión: Considera separar reporting a réplicas con más memoria, añadir índices covering o descargar a un warehouse. No dejes que los dashboards expulsen las páginas calientes de OLTP.
Task 6: MySQL — confirmar si tienes el índice compuesto correcto (o no)
cr0x@server:~$ mysql -e "SHOW INDEX FROM orders;"
+--------+------------+------------------------+--------------+-------------+-----------+-------------+
| Table | Key_name | Column_name | Seq_in_index | Cardinality | Non_unique| Index_type |
+--------+------------+------------------------+--------------+-------------+-----------+-------------+
| orders | PRIMARY | id | 1 | 50000000 | 0 | BTREE |
| orders | idx_orders_created_at | created_at | 1 | 3650 | 1 | BTREE |
| orders | idx_orders_customer_id| customer_id | 1 | 2000000 | 1 | BTREE |
+--------+------------+------------------------+--------------+-------------+-----------+-------------+
Significado: Tienes índices de una sola columna pero no uno compuesto como (created_at, customer_id) o (created_at, status) que coincida con filtros de reporting.
Decisión: Añade índices compuestos para los predicados principales de reporting. Hazlo en una réplica primero, mide y luego avanza. Los índices no son gratis—optimiza para las consultas que realmente ejecutas.
Task 7: MongoDB — identificar agregaciones pesadas y si derraman
cr0x@server:~$ mongosh --quiet --eval 'db.orders.explain("executionStats").aggregate([{ $match: { createdAt: { $gte: ISODate("2025-01-01"), $lt: ISODate("2025-02-01") } } },{ $group: { _id: "$segment", revenue: { $sum: "$total" } } }], { allowDiskUse: true })'
{
"stages": [
{ "$cursor": { "queryPlanner": { "winningPlan": { "stage": "IXSCAN" } }, "executionStats": { "totalDocsExamined": 4200000, "totalKeysExamined": 4200000 } } },
{ "$group": { "usedDisk": true, "spills": 3 } }
]
}
Significado: El agrupamiento derramó a disco (usedDisk: true). Eso es una luz de advertencia de latencia e IO.
Decisión: Reduce la cardinalidad de las claves de grupo, pre-agrega o mueve la carga fuera de MongoDB. Si el derrame a disco es común para dashboards centrales, estás haciendo analítica en el lugar equivocado.
Task 8: MongoDB — comprobar si una consulta usa el índice previsto
cr0x@server:~$ mongosh --quiet --eval 'db.orders.find({createdAt: {$gte: ISODate("2025-01-01"), $lt: ISODate("2025-02-01")}, status: "paid"}).sort({createdAt: -1}).hint({createdAt: 1}).explain("executionStats").queryPlanner.winningPlan'
{
"stage": "FETCH",
"inputStage": {
"stage": "IXSCAN",
"keyPattern": { "createdAt": 1 },
"direction": "forward"
}
}
Significado: El planner puede usar el índice createdAt, pero tu filtro status no está en el índice; puede examinar más documentos de los necesarios.
Decisión: Añade un índice compuesto como {status: 1, createdAt: 1} si esta es una consulta principal. Si además haces sort por createdAt, elige el orden del índice con cuidado.
Task 9: MongoDB — encontrar operaciones actuales que generan dolor
cr0x@server:~$ mongosh --quiet --eval 'db.currentOp({ "active": true, "secs_running": { $gte: 5 } }).inprog.map(op => ({secs: op.secs_running, ns: op.ns, op: op.op, command: op.command && Object.keys(op.command)[0]}))'
[
{ "secs": 42, "ns": "prod.orders", "op": "command", "command": "aggregate" },
{ "secs": 19, "ns": "prod.events", "op": "command", "command": "find" }
]
Significado: Operaciones de larga ejecución incluyen una agregación en orders. Eso suele ser un dashboard o una exportación.
Decisión: Mata o limita la conocida culpable, añade guardrails (timeouts, maxTimeMS) y mueve esa consulta a una tienda de reporting.
Task 10: OS — comprobar latencia de disco cuando la analítica “se ralentiza aleatoriamente”
cr0x@server:~$ iostat -x 1 3
avg-cpu: %user %nice %system %iowait %steal %idle
12.00 0.00 6.00 18.00 0.00 64.00
Device r/s w/s rkB/s wkB/s await svctm %util
nvme0n1 820.0 1100.0 64000 72000 28.5 0.9 97.0
Significado: await es alto y %util está saturado. Tu base de datos está limitada por IO ahora, no “lenta por CPU”.
Decisión: Reduce escaneos grandes (índices/particionado), mueve reporting a réplicas en almacenamiento separado, o programa jobs pesados fuera de hora. No sigas afinando la sintaxis de consultas si el disco está al máximo.
Task 11: OS — confirmar si la presión de memoria está causando swap storms
cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 1 524288 10240 12000 98000 15 25 1800 2400 900 1200 20 8 50 22 0
Significado: Swap-in/out (si/so) es distinto de cero bajo carga. Eso mata el rendimiento de una base de datos, especialmente en agregaciones.
Decisión: Deja de ejecutar reporting en nodos subdimensionados. Añade memoria, aísla reporting o limita la concurrencia. Una base de datos que hace swapping es una manera educada de decir “me niego”.
Task 12: MySQL — medir saturación de conexiones y hilos
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_running'; SHOW GLOBAL STATUS LIKE 'Connections'; SHOW VARIABLES LIKE 'max_connections';"
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| Threads_running | 184 |
+-----------------+-------+
+---------------+----------+
| Variable_name | Value |
+---------------+----------+
| Connections | 9812231 |
+---------------+----------+
+-----------------+------+
| Variable_name | Value|
+-----------------+------+
| max_connections | 500 |
+-----------------+------+
Significado: Muchos hilos concurrentes; no estás en max connections, pero puedes estar con conmutación de contexto de CPU o contención interna.
Decisión: Añade connection pooling, reduce la paralelización de dashboards o enruta a una réplica. Si el tráfico de reporting provoca picos de hilos, aísla esa carga.
Task 13: MySQL — encontrar las tablas más grandes y el bloat de índices (chequeo de almacenamiento)
cr0x@server:~$ mysql -e "SELECT table_name, ROUND((data_length+index_length)/1024/1024/1024,2) AS size_gb FROM information_schema.tables WHERE table_schema='prod' ORDER BY (data_length+index_length) DESC LIMIT 10;"
+------------+---------+
| table_name | size_gb |
+------------+---------+
| events | 420.55 |
| orders | 88.12 |
| users | 12.40 |
+------------+---------+
Significado: Si tu tabla más grande es también la que alimenta dashboards, estás escaneando muchos datos te des cuenta o no.
Decisión: Particiona por fecha, archiva o mueve events a un warehouse. Si “events” tiene 400+ GB y se usa para reporting interactivo, deja de fingir que esto es solo OLTP.
Task 14: MongoDB — comprobar inventario de índices y detectar índices compuestos faltantes
cr0x@server:~$ mongosh --quiet --eval 'db.orders.getIndexes().map(i => i.key)'
[
{ "_id": 1 },
{ "createdAt": 1 },
{ "customerId": 1 }
]
Significado: Solo existen índices de campo único. Las agregaciones que filtran por múltiples campos examinarán demasiados documentos.
Decisión: Añade índices compuestos alineados con los filtros principales de dashboards (por ejemplo, {status: 1, createdAt: 1}). Si tu analítica requiere una docena de esos índices, eso es una señal de que deberías ETL hacia SQL/warehouse en su lugar.
Task 15: MySQL — validar binlog/configuración de replicación para réplicas de reporting
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'binlog_format'; SHOW VARIABLES LIKE 'sync_binlog';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW |
+---------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| sync_binlog | 1 |
+---------------+-------+
Significado: El binlog durable está activado. Genial para corrección, pero puede añadir overhead de escritura. Tu estrategia de reporting no debería depender de escrituras agresivas en horas pico.
Decisión: Si el ETL de reporting está golpeando el primary, mueve el ETL fuera del camino primario (CDC, stream o extracción basada en réplica). No “optimices” configuraciones de durabilidad porque los dashboards van lentos.
Guía rápida de diagnóstico: encuentra el cuello de botella en minutos
Este es el playbook que quiero en la pared cuando un incidente de dashboard ocurre. No tienes tiempo para ser elegante. Tienes tiempo para ser correcto.
Primero: decide si es costo de consulta, concurrencia o infraestructura
- Revisa saturación: CPU, IO, memoria, red. Si IO está al máximo o hay swapping, afinar índices no te salvará en el incidente.
- Revisa concurrencia: número de hilos/operaciones en ejecución. El reporting a menudo falla por demasiadas consultas “razonables” en paralelo.
- Revisa los principales culpables: slow query digest (MySQL) o currentOp/slow logs (MongoDB).
Segundo: aisla la carga
- Enruta dashboards a una réplica de lectura o nodo de reporting dedicado si lo tienes.
- Limita la tasa de exportaciones y endpoints “descargar CSV” (son trabajos batch encubiertos).
- Activa caché para los gráficos principales si la corrección tolera algo de estaleza.
Tercero: prueba el plan y arregla lo de mayor apalancamiento
- MySQL: ejecuta
EXPLAIN, añade el índice compuesto faltante o reescribe para reducir filas escaneadas. Si el agrupamiento es grande, construye una tabla rollup. - MongoDB: ejecuta
explain("executionStats")para el pipeline, mira docs examinados vs devueltos y spills a disco. Si está derramando y la cardinalidad es alta, para y descarga.
Cuarto: prevenir recurrencia
- Crea un “presupuesto de consultas de dashboard” (tiempo máximo, docs máximos escaneados, concurrencia máxima).
- Añade revisión programada de consultas lentas.
- Define métricas compartidas en vistas/tablas, no en fragmentos de código de aplicación.
Errores comunes: síntomas → causa raíz → solución
1) Síntoma: los dashboards se vuelven más lentos cada semana sin cambios de código
Causa raíz: crecimiento de datos + falta de particionado/archivo; las consultas escanean rangos temporales más amplios con el tiempo.
Solución: particiona tablas de hechos por fecha (MySQL), crea tablas resumen o ETL a un warehouse. En MongoDB, introduce colecciones por buckets de tiempo o descarga la analítica.
2) Síntoma: la “misma consulta” a veces es rápida y otras veces terrible
Causa raíz: sensibilidad a parámetros y sesgo. Un cliente/segmento tiene 100× más datos; el optimizador elige un plan que funciona para segmentos pequeños pero es horrible para grandes.
Solución: en MySQL, añade índices compuestos alineados a predicados selectivos; considera reescrituras; mantén estadísticas precisas. En MongoDB, asegúrate de que el primer $match sea selectivo e indexado; evita $group de alta cardinalidad sin pre-aggregación.
3) Síntoma: el reporting “funciona” pero los números no coinciden entre equipos
Causa raíz: definiciones de métricas implementadas ad-hoc en pipelines o código de aplicación; no hay una capa semántica compartida.
Solución: define métricas centralmente (vistas SQL/transformaciones controladas), versiona cambios y prueba con datasets fijos. Trata las definiciones de métricas como contratos de API.
4) Síntoma: el clúster MongoDB se vuelve inestable durante revisiones de negocio
Causa raíz: agregaciones grandes con spills a disco; $lookup; índices inadecuados; shard key optimizada para escrituras, no para reporting.
Solución: mueve analítica a SQL/warehouse; pre-agrega; rediseña el shard key para consultas analíticas solo si aceptas los tradeoffs de OLTP. La mayoría de equipos no deberían pelear esta batalla en MongoDB.
5) Síntoma: el primary MySQL se vuelve lento cuando los analistas ejecutan consultas
Causa raíz: ejecutar reporting en el primary; lecturas grandes y tablas temporales expulsan caché de OLTP; bloqueos/contención.
Solución: separa reporting vía réplicas; impone usuarios read-only; limita tiempo de consulta; usa tablas resumen. Protege el primary como si pagara tu salario (porque lo hace).
6) Síntoma: picos de lag de réplica durante ETL o jobs de “refrescar métricas”
Causa raíz: ráfagas de escritura pesadas, transacciones grandes o ETL que actualiza muchas filas; la réplica no aplica lo suficientemente rápido.
Solución: actualizaciones incrementales, chunking, cargas idempotentes y programación. Considera CDC hacia una tienda de reporting separada en lugar de golpear MySQL con updates.
7) Síntoma: añadir índices “no hizo nada”
Causa raíz: orden de índice incorrecto, baja selectividad, funciones sobre columnas (predicados no sargables), o la consulta realmente está limitada por agrupamiento/ordenación.
Solución: inspecciona planes; evita envolver columnas indexadas en funciones; añade índices covering/compuestos; o pre-agrega. Los índices no son magia; son matemáticas con consecuencias.
Listas de verificación / plan paso a paso
Checklist de decisión: ¿debería el reporting ejecutarse directamente en MongoDB?
- ¿La mayoría de informes son de una sola colección con predicados selectivos? Si sí, quizá.
- ¿Necesitas joins frecuentes entre entidades, definiciones cambiantes o modelado dimensional? Si sí, planifica SQL/warehouse.
- ¿Necesitas compatibilidad con herramientas BI con mínima fricción? SQL gana.
- ¿Estás viendo spills de agregación a disco o presión de memoria? Esa es tu señal de salida.
Plan paso a paso: construir una ruta de reporting sensata sin drama
- Inventario de dashboards principales: lista las 20 consultas principales por frecuencia e importancia para el negocio.
- Clasifica cada consulta: tipo OLTP (búsquedas puntuales), analítica (scan/group), export/batch (grandes dumps).
- Elige una estrategia de aislamiento:
- MySQL: réplicas de lectura para dashboards, réplica separada para exports.
- MongoDB: nodo analítico separado solo si puedes tolerar consistencia eventual; de lo contrario ETL fuera.
- Modela los datos explícitamente:
- Define hechos (orders, events) y dimensiones (customer, product, campaign).
- Decide qué dimensiones necesitan historial (slowly changing) vs snapshots.
- Crea una capa semántica mínima: vistas SQL para definiciones compartidas de métricas y filtros.
- Añade los índices correctos: basados en patrones reales de consulta, no en intuición.
- Pre-agrega métricas calientes: resúmenes diarios/hora, actualizados incrementalmente.
- Establece guardrails:
- Timeouts de consulta, límites de concurrencia, tamaño máximo de exportación.
- Roles separados para analistas vs cuentas de servicio.
- Operacionalízalo:
- Cadencia de revisión de consultas lentas.
- Alertas y runbooks para lag de réplicas.
- Planificación de capacidad ligada al uso de dashboards.
- Migra gradualmente: empieza con un dashboard, valida números y luego expande.
Checklist de migración: mover analítica de MongoDB a MySQL (o una capa SQL)
- Define el esquema canónico de eventos/pedidos (con tipos y nulabilidad).
- Decide claves de idempotencia para las cargas (ej.: event_id).
- Backfill de datos históricos en chunks; valida totales por día.
- Ejecuta dual-write o sync basado en CDC hasta que la confianza sea alta.
- Congela definiciones en vistas; requiere revisión para cambios.
- Corta los dashboards uno por uno; mantiene la tubería antigua en una ventana de rollback.
Preguntas frecuentes
1) ¿MongoDB es “malo” para analítica?
No. Es bueno en analítica que coincide con sus fortalezas: consultas de una sola colección con buenos índices y cardinalidad de agrupación razonable. Es menos apto como backend BI de propósito general bajo joins ad-hoc y definiciones de negocio que evolucionan.
2) ¿Por qué los joins importan tanto para reporting?
Porque las preguntas de reporting son relacionales: hechos más dimensiones. Si evitas joins embebiendo todo, eliges un modelo de versionado y pagas con backfills, duplicación e incoherencia en la verdad.
3) ¿No puede MongoDB simplemente usar $lookup y funcionar?
$lookup son joins, pero en un modelo de pipeline que puede crear grandes resultados intermedios y presión de memoria. Puede funcionar para joins pequeños. A menudo se vuelve frágil a escala, especialmente con consultas ad-hoc.
4) ¿Por qué no ejecutar reporting en el primary MySQL si es “suficientemente rápido”?
Porque “suficientemente rápido” es una mentira que te cuentas hasta que llega la fecha del board deck. La carga de reporting es explosiva e impredecible, y puede expulsar páginas calientes de OLTP, aumentar la contención por locks y desencadenar latencias en cascada. Usa réplicas o una tienda de reporting separada.
5) ¿Cuál es la ganancia más rápida para rendimiento de reporting en MySQL?
Normalmente: añade el índice compuesto correcto para el patrón de consulta del dashboard principal y enruta el tráfico de reporting a una réplica de lectura. La segunda ganancia más rápida es una tabla resumen para una métrica caliente.
6) ¿Cuál es la ganancia más rápida para rendimiento de reporting en MongoDB?
Haz que el primer $match sea selectivo e indexado; añade índices compuestos que coincidan con patrones de filtro; usa allowDiskUse cuando corresponda. Si la consulta aún derrama y escanea millones de documentos, deja de optimizar y descarga.
7) ¿Deberíamos almacenar eventos analíticos en MongoDB?
Si tu caso de eventos es centrado en la aplicación (“mostrar feed de actividad del usuario”), MongoDB puede ser adecuado. Para analítica de nivel BI, probablemente quieras un sistema optimizado para scans, agregaciones y compresión. Muchos equipos mantienen MongoDB como fuente y replican eventos hacia afuera.
8) ¿Cómo mantenemos consistencia de números de reporting entre equipos?
Define métricas centralmente (vistas SQL/transformaciones controladas), versiona cambios y prueba con datasets fijos. Evita que la lógica de métricas viva en código de aplicación disperso o pipelines puntuales.
9) ¿MySQL es suficiente o necesitamos un warehouse?
Si tu reporting es operativo (ventanas temporales recientes, volumen moderado, dashboards interactivos), MySQL con réplicas y tablas resumen puede ser suficiente. Si haces historia profunda, scans amplios y slicing multidimensional pesado, un warehouse reducirá el dolor.
10) ¿Y si nuestro equipo ya conoce MongoDB y no SQL?
Aprende lo suficiente de SQL para operar reporting de forma sensata. El reporting es una interfaz de negocio, no un concurso de preferencias de ingeniería. La alfabetización en SQL paga el alquiler.
Conclusión: siguientes pasos pragmáticos
Si estás eligiendo entre MySQL y MongoDB específicamente para reporting y analítica, elige SQL a menos que puedas demostrar claramente que tus preguntas de reporting tienen forma documental y se mantendrán así. No lo harán. La organización cambiará de opinión. Siempre lo hace.
Haz esto a continuación:
- Mueve el reporting fuera del camino de la base de datos primaria: réplicas, clúster de reporting o una tienda SQL separada.
- Escribe tus 20 consultas de reporting principales y trátalas como ciudadanos de primera clase: índices, planes y presupuestos.
- Deja de embeder dimensiones mutables en hechos inmutables a menos que quieras explícitamente la “verdad al escribir”. Modela la historia intencionalmente.
- Construye una pequeña capa semántica (vistas/tablas) para que la compañía pueda discutir definiciones sin pelear por pipelines.
- Operacionaliza los hábitos aburridos: revisión de consultas lentas, alertas de lag de réplicas y tests de carga con datos similares a producción.
Los equipos “vuelven a SQL” porque SQL es más antiguo, sí—pero principalmente porque es operativamente legible bajo presión. Cuando el reporting se vuelve un sistema crítico para el negocio, la legibilidad vence a la elegancia. Siempre.