Qué encontramos

Tres bases de datos, antes y después

Cada historia es un compuesto de análisis reales con nombres, cifras y detalles del esquema cambiados. El patrón siempre es el mismo: la clasificación señala una sentencia, el plan y las estadísticas de la tabla la explican, y el arreglo es pequeño.

Mercado en línea · PostgreSQL 15 en Amazon RDS, tabla de pedidos de 3,1 GB, 40 peticiones/s en hora punta

Un índice compuesto que faltaba era el 61 % de la CPU del primario

El problema

La latencia del pago se duplicaba cada viernes por la tarde. El equipo había añadido réplicas de lectura dos veces; el primario seguía al 90 % de CPU.

Qué mostró el informe

La primera sentencia por tiempo total era una búsqueda de pedidos filtrada por cliente y estado. Se ejecutaba 48 000 veces por hora y cada ejecución leía toda la tabla de pedidos: un recorrido secuencial que descartaba 4,2 millones de filas por filtro y después ordenaba. Existía un índice sobre created_at, pero nunca se usaba porque las columnas del filtro no estaban en él.

El arreglo

CREATE INDEX CONCURRENTLY orders_customer_status_created_idx ON orders (customer_id, status, created_at DESC); el índice antiguo sin uso se eliminó una semana después.

Antes → después

AntesDespués
Tiempo medio de la sentencia412 ms1,8 ms
CPU del primario en hora punta90 %34 %
p95 del pago2,9 s640 ms
«Llevábamos un mes mirando Grafana. El informe puso el problema en primer lugar y mostró el plan junto a las estadísticas de la tabla; el arreglo llevó diez minutos.»

SaaS de analítica B2B · PostgreSQL 16 autogestionado, tabla de eventos de 52 GB, Kubernetes

Un recuento sobre ocho millones de filas se escondía tras una llamada pequeña a la API

El problema

El widget de uso del panel agotaba el tiempo de espera para las cuentas más grandes. Los ingenieros sospechaban del ORM; estaban a punto de añadir una capa de caché.

Qué mostró el informe

pg_stat_statements mostraba un count(*) ejecutado 120 000 veces al día con una media de 220 ms y un plan que usaba un bitmap scan solo sobre account_id, filtraba después seis millones de filas por fecha y tenía 48 000 bloques de heap con pérdida porque work_mem era de 4 MB.

El arreglo

Un índice compuesto sobre (account_id, occurred_at) convirtió el filtro en condición de índice; el widget lee ahora un agregado por horas para las cuentas por encima de un umbral.

Antes → después

AntesDespués
Tiempo medio de la sentencia220 ms3,4 ms
Parte de la sentencia en el tiempo total de la base31 %menos del 1 %
Tiempos de espera agotados del widget al día1 4000
«El plan decía bloques de heap con pérdida; ninguno de nosotros conocía el término. El prompt de IA del informe lo explicó y propuso el índice.»

Empresa de pagos · PostgreSQL 14 en Supabase, tabla de carritos de 2,3 GB, autovacuum por defecto

El bloat, y no el tráfico, frenaba un UPDATE de una sola fila

El problema

Un UPDATE trivial por id de sesión tenía una media de 95 ms y se ejecutaba un cuarto de millón de veces por hora. Nadie lo había mirado porque cada llamada era «suficientemente rápida».

Qué mostró el informe

No había índice sobre session_id, así que cada actualización recorría 2,3 millones de filas vivas más un millón de filas muertas: la tabla tenía un 31 % de tuplas muertas y autovacuum llevaba dos días sin ponerse al día. La sentencia era la tercera por tiempo total a pesar de su media pequeña.

El arreglo

Un índice sobre (session_id), un autovacuum_vacuum_scale_factor de 0,01 para esa tabla y un VACUUM (ANALYZE) manual en una hora tranquila.

Antes → después

AntesDespués
Tiempo medio de la sentencia95 ms0,4 ms
Tuplas muertas31 %2 %
Tiempo total de la base por hora6,6 h1,9 h
«La clasificación ordena por tiempo total, no por tiempo medio. Esa sola decisión sacó a la luz un problema que nuestra herramienta de APM había filtrado.»

Compruébalo en tu propia base de datos

Gratis para tres análisis al mes. Solo lectura; la contraseña se usa una vez y nunca se guarda.