From n00b to ZeroCool / Profesionalización
SQL: el hechizo que nunca muere (y por qué tu backend lo necesita bien aprendido)
SQL sigue ganando: queries reales, índices, joins, EXPLAIN y errores comunes. Guía práctica para backend y producción sin sufrir.
Lo que vale la pena leer aquí
Tú juras que no tocaste nada… hasta que te acuerdas del “cambiecito” de la mañana: una query nueva para “nomás traer unos datos extra”.
Intro con gancho
Son las 11:47 pm. Ya estabas cerrando la laptop (esa guerrera que prende cuando quiere) y production empieza a sonar como microondas viejo: lento, ruidoso y con olor a “algo se va a quemar”. En Slack cae el clásico: “¿Por qué la API de pedidos está tardando 8 segundos?”
Tú juras que no tocaste nada… hasta que te acuerdas del “cambiecito” de la mañana: una query nueva para “nomás traer unos datos extra”.
Y ahí aparece el hechizo que nunca muere: SQL.
No importa si estás en Node, Java, Go, Python, Rails o .NET. No importa si tu CTO anda enamorado de “serverless” o de “NoSQL-first”. En cuanto tu app necesita consistencia, reportes que sí salgan, búsquedas decentes, integridad y auditoría… regresas a SQL. Como ese ex que te escribe cuando ya estabas en paz.
Qué vas a aprender
- A escribir queries que no se conviertan en un hoyo negro en producción.
- A leer
EXPLAINsin fingir y ubicar el “aquí está el cuello de botella”. - Cuándo usar
JOIN, cuándo separar en dos queries y cuándo mejor cachear. - Índices: qué sí sirve, qué es humo y cómo no matar escrituras.
- Errores de vida real (de los que cuestan horas y reputación) y cómo evitarlos.
Contexto práctico
SQL no es “nomás SELECT”. En backend se vuelve esto:
- Performance: una query malita puede tumbar tu servicio aunque tu código esté bonito.
- Costo: en cloud, queries lentas = más CPU, más réplicas, más lana.
- Confiabilidad: transacciones, constraints y locks son tu cinturón de seguridad.
- Negocio: el reporte de ventas, el dashboard y la conciliación viven aquí.
Escena real: el jefe te pide “un reporte para mañana a las 9 am” y nadie modeló bien la tabla de eventos. Ahí andas con café del Oxxo y un curso a medias de SQL, armando magia para que el negocio funcione.
Otra: se te cae el internet en la colonia, te vas con hotspot, y el deploy sale tarde. ¿Qué arreglas en corto? Muchas veces: un índice bien puesto o dejar de hacer SELECT *.
Paso a paso: cómo invocar SQL sin romper producción
1) Empieza por la pregunta correcta (no por la tabla)
Antes de escribir SQL, apunta esto en una nota:
- ¿Qué necesito exactamente?
- ¿Cuántas filas espero?
- ¿Cuál es el filtro principal?
- ¿Cuál es el orden?
- ¿Qué tan fresco debe ser el dato (se puede cachear)?
Ejemplo: “Listar pedidos del usuario con total, estatus y fecha, paginados.”
Lo típico:
SELECT *
FROM orders
WHERE user_id = 123
ORDER BY created_at DESC;
Sí, “jala”… hasta que orders crece y tu SELECT * se trae medio mundo (JSONs, columnas que nadie usa, blobs, lo que sea). Mejor ser específico:
SELECT id, total_cents, status, created_at
FROM orders
WHERE user_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3;
Decisión práctica: si ya tienes miles de filas por usuario y estás paginando con OFFSET, ve planeando paginación por cursor. Es de esos fixes que te evitan un rollback a medianoche.
2) Índices: el 80% del “por qué está lento”
Un índice no es acelerador gratis. Es tradeoff: acelera lecturas, encarece escrituras y ocupa espacio. El truco es que el índice le quede como guante a tu query real, no a tu intuición.
Si filtras por user_id y ordenas por created_at, un índice compuesto suele ser buena jugada.
PostgreSQL:
CREATE INDEX CONCURRENTLY idx_orders_user_created
ON orders (user_id, created_at DESC);
MySQL (InnoDB):
CREATE INDEX idx_orders_user_created
ON orders (user_id, created_at);
Notas de batalla:
- En Postgres,
CONCURRENTLYreduce el bloqueo (tarda más, pero es más amable con prod). - No indexes “por fe”. Indexa por query frecuente y dolorosa.
Decisión práctica: si el endpoint está en el top 5 de tráfico, merece índices dedicados. Si es un reporte mensual que nadie ve hasta que hay bronca, quizá conviene una réplica o un job offline.
3) EXPLAIN: aprende a leer el mapa del infierno
Cuando algo va lento, tu mejor compa es el plan de ejecución.
PostgreSQL:
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, total_cents, status, created_at
FROM orders
WHERE user_id = 123
ORDER BY created_at DESC
LIMIT 20;
Qué buscar sin clavarte en todo:
- Seq Scan / Full table scan cuando esperabas índice.
- Rows estimadas vs reales (si difiere muchísimo, stats desactualizadas o query rara).
- Sort costoso (probablemente te falta índice para el orden).
- Nested Loop con millones (a veces duele; a veces es normal, pero hay que medir).
Decisión práctica: si ves Seq Scan en una tabla grande en un endpoint caliente, no lo normalices. Cambias query, agregas índice o cambias estrategia. Punto.
4) JOINs: poderosos, pero no gratis
Error común: meter 5 joins “porque queda elegante”. La base no te aplaude la elegancia; te cobra con CPU.
Ejemplo: pedidos con email del usuario.
SELECT o.id, o.total_cents, o.status, o.created_at,
u.email
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.user_id = $1
ORDER BY o.created_at DESC
LIMIT 20;
Eso suele estar bien.
Cuándo duele:
- Haces join con una tabla enorme sin buen índice.
- Tu
WHEREfiltra poquito y el join explota. - Te cuelgas de una relación 1:N y duplicas filas sin querer.
Decisión práctica: si el join es para un dato que cambia poco (tipo u.email) y el endpoint está en llamas, considera:
- denormalizar poquito (guardar
user_email_snapshotenorders), o - cache, o
- dos queries (primero ids de orders, luego batch) si el plan sale mejor.
5) Paginación: OFFSET es cómodo… hasta que duele
OFFSET se vuelve caro porque la DB “camina” filas para luego tirarlas.
Keyset / cursor pagination suele ser más estable:
SELECT id, total_cents, status, created_at
FROM orders
WHERE user_id = $1
AND created_at < $2
ORDER BY created_at DESC
LIMIT 20;
Decisión práctica: si hay empates por created_at, usa cursor compuesto (created_at, id) para no saltarte o duplicar registros.
6) Transacciones: tu seguro contra datos chuecos
Si haces dos o más cambios que deben ser “todo o nada”, usa transacciones. La neta, esto salva proyectos.
BEGIN;
UPDATE accounts
SET balance_cents = balance_cents - 5000
WHERE id = 10;
UPDATE accounts
SET balance_cents = balance_cents + 5000
WHERE id = 25;
COMMIT;
Decisión práctica: dinero, inventarios, reservas, límites… eso casi siempre pide transacciones + constraints. No lo dejes al “lo validamos en el backend” porque luego llega un bug, un retry, o un job duplicado y ya valió.
7) Constraints: deja que la DB te cuide
Si tu DB no tiene constraints, tu app vive en modo “a ver si no se rompe”. Y cuando se rompe, se rompe raro.
Ejemplos útiles:
ALTER TABLE orders
ADD CONSTRAINT orders_total_nonnegative
CHECK (total_cents >= 0);
ALTER TABLE orders
ADD CONSTRAINT orders_user_fk
FOREIGN KEY (user_id) REFERENCES users(id);
CREATE UNIQUE INDEX uniq_users_email ON users (email);
Decisión práctica: constraints te ahorran bugs raros en sincronías, scripts de emergencia y jobs que corren a las 2 am (cuando nadie quiere pensar).

Screenshots sugeridos
- Captura de
EXPLAIN (ANALYZE, BUFFERS)mostrando unSeq Scany luego el mismo query usandoIndex Scandespués de crear el índice. - Screenshot de Datadog/New Relic/Grafana donde se vea el p95 de un endpoint antes/después.
- Imagen de un cliente SQL (DBeaver/pgAdmin/TablePlus) con la query paginada por cursor.
Errores comunes (y cómo salir vivo)
Error 1: SELECT * en endpoints calientes
Síntoma: sube el tiempo de respuesta y el consumo de red/CPU.
Solución: selecciona columnas, evita traer blobs/JSON gigantes y valida si realmente necesitas esa data.
Error 2: N+1 queries disfrazado de “código limpio”
Síntoma: el backend hace 1 query para listar y luego 50 queries para detalles.
Solución: usa joins o batch con IN (...).
SELECT id, email
FROM users
WHERE id = ANY($1);
Error 3: índices duplicados o inútiles
Síntoma: escrituras lentas, migraciones pesadas y la query sigue igual.
Solución: compara índices existentes, revisa cardinalidad y confirma con EXPLAIN. En Postgres, date una vuelta por pg_stat_user_indexes.
Error 4: LIKE '%texto%' sin estrategia
Síntoma: búsquedas lentas en tablas grandes.
Solución: usa full-text search (Postgres tsvector) o trigram (pg_trgm), o manda búsquedas complejas a un motor dedicado.
Error 5: falta de límites en queries de soporte
Síntoma: alguien de soporte corre una query sin WHERE en prod (el clásico “solo era tantito”).
Solución: políticas y herramientas: usuarios read-only, réplicas para lectura, statement_timeout (Postgres) o límites en el cliente.
Ejemplo Postgres:
SET statement_timeout = '3s';

Checklist final
- ¿Tu query trae solo las columnas necesarias?
- ¿Tienes índice alineado con
WHERE+ORDER BY? - ¿Validaste con
EXPLAIN (ANALYZE, BUFFERS)? - ¿Evitaste
OFFSETsi la tabla/usuario crece mucho? - ¿Tus joins no duplican filas sin querer?
- ¿Tus writes importantes están en transacción?
- ¿Hay constraints que protejan integridad (FK, UNIQUE, CHECK)?
- ¿Tienes plan de rollback para el cambio (migración/índice)?
FAQ
1) ¿PostgreSQL o MySQL para aprender SQL “bien”?
Los dos sirven. Postgres suele darte mejores herramientas de análisis y features (CTE, tipos, extensiones). MySQL domina en un buen de stacks. Aprende SQL estándar y luego afina por motor.
2) ¿Cuándo conviene denormalizar?
Cuando ya mediste que el join/cálculo es el cuello de botella, el dato cambia poco y el costo de mantener consistencia es manejable. Denormalizar por ansiedad es receta para bugs.
3) ¿Qué tan seguido debo agregar índices?
Cuando una query frecuente lo pide y lo confirmaste con EXPLAIN + métricas. Cada índice nuevo es deuda de mantenimiento, sobre todo en tablas de alta escritura.
4) ¿Qué hago si no puedo tocar la DB porque “es del cliente” o “es legacy”?
Empieza por lo que sí controlas: limitar columnas, paginar mejor, cachear, reducir N+1 y pedir una ventana para el índice mínimo viable. Lleva evidencia: métricas + EXPLAIN.
5) ¿Cómo evito que una migración me tumbe production?
Haz cambios incrementales: crea índices sin bloquear cuando aplique, migra en batches, usa feature flags y ten rollback. Si puedes, prueba el plan en un clon con datos reales.
Siguiente episodio
Te cuento cómo se ven los jobs y colas cuando el backend crece: retries, idempotencia y por qué el “nomás reintenta” te puede duplicar cobros. Dos o tres patrones que te salvan cuando ya no cabe todo en una sola request.
Idea para cerrar bien este post: toma una sola práctica de aquí y conviértela en algo que tu equipo pueda aplicar hoy.
Cuando un artículo aterriza en decisiones reales, deja de ser contenido y se vuelve ventaja.


