7 errores críticos que todos cometen con SQL y cómo solucionarlos
Este post va directo al grano: errores frecuentes en SQL que causan bugs, caídas de rendimiento o brechas de seguridad, y cómo arreglarlos con ejemplos prácticos. Para cada error verás por qué sucede, qué daño provoca y cómo solucionarlo (con comandos y snippets reproducibles).
-
Error 1 — Usar
SELECT *en producciónPor qué pasa: comodidad. Daño: tráfico innecesario, más I/O, peor uso de caché y acoplamiento a cambios de esquema.
Ejemplo malo:
SELECT * FROM users WHERE created_at > '2025-01-01';Arreglo:
SELECT id, email, created_at FROM users WHERE created_at > '2025-01-01';Por qué funciona: reduces ancho de banda, mejoras tiempos de deserialización y permites seleccionar columnas indexadas cuando conviene.
-
Error 2 —
UPDATE/DELETEsinWHEREo con condición erróneaPor qué pasa: fat finger, falta de transacciones o tests. Daño: pérdida de datos, downtime.
Ejemplo peligroso:
UPDATE orders SET status = 'cancelled'; -- ¡uh-oh!Protecciones y arreglos:
- Probar con
SELECTantes:SELECT COUNT(*) FROM orders WHERE ... - Usar transacciones y verificaciones:
BEGIN; UPDATE orders SET status = 'cancelled' WHERE id = 123; -- comprobar con SELECT COMMIT;En MySQL habilita
binlog_format=ROWy en entornos críticos usa backups/replicas para recovery. - Probar con
-
Error 3 — No usar índices o usarlos mal (incluye aplicar funciones en columnas indexadas)
Por qué pasa: desconocimiento del planner o diseño rápido. Daño: full table scans y latencias altas.
Ejemplo problemático (Postgres):
SELECT * FROM events WHERE LOWER(user_email) = 'alice@example.com';Problema: función sobre la columna impide usar índice B-tree normal.
Arreglo 1 — índice funcional:
CREATE INDEX idx_events_useremail_lower ON events (LOWER(user_email)); -- ahora la consulta puede usar el índiceArreglo 2 — normalizar/saneado al insertar (mejor para performance):
-- guardar user_email_normalized en minúsculas y consultar esa columnaComprobar con:
EXPLAIN ANALYZE SELECT ... -
Error 4 — Tipos de datos inapropiados e implicit conversions
Por qué pasa: usar strings para todo o incompatibilidades entre columnas/join keys. Daño: descartes de índices, errores sutiles y desperdicio de almacenamiento.
Ejemplo:
-- user_id guardado como text en una tabla y integer en otra SELECT * FROM sessions s JOIN users u ON s.user_id = u.id; -- esto puede forzar conversión y evitar índicesArreglo:
- Usar tipos adecuados: integers para ids, numeric/decimal para dinero, timestamp/datetime para fechas.
- Si no puedes cambiar el esquema: convertir explícitamente durante ETL y crear columnas normalizadas.
-
Error 5 — No usar consultas parametrizadas (riesgo de SQL injection y de plan-cache)
Por qué pasa: concatenar strings para construir SQL. Daño: vulnerabilidades críticas y planes de ejecución no reutilizables.
Ejemplo vulnerable (Node.js):
const q = `SELECT * FROM users WHERE email = '${email}'`; await db.query(q);Arreglo (parametrizado):
// pg (node-postgres) await db.query('SELECT id, email FROM users WHERE email = $1', [email]); // MySQL (mysql2) await db.execute('SELECT id, email FROM users WHERE email = ?', [email]);Beneficios: evita inyección, mejora caché de planes y separación clara entre lógica y datos.
-
Error 6 — No inspeccionar el plan de ejecución (no usar EXPLAIN/ANALYZE)
Por qué pasa: confianza en "parece rápido" o falta de herramientas. Daño: optimizaciones inútiles o peligrosas.
Uso correcto:
-- Postgres EXPLAIN ANALYZE SELECT ...; -- MySQL EXPLAIN FORMAT=JSON SELECT ...;Qué buscar: scans en tablas grandes, cost/high rows estimates, joins en bucle anidado donde quieras hash/merge, uso de índices inexistente. Antes de añadir índices, verifica con EXPLAIN si el planner los usaría.
-
Error 7 — Transacciones demasiado largas o mal entendidas las isolation levels
Por qué pasa: opens/long-running transactions por errores de diseño o polling dentro de una tx. Daño: locks prolongados, bloating (VACUUM/GC), contención y latencia.
Ejemplos de malas prácticas:
BEGIN; -- hacer operaciones de red/IO lentas aquí UPDATE ...; -- esperar respuesta HTTP externa COMMIT;Arreglos
- Minimizar el trabajo dentro de la transacción: solo operaciones DB necesarias.
- Batch y chunking para operaciones masivas:
-- ejemplo de batching -- pseudo: procesar 1000 filas por batch WHILE (1) { WITH cte AS ( SELECT id FROM big_table WHERE processed = false LIMIT 1000 FOR UPDATE SKIP LOCKED ) UPDATE big_table SET processed = true WHERE id IN (SELECT id FROM cte); -- commit y continuar }Evita long transactions en Postgres para impedir bloat; en MySQL/InnoDB ten cuidado con gap locks en REPEATABLE READ.
Checklist rápido para producción
- No usar
SELECT *. - Probar condiciones de
UPDATE/DELETEconSELECTy envolver en transacciones si es necesario. - Crear índices pensando en consultas críticas; evita funciones en columnas a menos que indexes la forma funcional.
- Usar tipos de datos correctos desde el diseño del esquema.
- Usar consultas parametrizadas en la capa de aplicación.
- Revisar el plan con
EXPLAIN ANALYZEantes y después de cambios significativos. - Evitar transacciones largas; procesar en batches y usar
FOR UPDATE SKIP LOCKEDcuando corresponda.
Comandos y herramientas recomendadas
- EXPLAIN / EXPLAIN ANALYZE (Postgres). MySQL: EXPLAIN FORMAT=JSON.
- pg_stat_statements (Postgres) para identificar queries frecuentes y costosas.
- pt-query-digest (Percona) para análisis de queries MySQL.
- Herramientas APM (NewRelic, Datadog) para correlacionar queries con latencia de aplicaciones.
Siguiente paso: elige una de tus consultas más críticas, ejecútala con EXPLAIN ANALYZE, revisa si hay full scans o filas examinadas en exceso, y aplica uno de los arreglos anteriores (index funcional, parametrización, batch o cambio de tipo). Un consejo avanzado: habilita métricas de long-running transactions en tu monitoring y alerta cuando superen X segundos — eso suele descubrir problemas de bloqueo antes de que afecten al SLA.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación