5 errores críticos que todos cometen con SQL (y cómo solucionarlos)
Voy al grano: estos errores aparecen en proyectos pequeños y grandes. Detectarlos y arreglarlos eleva rendimiento, seguridad y mantenimiento. Aquí tienes ejemplos concretos, cómo detectarlos y correcciones prácticas.
1) Confiar en scans de tabla: ausencia o mal uso de índices
Qué pasa: consultas frecuentes realizan full table scans, causando latencia y carga alta. Suele ocurrir por columnas sin índice, usar funciones sobre columnas (bloquea el índice) o esperar que el motor «adivine» el mejor plan.
Ejemplo problemático:
SELECT * FROM orders WHERE customer_id = 123;
Si customer_id no está indexado, cada consulta recorre toda la tabla.
Solución:
-- PostgreSQL
CREATE INDEX CONCURRENTLY idx_orders_customer_id ON orders(customer_id);
-- MySQL
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
Evita esto: WHERE LOWER(email) = 'x' — una función sobre la columna evita el uso del índice. En PostgreSQL, usa un índice de expresión:
CREATE INDEX idx_users_lower_email ON users (LOWER(email));
-- o mejor: normaliza emails a minúsculas al guardar
Cómo detectar: usar EXPLAIN ANALYZE (Postgres) o EXPLAIN (MySQL) y revisar planes; revisar logs de consultas lentas y herramientas como pg_stat_statements.
2) SELECT * y sobre-fetching
Qué pasa: pedir todas las columnas transmite datos innecesarios (cpu, io, red). Además impide que la consulta sea cubierta por un índice.
Ejemplo problemático:
SELECT * FROM users WHERE id = 42;
Solución: pide solo lo que necesitas; si necesitas datos que están en un índice, úsalo como índice cubriente.
SELECT id, email, name FROM users WHERE id = 42;
-- O, si solo necesitas email:
-- CREATE INDEX idx_users_email_id ON users (id, email);
-- SELECT email FROM users WHERE id = 42;
Cómo detectar: revisa payloads en endpoints, perfiles de red y EXPLAIN para ver si la consulta lee columnas innecesarias.
3) UPDATE/DELETE sin WHERE o joins mal construidos (productos del cartesian)
Qué pasa: comandos que alteran o borran más filas de las previstas. Causa downtime lógico, pérdida de datos y horas de depuración.
Ejemplo peligroso:
-- ¡Peligro!
UPDATE orders SET status = 'cancelled'; -- se aplica a toda la tabla
Mejor patrón: siempre prueba con una SELECT idéntica antes de ejecutar la modificación; envuelve en transacción; cuando sea posible, usa RETURNING para comprobar cambios (Postgres).
BEGIN;
SELECT id FROM orders WHERE id = 123; -- verificar
UPDATE orders SET status = 'cancelled' WHERE id = 123 RETURNING id, status;
COMMIT;
-- Si hay joins:
UPDATE o
SET status = 'cancelled'
FROM customers c
WHERE o.customer_id = c.id AND c.flag = true; -- revisar JOIN para evitar multiplicación
Cómo minimizar riesgo: usar transacciones, backups, LIMIT con ORDER BY en casos de correcciones iterativas, y pruebas en staging.
4) Construir consultas concatenando cadenas: inyección SQL
Qué pasa: concatenar parámetros directamente en la consulta permite inyección y compromete datos o integridad.
Ejemplo vulnerable (pseudocódigo):
// NO usar
const q = "SELECT * FROM users WHERE email = '" + email + "'";
db.query(q);
Corrección: usar consultas parametrizadas / prepared statements.
// Node + pg (Postgres)
const q = 'SELECT id, email FROM users WHERE email = $1';
const res = await client.query(q, [email]);
// Python + psycopg2
cur.execute('SELECT id FROM users WHERE email = %s', (email,));
Cómo detectar: revisa el código para concatenaciones de strings con inputs; ejecuta pruebas de seguridad (OWASP SQLi), escaneo dinámico y revisiones de PR que validen parámetros.
5) N+1 queries y falta de batching
Qué pasa: leer una lista y por cada fila ejecutar otra consulta produce latencia exponencial. Muy común en ORMs mal configurados.
Ejemplo N+1 (pseudocódigo):
-- Pseudocódigo: obtener posts y sus comentarios
posts = db.query('SELECT * FROM posts WHERE user_id = ?', [user_id]);
for (p of posts) {
p.comments = db.query('SELECT * FROM comments WHERE post_id = ?', [p.id]);
}
Solución: hacer JOINs, usar IN (...) o aprovechar funciones de agregación/arrays para traer todo en una consulta.
-- Usando JOIN (devuelve duplicados por fila de comment, agrupa si se necesita)
SELECT p.id, p.title, c.id AS comment_id, c.body
FROM posts p
LEFT JOIN comments c ON c.post_id = p.id
WHERE p.user_id = 42;
-- O, resultados agregados en Postgres
SELECT p.id, p.title, COALESCE(json_agg(c.*) FILTER (WHERE c.id IS NOT NULL), '[]') AS comments
FROM posts p
LEFT JOIN comments c ON c.post_id = p.id
WHERE p.user_id = 42
GROUP BY p.id;
-- Alternativa con IN (batching)
SELECT * FROM comments WHERE post_id IN (1,2,3,4);
Cómo detectar: revisa logs para múltiples consultas similares por request; utiliza tracing (OpenTelemetry) y perfiles de ORM (Eloquent, ActiveRecord, Hibernate) que muestran N+1 warnings.
Herramientas rápidas para diagnosticar
- EXPLAIN / EXPLAIN ANALYZE (Postgres), EXPLAIN (MySQL): revisa scans, joins y costos.
- pg_stat_statements: identifica las consultas más costosas en Postgres.
- Slow query log (MySQL): activa y revisa patrones repetidos.
- Tracing / APM (Datadog, New Relic, OpenTelemetry): identificar latencias por request.
Checklist rápido para cada PR que toca SQL
- ¿Se usaron parámetros en lugar de concatenar? (evitar inyección)
- ¿Se seleccionan solo las columnas necesarias?
- ¿La consulta usa índices o necesita un índice nuevo? (ver EXPLAIN)
- ¿Hay riesgo de N+1? ¿Se puede hacer batching o un JOIN eficiente?
- ¿UPDATE/DELETE tienen WHERE verificado y pruebas en staging?
Consejo avanzado: automatiza la detección básica en CI: ejecuta EXPLAIN en las queries más críticas (o los mappers/queries SQL de tu repo) y falla el pipeline si detecta full table scans repetidos sobre tablas grandes. También configura el slow query log y agrega alertas si el 95º percentil de latencia sube.
Siguiente paso: identifica las 10 consultas más frecuentes/nevralgicas en tu app (pg_stat_statements o slow log) y aplica EXPLAIN + pequeños cambios (index, SELECT explícito, batching). Repite y automatiza la verificación en tu flujo de desarrollo.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación