5 mejores prácticas para escribir consultas SQL eficientes y seguras
Si trabajas con bases de datos relacionales a diario, hay una diferencia entre consultas que funcionan y consultas que escalan. Aquí tienes prácticas concretas, ejemplos y por qué aplicarlas en tus proyectos (ejemplos orientados a PostgreSQL, pero aplicables a la mayoría de motores).
1) Selecciona solo las columnas que necesitas
Evita SELECT *. Traer columnas innecesarias impacta en I/O, memoria y red, además de evitar el uso de índices cubriendo.
-- MAL
SELECT * FROM orders WHERE customer_id = 42;
-- BIEN
SELECT id, order_date, total_amount FROM orders WHERE customer_id = 42;
Por qué: al limitar columnas, el planner puede usar índices cubiertos (index-only scans) y reduces el tiempo de serialización.
2) Índices: diseño y uso correcto
- No indexar todo: indexar columnas que se usan en WHERE, JOIN y ORDER BY.
- Evita funciones sobre columnas indexadas (ej. LOWER(col)) sin un índice funcional.
- Considera índices parciales o por expresión para patrones específicos.
-- Evitar: función sobre columna impide uso de índice simple
WHERE LOWER(email) = 'a@b.com'
-- Mejor: crear índice funcional
CREATE INDEX idx_users_lower_email ON users (LOWER(email));
-- Índice parcial para estado activo
CREATE INDEX idx_orders_active ON orders(order_date) WHERE status = 'active';
Por qué: los índices bien pensados reducen lecturas; los parciales son útiles para columnas con cardinalidad sesgada.
3) JOINs y filtros: orden y cardinalidad importan
Aplica filtros temprano y evita operaciones que materialicen demasiadas filas antes de filtrar.
-- MAL: traer muchas filas y luego filtrar en aplicación
SELECT o.id, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE c.country = 'US' AND o.total_amount > 100;
-- BIEN: usar índices en ambas condiciones y filtrar por columnas selectivas
-- Asegúrate de tener índices en customers(country) y orders(customer_id, total_amount)
Por qué: el planner estima costos por cardinalidad; mantener estadísticas actualizadas (ANALYZE) es clave.
4) Pagination correcta: evita OFFSET para grandes desplazamientos
OFFSET N es sencillo pero desperdicia trabajo al descartar filas anteriores. Usa paginación por clave (keyset pagination).
-- MAL (lentísimo con OFFSET grande)
SELECT id, created_at FROM posts ORDER BY created_at DESC LIMIT 20 OFFSET 10000;
-- BIEN (keyset pagination)
-- Suponiendo que la última fila mostrada tiene created_at = '2026-01-01' y id = 123
SELECT id, created_at FROM posts
WHERE (created_at < '2026-01-01')
OR (created_at = '2026-01-01' AND id < 123)
ORDER BY created_at DESC, id DESC
LIMIT 20;
Por qué: keyset usa índices y evita escanear/descartar N filas.
5) Mide: EXPLAIN, EXPLAIN ANALYZE y métricas
No adivines: inspecciona planes de ejecución y tiempos reales.
-- Ver plan estimado
EXPLAIN SELECT ...;
-- Ejecuta con tiempos y filas reales (postgreSQL)
EXPLAIN ANALYZE SELECT ...;
Busca: secuencias de escaneo costosas (seq scan), joins cartesianos inesperados, alta escritura de temporales (disk). Si ves sec scans en columnas selectivas, revisa estadísticas o añade índice.
Seguridad y consistencia: prácticas que no fallan
- Usa consultas parametrizadas / prepared statements para evitar SQL injection.
- Define tipos de datos correctos y límites (VARCHAR(n), NUMERIC con precisión) para prevenir datos inválidos y reducir tamaño.
- Maneja transacciones con claridad: COMMIT/ROLLBACK y el isolation level apropiado (por defecto READ COMMITTED en Postgres suele bastar; usa SERIALIZABLE solo cuando lo necesites).
-- Parámetros en aplicación (ejemplo conceptual)
psql.query('SELECT id, name FROM users WHERE email = $1', [email]);
Consejos prácticos y correcciones rápidas
- Evita usar LIKE '%foo%': si necesitas búsquedas por subcadena, considera trigram indexes o full-text search.
- CTEs: en PostgreSQL <=11, las CTEs son fences de optimización; en 12+ puedes controlar MATERIALIZED/NOT MATERIALIZED. Para consultas críticas, compara CTE vs subquery.
- Mantén VACUUM/ANALYZE programado para bases OLTP; configuraciones por defecto no siempre son óptimas para cargas intensivas.
-- Evitar LIKE con wildcard inicial; usar trigram para búsqueda eficiente
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_people_name_trgm ON people USING gin (name gin_trgm_ops);
SELECT id, name FROM people WHERE name ILIKE '%martínez%';
Errores frecuentes y cómo evitarlos
- No actualizar estadísticas tras cambios masivos de datos → ejecutar ANALYZE.
- Crear índices sin medir impacto en escrituras → evalúa costo de insert/update/delete.
- Confiar en un solo ambiente para pruebas de rendimiento → replica producción o usa dataset representativo.
Por qué estas prácticas funcionan
Porque atacan las dos fuentes primarias de lentitud: I/O (lecturas/escrituras innecesarias) y planeamiento ineficiente (estimaciones erróneas). Reducir filas/columnas procesadas, permitir index usage y medir con EXPLAIN es la receta.
Próximo paso avanzado: automatiza pruebas de regresión de consultas (benchmarks reproducibles) y añade monitoreo de consultas lentas en producción. Advertencia: optimizar sin medir puede empeorar el rendimiento; siempre compara métricas antes y después.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación