7 errores críticos que todos cometen con SQL (y cómo evitarlos)
Trabajar con bases de datos relacionales parece sencillo hasta que una consulta lenta, un deadlock o una inyección arruinan tu día. Aquí tienes 7 errores comunes en SQL con porqués, cómo detectarlos y soluciones prácticas con ejemplos. Ve al grano: código, explicaciones claras y comandos para comprobar en producción.
1) Usar SELECT * en producción
Por qué ocurre: comodidad. SELECT * trae todas las columnas, muchas veces innecesarias, aumentando ancho de banda, I/O y memoria. Además rompe cuando cambias esquema.
Detectar: revisa consultas que transfieren muchas columnas, logs de consultas lentas o herramientas APM.
Solución: seleccionar solo las columnas necesarias y documentar el contrato de la consulta.
-- Evitar esto
SELECT * FROM orders WHERE customer_id = 42;
-- Mejor
SELECT id, total_amount, created_at FROM orders WHERE customer_id = 42;
2) No usar índices o usar índices incorrectos
Por qué ocurre: diseños rápidos, desconocimiento de cómo funcionan los índices o su mantenimiento (inserciones/updates más lentos si abusas).
Detectar: EXPLAIN/EXPLAIN ANALYZE muestra full table scans; tiempos altos en índices inexistentes.
Solución: crear índices adecuados y usar índices compuestos cuando las consultas filtran por varias columnas.
-- Consulta lenta: filtro por customer_id y created_at
SELECT id, total_amount FROM orders WHERE customer_id = 42 AND created_at > '2025-01-01';
-- Crear índice compuesto para cubrir el WHERE
CREATE INDEX idx_orders_customer_created_at ON orders (customer_id, created_at);
-- Para PostgreSQL, usar EXPLAIN ANALYZE
EXPLAIN ANALYZE SELECT id, total_amount FROM orders WHERE customer_id = 42 AND created_at > '2025-01-01';
Nota: el orden de columnas en índices compuestos importa: coloca primero las columnas más selectivas o las usadas en igualdad.
3) Aplicar funciones en columnas en filtros (rompe índices)
Por qué ocurre: para normalizar o comparar (ej. LOWER(name) = 'ana') y no se crea índice funcional.
Detectar: EXPLAIN muestra que el índice no es utilizado.
Soluciones:
- Crear índices funcionales (Postgres) o columnas persistidas y indexarlas (MySQL).
- O evitar la función: almacenar normalizado o buscar con collation apropiada.
-- Evitar esto (no usa índice en muchas DBs)
SELECT * FROM users WHERE LOWER(email) = 'juan@ejemplo.com';
-- Postgres: crear índice funcional
CREATE INDEX idx_users_lower_email ON users (LOWER(email));
-- O almacenar una columna email_normalized y indexarla
ALTER TABLE users ADD COLUMN email_normalized TEXT;
UPDATE users SET email_normalized = LOWER(email);
CREATE INDEX idx_users_email_normalized ON users (email_normalized);
4) No usar consultas parametrizadas / concatenar strings (riesgo de SQL injection)
Por qué ocurre: rapidez en pruebas, malas prácticas con ORMs o construir queries dinámicas sin escapar.
Detectar: búsqueda en el código por concatenaciones de SQL o logs que muestren queries con valores interpolados.
Solución: usar siempre prepared statements o binding de parámetros.
-- PELIGRO: concatenación directa
sql = "SELECT * FROM users WHERE username = '" + username + "'";
-- CORRECTO (pseudocódigo)
stmt = db.prepare("SELECT * FROM users WHERE username = ?");
stmt.execute([username]);
5) Long-running transactions y bloqueo excesivo
Por qué ocurre: lógica que mantiene transacción abierta mientras hace llamadas externas, o batching inadecuado.
Detectar: queries en pg_stat_activity (Postgres), INFORMATION_SCHEMA.PROCESSLIST (MySQL) con alta duración, aumento de deadlocks.
Soluciones:
- Reducir el trabajo dentro de la transacción: preparar datos antes, luego abrir transacción solo para writes necesarios.
- Usar niveles de aislamiento adecuados (por ejemplo, READ COMMITTED vs SERIALIZABLE) según necesidad.
- Hacer commits frecuentes en procesos de batch para evitar retener locks por mucho tiempo.
-- Ejemplo: mal (transacción abierta durante llamadas externas)
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- Llamada HTTP externa que retrasa el commit
CALL external_service();
COMMIT;
-- Mejor: hacer la llamada externa antes y mantener la transacción corta
CALL external_service();
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
6) Ignorar los tipos de datos y nulabilidad
Por qué ocurre: compatibilidades rápidas, usar tipos genéricos (VARCHAR, TEXT) o flags NULL mal pensadas.
Detectar: columnas con tipos inconsistentes, conversiones frecuentes, uso excesivo de CAST en consultas.
Soluciones:
- Elegir tipos lo más específicos posible (INTEGER, TIMESTAMP WITH TIME ZONE, DECIMAL(10,2)).
- Definir NOT NULL cuando aplique y usar defaults razonables.
-- Mejor definir precisión en montos
-- Evitar FLOAT para dinero
CREATE TABLE invoices (
id SERIAL PRIMARY KEY,
amount DECIMAL(12,2) NOT NULL,
issued_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
7) Confiar ciegamente en el ORM sin entender las queries que genera
Por qué ocurre: productividad del ORM, pero a veces genera N+1 queries, JOINS ineficientes o SELECTs con demasiadas columnas.
Detectar: profiler/SQL logs del ORM, consultas repetidas por petición (N+1).
Soluciones:
- Revisa las queries generadas en staging/prod con muestra de tráfico real.
- Usa herramientas del ORM para eager loading correctamente y evita lazy loads por defecto.
- Cuando sea necesario, escribir consultas SQL optimizadas (views, materialized views o consultas RAW).
-- Ejemplo N+1 (pseudo-ORM)
for order in orders:
print(order.customer.name) -- genera una query por cada order
-- Solución: eager load
orders = Order.query.options(joinedload('customer')).all()
Comprobaciones prácticas rápidas
- Usa EXPLAIN / EXPLAIN ANALYZE para entender planes de ejecución.
- Habilita logs de consultas lentas (slow query log) y revisa con frecuencia.
- Monitorea métricas: latencia, I/O, locks, tasa de cache hits (buffer cache).
Comandos útiles (Postgres):
-- Plan de ejecución y tiempos
EXPLAIN ANALYZE SELECT ...;
-- Consultas más costosas (requiere pg_stat_statements)
SELECT query, total_time, calls FROM pg_stat_statements ORDER BY total_time DESC LIMIT 10;
-- Consultas activas y locks
SELECT pid, now() - pg_stat_activity.query_start AS duration, query FROM pg_stat_activity WHERE state <> 'idle' ORDER BY duration DESC;
Último consejo avanzado: establece una práctica de revisión regular de SQL en tu equipo: lee planes de ejecución en code reviews, añade pruebas de performance para consultas críticas y automatiza alertas para regressiones en latencia. Evita arreglos rápidos que no documentes: lo barato a corto plazo suele costar mucho en producción.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación