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

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

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 TABLE para 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:

  1. Ver plan: EXPLAIN ANALYZE para ver cuál hace seq scan.
  2. 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.

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