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 EXPLAIN sin 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, CONCURRENTLY reduce 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 WHERE filtra 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_snapshot en orders), 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).

SQL: el hechizo que nunca muere (y por qué tu backend lo necesita bien aprendido) - visual explicativa 1
Visual de apoyo: Intro con gancho

Screenshots sugeridos

  • Captura de EXPLAIN (ANALYZE, BUFFERS) mostrando un Seq Scan y luego el mismo query usando Index Scan despué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';
SQL: el hechizo que nunca muere (y por qué tu backend lo necesita bien aprendido) - visual explicativa 2
Visual de apoyo: Qué vas a aprender

Checklist final

  • ¿Tu query trae solo las columnas necesarias?
  • ¿Tienes índice alineado con WHERE + ORDER BY?
  • ¿Validaste con EXPLAIN (ANALYZE, BUFFERS)?
  • ¿Evitaste OFFSET si 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.