Guía completa de optimización de consultas SQL para desarrolladores
Esta guía reúne técnicas prácticas, ejemplos y diagnóstico para mejorar el rendimiento de consultas SQL. Está pensada para desarrolladores y DBAs que quieren arreglar problemas reales: desde malas estimaciones y joins ineficientes hasta índices mal diseñados y paginación lenta.
1. Flujo de trabajo mínimo para optimizar
- Medir: reproducir y recopilar métricas (latencia, CPU, I/O).
- Analizar el plan de ejecución (EXPLAIN/EXPLAIN ANALYZE, Query Store, extended events).
- Identificar la causa raíz (escaneos completos, sorts, búsquedas no selectivas).
- Probar cambios aislados (índices, reescritura de consulta, estadísticas).
- Verificar con datos reales o representativos.
2. Diagnóstico: herramientas por motor
- PostgreSQL:
EXPLAIN ANALYZE,pg_stat_statements,auto_explain,pgBadger. - SQL Server: Query Store, Management Studio Actual Execution Plan,
SET STATISTICS IO/TIME ON, Extended Events. - MySQL:
EXPLAIN, Performance Schema, slow query log, pt-query-digest.
3. Principios clave
- SARGability: evita envolver columnas en funciones en WHERE/JOIN (ej.
WHERE UPPER(name) = 'JOSE'evita usar índice). Mejor: normalizar o usar índices expresivos. - Evita SELECT *: tráete solo columnas necesarias para reducir I/O y ancho de red.
- Usa índices adecuados y mantén estadísticas actualizadas.
- Prefiere operaciones que permitan index seeks sobre table scans.
- Ten cuidado con OFFSET/LIMIT para paginación: LIMIT con OFFSET grandes escala mal.
4. Ejemplos prácticos y reescrituras
Ejemplo: función en columna mata índice
Consulta lenta:
SELECT id, name FROM users WHERE LOWER(email) = 'alice@example.com';
Problema: función en columna impide usar índice. Soluciones:
- Crear columna normalizada o índice expresivo:
-- PostgreSQL: índice funcional
CREATE INDEX idx_users_email_lower ON users (LOWER(email));
-- SQL Server: computed column + index
ALTER TABLE users ADD email_lower AS LOWER(email);
CREATE INDEX idx_users_email_lower ON users (email_lower);
-- O cambiar la consulta si el cliente normaliza:
SELECT id, name FROM users WHERE email = 'alice@example.com';
Ejemplo: SELECT * vs cobertura
Consulta:
SELECT id, created_at, amount FROM orders WHERE customer_id = 42 ORDER BY created_at DESC LIMIT 50;
Si hay un índice sobre (customer_id, created_at DESC) y el índice contiene las columnas usadas, el motor puede hacer index-only scan/seek:
-- PostgreSQL
CREATE INDEX idx_orders_customer_created ON orders (customer_id, created_at DESC);
-- SQL Server (puede usar INCLUDE para cobertura)
CREATE INDEX idx_orders_customer_created ON orders (customer_id, created_at DESC) INCLUDE (amount);
Reescritura para evitar joins pesados
Consulta inicial:
SELECT u.id, u.name, o.total FROM users u JOIN orders o ON o.user_id = u.id WHERE o.created_at > now() - interval '30 days';
Si la cardinalidad de users es pequeña, OK. Si orders es gigante y quieres solo usuarios con actividad reciente, considera filtrar primero:
-- Filtrar orders primero (semánticamente igual)
WITH recent_orders AS (
SELECT user_id, SUM(total) AS total
FROM orders
WHERE created_at > now() - interval '30 days'
GROUP BY user_id
)
SELECT u.id, u.name, r.total
FROM recent_orders r
JOIN users u ON u.id = r.user_id;
Esto reduce el set que participa en el JOIN.
5. Índices: reglas y antipatróns
- Índice en columnas usadas en WHERE, JOIN y ORDER BY (en ese orden de prioridad).
- Prefiere índices compuestos donde la columna de búsqueda aparece primero.
- Cuidado con índices demasiado amplios: alta cardinalidad y muchas columnas incluidas aumentan coste de mantenimiento (inserts/updates).
- Usa índices filtrados (SQL Server) o partial indexes (Postgres) para reducir tamaño si aplicable (ej. solo rows activas).
- Evita demasiados índices en tablas de escritura intensa.
Índice parcial en PostgreSQL
CREATE INDEX idx_active_users_email ON users (email) WHERE active = true;
6. Estadísticas, cardinalidad y estimaciones
El optimizador depende de las estadísticas para estimar filas. Si las estimaciones son malas, el plan puede ser ineficiente.
- Postgres:
ANALYZEo autovacuum. Ajustadefault_statistics_targetpara columnas con distribuciones complejas. - SQL Server: actualización automática de estadísticas, pero en cargas grandes quizá necesites manualmente
UPDATE STATISTICSo habilitar Trace Flag para autocreate stats agressive. - Detecta parámetro sniffing (plan malo por valores no representativos). Soluciones: OPTIMIZE FOR UNKNOWN, recompile, plan guides, or use local variables in stored procs carefully.
7. Paginar eficientemente
OFFSET + LIMIT penaliza con offsets grandes. Usa keyset pagination (seek method):
-- Lento con OFFSET
SELECT * FROM posts ORDER BY created_at DESC LIMIT 100 OFFSET 100000;
-- Keyset pagination (rápida)
SELECT * FROM posts WHERE created_at < :last_created_at ORDER BY created_at DESC LIMIT 100;
8. Batch operations y mantenimiento
- Para deletes/updates masivos: procesar en lotes (ej. 1000-10000 filas por batch) para reducir bloqueo y llenar WAL.
- Vacuum/maintenance: evitar bloat en Postgres; rebuild/reorganize índices en SQL Server según fragmentación.
- Monitorea contadores de I/O, latencia y lock waits.
9. Temp tables vs table variables vs CTEs
- CTEs en algunos motores son optimizables (Postgres) y en otros materializados por defecto (dependiendo de versión); no asumas que mejoran rendimiento. Evalúa su impacto en el plan.
- Temp tables pueden ser útiles para almacenar resultados intermedios y agregar índices sobre ellos.
- Table variables en SQL Server no mantienen estadísticas (en versiones antiguas), lo que puede producir planes subóptimos para grandes volúmenes.
10. Operaciones caras comunes
- Sorts/ORDER BY: si los datos no están en orden por índice, provoca memorización y spill to disk.
- Hash Join vs Nested Loop vs Merge Join: la elección depende del tamaño y orden - las estadísticas guían la decisión.
- Network latency por transferir demasiados datos al cliente: filtra lo más posible en el servidor.
11. Ejemplo completo: optimizando una consulta
Esquema simplificado:
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT,
status TEXT,
created_at TIMESTAMP,
amount NUMERIC
);
CREATE INDEX idx_orders_customer_created ON orders (customer_id, created_at DESC);
Consulta lenta (devuelve últimas 50 órdenes activas por customer):
SELECT o.*
FROM orders o
WHERE o.customer_id = 12345
AND o.status = 'active'
ORDER BY o.created_at DESC
LIMIT 50;
Diagnóstico:
- EXPLAIN ANALYZE muestra table scan y sort.
- Falta índice que cubra
(customer_id, status, created_at).
Solución:
-- Postgres: índice compuesto y cover
CREATE INDEX idx_orders_cust_status_created ON orders (customer_id, status, created_at DESC);
-- O incluir 'amount' si la consulta lo necesita y quieres index-only scan
CREATE INDEX idx_orders_cover ON orders (customer_id, status, created_at DESC) INCLUDE (amount);
Resultado: index seek + index-only scan, evita sort y reduce I/O.
12. Otros trucos y consideraciones
- Evita implicit conversions: comparar distintos tipos puede impedir el uso de índices.
- Para agregaciones grandes, validar si pre-aggregar en ETL o mantener materialized views es apropiado.
- Monitorea contención por locks y espera de LWT/long transactions en Postgres (vacuum bloating si hay transacciones largas).
- En sistemas distribuidos o cloud (Aurora, RDS), ten en cuenta I/O y throttling de la capa almacenada.
13. Checklist rápido antes de optimizar
- ¿Reproducible con datos representativos?
- ¿EXPLAIN ANALYZE muestra discrepancia entre estimado y real?
- ¿Existen índices adecuados para WHERE/JOIN/ORDER BY?
- ¿Se puede reescribir la consulta para reducir rows tempranamente?
- ¿Las estadísticas están actualizadas?
- ¿Cuánto costaría el mantenimiento del índice propuesto en writes?
14. Herramientas y lecturas recomendadas
- Documentación oficial de EXPLAIN para tu motor (Postgres, SQL Server, MySQL).
- pg_stat_statements, Query Store, pt-query-digest.
- Libros y artículos sobre optimizadores: entendiendo cardinalidad y selectividad.
Consejo avanzado: captura un workload real (p.ej. pg_stat_statements o Query Store), identifica las queries que consumen más recursos acumulados y prioriza por beneficio de optimización. Si trabajas con aplicaciones críticas, prueba cambios en un entorno con datos representativos para evitar efectos colaterales como bloqueos, aumento de latencia en writes o regresiones por plan switching.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación