Optimización de Rendimiento¶
Casi toda "lentitud de base de datos" que nos llega no es lentitud de la base: es una consulta sin índice, un ORM emitiendo trescientas consultas por petición, o un conjunto de datos que dejó de caber en memoria. Cambiar el servidor por uno más grande enmascara el problema unos meses y devuelve la misma cuenta después.
El método es siempre el mismo: medir, encontrar las pocas consultas que explican la mayor parte del tiempo, corregir la causa, medir de nuevo.
El orden importa: corregir una capa abarata la de abajo. El camino inverso no funciona.
1. Medir antes de tocar¶
Sin números, optimizar es adivinar.
| Fuente | Qué revela |
|---|---|
pg_stat_statements (PostgreSQL) | Consultas por tiempo total acumulado — el ranking que importa |
performance_schema / slow query log (MySQL) | Consultas lentas y las ejecutadas muchas veces |
| Métricas de sistema | CPU, latencia de I/O, memoria, conexiones activas |
| Métricas de replicación | Lag, que delata escritura pesada o réplica subdimensionada |
| Trazas en la aplicación | Dónde gasta tiempo la petición: base, red o el propio código |
El tiempo total gana al tiempo individual
Una consulta de 2 segundos ejecutada 10 veces al día importa menos que una de 20 ms ejecutada 200 mil veces. Ordene por tiempo total acumulado, no por la consulta más lenta.
2. La aplicación: el problema más barato de corregir¶
- N+1 — una consulta para listar, más una por ítem. Es el patrón más común y el de mayor ganancia: se resuelve con
JOINo carga por lotes. SELECT *— trae columnas que nadie usa, impide el índice de cobertura e infla la red.- Paginación con
OFFSETalto —OFFSET 100000hace que la base recorra y descarte 100 mil filas. La paginación por clave (keyset) no tiene ese costo. - Ausencia de lotes — mil
INSERTindividuales cuestan mucho más que unINSERTcon mil filas. - Transacción larga — retiene recursos, retrasa la limpieza de versiones antiguas y aumenta el lag de las réplicas.
3. Plan de ejecución¶
Leer el plan es lo que separa una corrección de un intento.
-- PostgreSQL: con tiempo real y páginas leídas, no solo la estimación
EXPLAIN (ANALYZE, BUFFERS) SELECT ... ;
-- MySQL
EXPLAIN ANALYZE SELECT ... ;
Qué buscar:
- Recorrido secuencial en tabla grande — falta un índice, o el índice existe y no puede usarse (función sobre la columna, tipo incompatible,
LIKE '%algo'). - Estimación muy lejos de lo real — estadísticas desactualizadas. Ejecute
ANALYZEantes de concluir cualquier otra cosa. - Ordenación en disco — memoria de trabajo insuficiente para el
ORDER BY/GROUP BY. - Método de junción equivocado — casi siempre consecuencia de una mala estimación, no del planificador.
4. Índices¶
Un buen índice resuelve; demasiados índices cuestan en cada escritura.
- Índice compuesto en el orden correcto — columnas de igualdad primero, rango al final. Un índice en
(cliente_id, creado_en)sirve a la consulta por cliente y por cliente + período; al revés no. - Índice de cobertura — cuando el índice contiene todas las columnas de la consulta, la base ni siquiera toca la tabla.
- Índice parcial — en PostgreSQL, indexar solo las filas que importan (
WHERE status = 'activo') reduce tamaño y costo de mantenimiento. - Los índices no usados son pérdida — ocupan disco, encarecen cada
INSERTe inflan la copia de seguridad. Las estadísticas de uso muestran cuáles nunca se leyeron. - Índice duplicado —
(a)es redundante cuando existe(a, b).
5. Memoria y caché¶
| Motor | Parámetro central | Regla de partida |
|---|---|---|
| MySQL / InnoDB | innodb_buffer_pool_size | ~60–70% de la RAM en servidor dedicado |
| PostgreSQL | shared_buffers + caché del SO | ~25% en shared_buffers, contando con la caché del sistema |
| PostgreSQL | work_mem | Por operación de ordenación — cuidado: se multiplica por conexión |
| ClickHouse | Límite de memoria por consulta | Evita que una agregación tumbe el nodo |
La señal a seguir es la tasa de acierto de caché. Cuando cae de forma sostenida, el conjunto activo creció más allá de la RAM — y el siguiente paso es memoria o particionamiento, no otro índice.
6. Conexiones¶
A las bases de datos no les gustan miles de conexiones. Cada una consume memoria y disputa CPU.
- Pool obligatorio — PgBouncer en PostgreSQL, ProxySQL en MySQL, o el pool del propio framework, bien configurado.
- Menos conexiones, más throughput — pasado cierto punto, agrandar el pool empeora el rendimiento: la cola se traslada del pool al interior de la base, donde es más cara.
- Separar lectura de escritura — dirigir la lectura a las réplicas alivia al primario y es el camino más directo para escalar lectura.
7. Almacenamiento e I/O¶
Lo que queda después de todo es físico:
- latencia de escritura del volumen, especialmente para el log de transacciones;
- IOPS disponibles frente a las exigidas en el pico;
- separación entre volumen de datos y volumen de log;
- compresión, que cambia CPU por I/O — casi siempre un buen negocio en carga analítica.
Ganancias típicas¶
| Corrección | Ganancia observada |
|---|---|
| Índice ausente en consulta de alta frecuencia | 10× a 1000× en la consulta |
| Eliminación de N+1 | 5× a 50× en la latencia de la petición |
buffer pool dimensionado para el conjunto activo | 2× a 20× en carga de lectura |
| Pool de conexiones corregido | Fin de los picos de error y latencia más estable |
| Lectura dirigida a réplica | Alivio proporcional al volumen de lectura |
Un servidor más grande es la última alternativa, no la primera
Aumentar el hardware antes de mirar el plan de ejecución resuelve por un tiempo y cobra después — con una cuenta mayor y el mismo problema.
Páginas Relacionadas¶
- Administración — Revisión periódica de consultas lentas
- Replicación — Escalar lectura sin sobrecargar al primario
- MySQL Multi-Región · PostgreSQL Multi-Región — Cuando la latencia es de red, no de base
- Alta Disponibilidad ClickHouse — Carga analítica separada del OLTP
- Instalación — Dimensionamiento que evita la mitad de estos problemas