Introducción
Optimizar consultas SQL no es solo microajuste: es diseño, índices, estadísticas y entender el plan de ejecución. Aquí tienes 5 prácticas concretas, con ejemplos y el porqué de cada una, para mejorar rendimiento en bases de datos relacionales (Postgres/MySQL).
Práctica 1 — Evita SELECT * y selecciona solo columnas necesarias
Por qué: reduce I/O, uso de memoria y permite a la base de datos usar índices de cobertura.
-- Malo
SELECT * FROM orders WHERE customer_id = 42;
-- Bueno
SELECT id, total_amount, created_at FROM orders WHERE customer_id = 42;
Si solo necesitas total_amount y created_at, crear un índice que incluya esas columnas puede eliminar la lectura del heap.
Práctica 2 — Índices adecuados: columnas de filtro y de join, orden y selectividad
Por qué: un índice mal pensado no mejora nada; orden y selectividad importan.
- Coloca primero la columna más selectiva en índices compuestos.
- Para consultas que filtran por varias columnas y solo consultan otras columnas, usa índices que incluyan (INCLUDE) las columnas de salida (Postgres) o columnas adicionales para cubrir la consulta (MySQL).
-- Ejemplo Postgres: índice compuesto con INCLUDE
CREATE INDEX idx_orders_customer_status ON orders (customer_id, status) INCLUDE (total_amount, created_at);
-- Ejemplo MySQL (no INCLUDE): crear índice con columnas que se usan en WHERE y en SELECT
ALTER TABLE orders ADD INDEX idx_orders_customer_status (customer_id, status, created_at);
Resultado esperado: la consulta se satisface leyendo solo el índice (index-only scan).
Práctica 3 — Usa EXPLAIN/ANALYZE y métricas antes de cambiar
Por qué: sin entender el plan no sabrás si un índice o reescritura ayuda.
-- Postgres: plan estimado y plan real con tiempos
EXPLAIN ANALYZE SELECT id FROM orders WHERE customer_id = 42 AND status = 'paid';
-- MySQL: plan en JSON
EXPLAIN FORMAT=JSON SELECT id FROM orders WHERE customer_id = 42 AND status = 'paid';
Elementos clave a revisar: tipo de acceso (seq scan, index scan), filas estimadas vs reales, filtros, costo y tiempo. Si las filas reales son mucho mayores que las estimadas, actualiza estadísticas.
Práctica 4 — Evita funciones sobre columnas en WHERE y los casts implícitos
Por qué: funciones y casts impiden el uso de índices en la mayoría de motores.
-- Malo: función sobre la columna evita índice
SELECT * FROM users WHERE LOWER(email) = 'a@b.com';
-- Mejor: normaliza/almacena en columna adicional o busca sin función
SELECT * FROM users WHERE email = 'a@b.com';
-- Malo: cast implícito
SELECT * FROM events WHERE event_date >= '2026-01-01'; -- si event_date es TIMESTAMP y literal string, puede haber cast
-- Mejor: usa tipos correctos
SELECT * FROM events WHERE event_date >= TIMESTAMP '2026-01-01 00:00:00';
Si necesitas búsquedas case-insensitive, considera almacenar una columna normalizada (email_lower) y crear índice sobre ella.
Práctica 5 — Paginar correctamente y evitar operaciones costosas en bucles
Por qué: OFFSET alto obliga a la base a leer muchas filas y descartar; operaciones fila-a-fila generan muchas roundtrips.
-- Malo: paginación con OFFSET en tablas grandes
SELECT id, created_at FROM posts ORDER BY created_at DESC LIMIT 50 OFFSET 50000;
-- Mejor: paginación por keyset (id/created_at)
SELECT id, created_at FROM posts WHERE (created_at, id) < ('2026-07-01', 12345) ORDER BY created_at DESC, id DESC LIMIT 50;
-- Evitar loops: en vez de ejecutar UPDATE por fila desde la app, usa un solo UPDATE con JOIN/WHERE
-- Malo: for each row -> UPDATE ... (miles de queries)
-- Bueno:
UPDATE users u
SET status = 'inactive'
WHERE u.last_login < NOW() - INTERVAL '1 year';
Mantenimiento y métricas operativas
- Actualizar estadísticas: Postgres:
VACUUM ANALYZE/ MySQL:ANALYZE TABLE. - Monitorea consultas lentas: Postgres:
pg_stat_statements, MySQL:slow_query_log. - Reindexar si hay mucha bloat: Postgres
REINDEX, MySQL:OPTIMIZE TABLEpara motores como InnoDB en ciertos casos.
Ejemplo práctico: mejorar una consulta JOIN
Situación: tabla orders (millones) y customers. Consulta lenta:
SELECT o.id, c.name, o.total_amount
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE c.region = 'EU' AND o.created_at > '2026-01-01';
Pasos de optimización:
- Ver plan:
EXPLAIN ANALYZEpara ver cuál hace seq scan. - Añadir índices: índice en customers(region, id) no tiene sentido; mejor: índice en customers(region) y en orders(created_at, customer_id) o un índice compuesto en orders(customer_id, created_at) según selectividad.
-- Índices sugeridos (Postgres)
CREATE INDEX idx_customers_region ON customers (region);
CREATE INDEX idx_orders_customer_created ON orders (customer_id, created_at);
-- Si la consulta selecciona más columnas de orders, usar INCLUDE
CREATE INDEX idx_orders_customer_created_inc ON orders (customer_id, created_at) INCLUDE (total_amount);
Tras aplicar, vuelve a ejecutar EXPLAIN ANALYZE y compara tiempos y filas leídas.
Herramientas y lecturas recomendadas
- Postgres: pg_stat_statements, pg_center, auto_explain
- MySQL: slow_query_log, EXPLAIN FORMAT=JSON, Percona Toolkit
- Lectura: entender cardinalidad y selectividad, y cómo el optimizador estima costos
Errores comunes rápidos
- Crear índices en todas las columnas "por si acaso" (costo en writes y espacio).
- Confiar solo en estimaciones sin verificar ejecuciones reales (EXPLAIN vs EXPLAIN ANALYZE).
- Ignorar mantenimiento: estadísticas desactualizadas causan planes pobres.
Advertencia práctica: un cambio que acelera una consulta puede degradar otras (writes más lentos, locks). Siempre probar en staging y medir antes/después con cargas representativas.
Siguiente paso avanzado: habilita trazas de consultas lentas, captura un sample con bind variables y analiza con EXPLAIN ANALYZE; si necesitas, explora particionamiento por rango/fecha para tablas masivas.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación