7 mejores prácticas para optimizar consultas SQL en producción

sql 7 mejores prácticas para optimizar consultas SQL en producción

7 mejores prácticas para optimizar consultas SQL en producción

Este post va directo al grano: técnicas prácticas y comprobables para consultas SQL más rápidas y estables en entornos reales. Incluye ejemplos, por qué funcionan y cuándo aplicarlos.

1) Evita SELECT * — solicita solo columnas necesarias

SELECT * carga datos extra, impide que el optimizador use índices cubriendo y aumenta I/O de red y memoria.

-- Malo (trae columnas innecesarias):
SELECT * FROM orders WHERE customer_id = 42;

-- Mejor (trae solo lo que necesitas):
SELECT id, total_amount, created_at FROM orders WHERE customer_id = 42;

Por qué: reduce ancho de banda, permite index-only scans si las columnas están en el índice, y hace más claras las dependencias del código con la DB.

2) Índices: crea índices bien pensados y evita los automáticos

Regla general: indexa columnas usadas en WHERE, JOIN y ORDER BY. Pero recuerda: cada índice penaliza INSERT/UPDATE/DELETE.

  • Índice simple: para igualdad en una columna.
  • Índice compuesto: útil cuando filtras por varias columnas en un orden consistente. Ejemplo:
-- Postgres/MySQL
CREATE INDEX idx_orders_customer_created ON orders (customer_id, created_at DESC);

Consejo: el orden importa. Si consultas WHERE customer_id = ? AND created_at < ?, el índice compuesto funciona bien. Si usas ORDER BY created_at DESC y filtras por customer_id, combina ambos.

3) Usa EXPLAIN/EXPLAIN ANALYZE y entiende planes

No adivines: inspecciona el plan. Busca scans completos de tabla, nested loop costosos y conversiones de tipos.

-- Postgres
EXPLAIN ANALYZE
SELECT id FROM orders WHERE customer_id = 42 AND status = 'paid';

Qué revisar:

  • ¿Usa índice o seq scan?
  • Coste estimado vs real (si están muy distintos, stats obsoletas)
  • Nested Loop vs Hash Join (elige según cardinalidad)

4) Evita funciones en columnas indexadas y conversiones implícitas

Si aplicas funciones sobre columnas (por ejemplo, LOWER(name) = 'x') el índice no se suele usar, salvo que crees un índice funcional.

-- Malo: esto puede prevenir uso de índice
SELECT * FROM users WHERE LOWER(email) = 'foo@example.com';

-- Mejor 1: almacenar el valor normalizado en otra columna
ALTER TABLE users ADD COLUMN email_lower text;
UPDATE users SET email_lower = LOWER(email);
CREATE INDEX idx_users_email_lower ON users (email_lower);
-- y mantenerlo en triggers o en la app

-- Mejor 2 (Postgres): índice funcional
CREATE INDEX idx_users_lower_email ON users (LOWER(email));

También evita comparar tipos distintos (e.g., varchar con text o int con text) para que no haya conversiones implícitas.

5) Reescribe consultas: JOINs vs subconsultas, EXISTS vs IN

Correlated subqueries pueden ser muy costosas. Considera JOINs o window functions.

-- Malo (correlated subquery por fila):
SELECT o.id, (SELECT count(*) FROM order_items oi WHERE oi.order_id = o.id) AS items
FROM orders o;

-- Mejor (aggregation + join):
SELECT o.id, COALESCE(c.items, 0) AS items
FROM orders o
LEFT JOIN (
  SELECT order_id, COUNT(*) AS items FROM order_items GROUP BY order_id
) c ON c.order_id = o.id;

IN vs EXISTS: para grandes conjuntos, EXISTS suele ser más eficiente en muchos motores:

-- Prefiere EXISTS si la subconsulta es grande
SELECT * FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id AND o.total_amount > 100);

6) Paginación eficiente: evita OFFSET si buscas rendimiento

OFFSET N obliga a la DB a recorrer N filas. Para paginación «infinita», usa el método seek (keyset pagination).

-- Malo (OFFSET costoso en páginas largas)
SELECT id, created_at FROM events ORDER BY created_at DESC LIMIT 20 OFFSET 100000;

-- Mejor (paginación por marcador):
SELECT id, created_at FROM events
WHERE (created_at < :last_seen_created_at)
ORDER BY created_at DESC LIMIT 20;

Keyset es más rápido y más estable cuando los datos cambian durante la paginación.

7) Mantén estadísticas, configura parámetros y monitoriza

Un optimizador con estadísticas obsoletas genera planes pobres. En Postgres ejecuta ANALYZE regularmente; en MySQL, asegúrate de que ANALYZE TABLE y innodb_stats estén saneados.

-- Postgres (manual)
VACUUM ANALYZE;
-- o para tabla específica
ANALYZE orders;

Monitoriza:

  • Consultas lentas (slow query log / pg_stat_statements)
  • Locks y transacciones largas
  • IO, CPU y contención en índices

Prácticas complementarias (rápidas)

  • Usa prepared statements / parametrización para el plan cache y seguridad (evita SQL injection).
  • Batch inserts/updates en vez de N filas individuales.
  • Considera particionado por rango (fecha) si la tabla crece mucho; facilita pruning de particiones.
  • Materialized views para cargas analíticas con actualización programada.
  • Evita triggers complejos en rutas críticas de escritura.

Ejemplo práctico: optimización paso a paso

Escenario: tabla orders con millones de filas. Consulta lenta:

SELECT id, customer_id, total_amount
FROM orders
WHERE customer_id = 12345
ORDER BY created_at DESC
LIMIT 50;

Pasos:

  1. EXPLAIN ANALYZE → vemos Seq Scan.
  2. Crear índice compuesto para filtrar y ordenar:
    CREATE INDEX idx_orders_cust_created ON orders (customer_id, created_at DESC);
    
  3. Volver a ejecutar EXPLAIN ANALYZE → ahora usa index scan o index-only scan si incluimos total_amount en índice.
    -- Índice cubriendo (si total_amount es pequeño y quieres evitar visitar la tabla)
    CREATE INDEX idx_orders_cover ON orders (customer_id, created_at DESC, total_amount);
    
  4. Medir latencia antes/después, revisar impacto en INSERTs; si la carga de escritura aumenta mucho, discutir con el equipo el trade-off.

Advertencia: no todos los índices compuestos son benéficos; prioriza consultas críticas y prueba en staging.

Consejo avanzado: si ves que el plan cambia drásticamente según parámetros (parameter sniffing), prueba forcing plans de forma controlada (pg_hint_plan en Postgres, hints en MySQL/Oracle) o restructura la consulta para estabilidad del plan. Mantén un pipeline de pruebas de rendimiento automatizado para validar cambios en índices o consultas.

Si quieres, puedo revisar una consulta lenta concreta: comparte la tabla (DDL), el EXPLAIN ANALYZE y la consulta y te propongo cambios concretos.

Comentarios
¿Quieres comentar?

Inicia sesión con Telegram para participar en la conversación


Comentarios (0)

Aún no hay comentarios. ¡Sé el primero en comentar!

Iniciar Sesión