7 errores críticos que todos cometen con SQL (y cómo solucionarlos)
Este post va directo al grano: errores que veo repetidamente en código y bases de datos de producción, por qué son peligrosos y cómo arreglarlos con ejemplos prácticos. Si gestionas consultas, esquemas o rendimiento, aplícalo hoy mismo.
1) Usar SELECT * en producción
Problema: retornas más columnas de las que necesitas, aumentas I/O, ancho de banda y memoria, y rompes el contrato de la API cuando cambian columnas.
-- Malo
SELECT * FROM users WHERE active = true LIMIT 100;
-- Mejor (selecciona solo lo necesario)
SELECT id, email, created_at FROM users WHERE active = true ORDER BY created_at DESC LIMIT 100;
Por qué importa: menos datos transferidos = consultas más rápidas y menos latencia. Además, evita fugas de datos sensibles.
2) Falta de índices o índices mal definidos
Problema: tablas grandes sin índices adecuados provocan full table scans; índices mal diseñados no se usan.
-- Consulta lenta
SELECT * FROM orders WHERE user_id = 123 AND created_at > now() - interval '30 days';
-- Crea un índice compuesto para esa consulta
CREATE INDEX ix_orders_user_created ON orders (user_id, created_at DESC);
-- Verifica con EXPLAIN ANALYZE (Postgres)
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123 AND created_at > now() - interval '30 days';
Cómo detectar: EXPLAIN/EXPLAIN ANALYZE, monitoreo (slow query log, pg_stat_statements), y profiler de la app. Evita índices inútiles: demasiados índices ralentizan INSERT/UPDATE/DELETE.
3) Patrón N+1 desde la capa de aplicación
Problema: por cada fila haces otra consulta; escalará linealmente y colapsará bajo carga.
-- Código pseudo-problemático (Python/ORM)
for order in orders:
user = db.query(User).filter(User.id == order.user_id).one()
# N queries para N orders
-- Solución: precarga con JOIN o IN
user_ids = [o.user_id for o in orders]
users = db.query(User).filter(User.id.in_(user_ids)).all()
users_by_id = {u.id: u for u in users}
for order in orders:
user = users_by_id[order.user_id]
Alternativa: usa JOIN o la funcionalidad de "eager loading" del ORM (e.g., select_related, prefetch_related).
4) Transacciones mal gestionadas
Problema: transacciones demasiado largas o sin commit/rollback, causan locks, bloquen replicación y pueden inflar WAL.
-- Malo: transacción abierta por mucho tiempo
BEGIN;
-- lógica de negocio lenta (HTTP call, cálculo pesado)
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- sin COMMIT por varios segundos o minutos
-- Mejor: minimizar la sección transaccional
-- haz llamadas externas antes y luego solo la parte crítica en la transacción
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
INSERT INTO ledger (...) VALUES (...);
COMMIT;
También: elige el nivel de aislamiento correcto y, cuando necesites consistencia para una fila, considera SELECT ... FOR UPDATE en lugar de bloquear más de lo necesario.
5) Concatenar strings y permitir SQL injection
Problema: construir consultas con interpolación de cadenas permite inyección SQL y compromete datos.
-- Vulnerable
query = "SELECT * FROM users WHERE email = '" + email + "'";
-- Seguro: parámetros/prepared statements
-- En Postgres (psycopg2)
cur.execute('SELECT * FROM users WHERE email = %s', (email,))
Siempre usa consultas parametrizadas en la capa de acceso a datos. Activa WAF/IDS si estás expuesto, pero la defensa real es parametrizar.
6) Aplicar funciones en columnas indexadas (rompe el uso del índice)
Problema: si haces WHERE LOWER(name) = 'ana' y el índice está sobre name, la base no usará el índice por defecto.
-- Malo (no usa índice en Postgres)
SELECT * FROM customers WHERE LOWER(name) = 'ana';
-- Soluciones:
-- 1) crear un índice funcional
CREATE INDEX ix_customers_lower_name ON customers (LOWER(name));
-- 2) almacenar una columna normalizada y consultar sobre ella
ALTER TABLE customers ADD COLUMN name_lower TEXT;
UPDATE customers SET name_lower = LOWER(name);
CREATE INDEX ix_customers_name_lower ON customers (name_lower);
-- Mantén la columna actualizada via trigger o en la app
Regla: evita transformar la columna en el WHERE; si necesitas, usa índices funcionales o columnas derivadas.
7) Tipos de datos incorrectos y falta de constraints
Problema: campos de tipo texto para fechas/facturas/monedas, sin CHECKs ni FK, llevan a inconsistencias y a costosas transformaciones en tiempo de consulta.
-- Malo
CREATE TABLE invoices (
id SERIAL PRIMARY KEY,
amount FLOAT,
issued_date TEXT
);
-- Mejor
CREATE TABLE invoices (
id SERIAL PRIMARY KEY,
amount NUMERIC(12,2) NOT NULL CHECK (amount >= 0),
issued_date DATE NOT NULL,
customer_id INT NOT NULL REFERENCES customers(id)
);
Usa tipos adecuados: NUMERIC/DECIMAL para dinero, DATE/TIMESTAMP para fechas, JSONB para JSON en Postgres. Añade NOT NULL, CHECK y FOREIGN KEYS para mantener integridad.
Cómo detectar estos problemas en tu base
- Revisa el slow query log y usa EXPLAIN ANALYZE regularmente.
- Habilita y consulta herramientas como pg_stat_statements (Postgres) o Performance Schema (MySQL).
- Audita el acceso a la BD desde la app: busca concatenaciones de strings y patrones N+1 en el código.
Checklist rápido para auditorías
- Sustituir SELECT * en endpoints públicos.
- Verificar índices para consultas frecuentes (EXPLAIN).
- Detectar y resolver N+1 en lógica de negocio.
- Revisar transacciones y acortar su duración.
- Eliminar concatenaciones SQL y usar prepared statements.
- Crear índices funcionales si aplicas funciones en WHERE.
- Corregir tipos de datos y añadir constraints.
Consejo avanzado: instala monitoreo de consultas (pg_stat_statements + pganalyze o similares), automatiza pruebas de carga para endpoints críticos y añade reglas de CI que ejecuten un set de EXPLAINs básicos contra migraciones/queries nuevas. Si no lo haces, incluso pequeños cambios pueden degradar tu base de datos en producción.
Advertencia: cambia índices y esquemas en ventanas de mantenimiento y verifica el impacto en INSERT/UPDATE/DELETE. El siguiente paso por tu cuenta: corre EXPLAIN ANALYZE en tus 10 queries más lentas y aplica las correcciones aquí descritas en una rama de pruebas.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación