SQL en 10 Minutos

SQL es la habilidad más duradera en tecnología — ha sobrevivido a cada framework de los últimos 50 años y sobrevivirá al siguiente lote. También se aprende en una tarde, porque solo le dices a la base de datos qué quieres, nunca cómo conseguirlo. Esta página: el modelo mental, la sección de JOIN que por fin los hace clic, y los errores que todos cometen exactamente una vez.

🎙️ Publicado y grabado:

01El modelo: hojas de cálculo con las que hablas

Una base de datos es un conjunto de tablas — hojas de cálculo con reglas. Cada fila es una cosa, cada columna es un dato sobre ella, y las tablas se refieren entre sí por ID. La gran idea de SQL: tú declaras lo que quieres, y la base de datos descubre cómo. Nunca escribes un bucle.

-- users                          -- orders
-- id | name  | country           -- id | user_id | amount
-- 1  | Ada   | UK                -- 1  | 1       | 19.99
-- 2  | Lin   | SG                -- 2  | 1       | 5.00
-- 3  | Sam   | UK                -- 3  | 2       | 42.50

-- "orders.user_id apunta a users.id" — ese enlace es el
-- secreto entero de las bases de datos relacionales. guarda eso.

02SELECT: pide exactamente lo que quieres

Cuatro cláusulas cubren la mayoría de queries que escribirás: qué columnas, qué tabla, qué filas, qué orden. Un hábito que vale la pena formar desde el día uno: nombra tus columnas en vez de SELECT * — las queries con asterisco se rompen silenciosamente cuando las tablas cambian, y arrastran megabytes que no necesitabas.

SELECT name, country        -- qué datos
FROM users                  -- qué tabla
WHERE country = 'UK'        -- qué filas
ORDER BY name               -- qué orden
LIMIT 10;                   -- cuántas (¡SIEMPRE LIMIT al explorar!)
Hábito del día uno
¿Explorando una tabla que no conoces? SELECT * FROM t LIMIT 10 está bien — para eso existe el asterisco. Es en código de aplicación y reportes donde * se convierte en bomba de tiempo. Y pon LIMIT en cada query exploratoria: la diferencia entre "ups" y "ups, 40 millones de filas imprimiéndose".

03WHERE, y la trampa de NULL

WHERE filtra filas con operadores familiares — más LIKE para patrones e IN para listas. Y luego está NULL, el marcador de valor ausente que rompe la lógica normal: NULL no es igual a nada, ni siquiera a sí mismo. Todo programador SQL pierde una hora con esto exactamente una vez. La tuya queda reembolsada aquí.

WHERE amount > 20
WHERE country IN ('UK', 'SG', 'DE')
WHERE name LIKE 'A%'          -- empieza con A (% = cualquier cosa)

-- la trampa:
WHERE email = NULL            -- ⚠ no devuelve NADA. siempre. en silencio.
WHERE email IS NULL           -- ✓ la única forma de preguntar
WHERE email IS NOT NULL       -- ✓ y su opuesto
Por qué es silencioso y mortal
= NULL no es un error de sintaxis — se ejecuta bien y devuelve cero filas, porque en la lógica SQL NULL = NULL no es ni verdadero ni falso, es desconocido. Ningún mensaje de error te señalará aquí. Cuando una query misteriosamente no devuelve nada, busca un = NULL antes de buscar cualquier otra cosa.

04JOINs: la sección que los hace clic

Olvida los diagramas de Venn — aquí está el modelo que funciona. Un JOIN construye una tabla más ancha emparejando filas: para cada fila de la izquierda, encuentra filas de la derecha donde la condición ON sea verdadera, y pégalas lado a lado. La única pregunta real es: ¿qué pasa con las filas de la izquierda sin pareja? INNER las descarta. LEFT las conserva (con NULLs rellenando el lado derecho).

-- "cada pedido, con el nombre del cliente adjunto"
SELECT orders.id, users.name, orders.amount
FROM orders
JOIN users ON orders.user_id = users.id;

-- "cada USUARIO y sus pedidos — incluyendo usuarios
--  que nunca compraron nada" → LEFT JOIN
SELECT users.name, orders.amount
FROM users
LEFT JOIN orders ON orders.user_id = users.id;
-- Sam no compró nada → una fila: Sam | NULL
El bug clásico de JOIN
"Encontrar usuarios sin pedidos" — la gente escribe un LEFT JOIN y luego WHERE orders.amount > 0 y se pregunta dónde fueron las filas NULL. Filtrar la tabla derecha en WHERE silenciosamente convierte tu LEFT JOIN de vuelta en un INNER. La solución: filtra por la ausencia de match — WHERE orders.id IS NULL.

05GROUP BY: colapsar y contar

GROUP BY colapsa filas en grupos — una fila de salida por grupo — y las funciones de agregación (COUNT, SUM, AVG, MAX) resumen cada grupo. Esta es la cláusula que convierte una tabla de eventos crudos en un reporte real.

-- "ingresos y número de pedidos por usuario"
SELECT user_id,
       COUNT(*)     AS orders,
       SUM(amount)  AS revenue
FROM orders
GROUP BY user_id;

-- user_id | orders | revenue
-- 1       | 2      | 24.99
-- 2       | 1      | 42.50
Te va a pasar esto
ERROR: column "users.name" must appear in the GROUP BY clause or be used in an aggregate function
El error de SQL más pegado en Stack Overflow. Una vez que las filas colapsan en grupos, cada columna que haces SELECT debe ser la clave del grupo o un resumen del grupo. Un grupo de 50 filas tiene 50 nombres — la base de datos se niega a elegir uno por ti. Solución: añade la columna al GROUP BY, o envuélvela en un aggregate.

06WHERE vs HAVING: antes vs después

Ambos filtran — la diferencia es cuándo. WHERE filtra filas antes de agruparse; HAVING filtra grupos después de la agregación. La pista: si tu condición contiene COUNT() o SUM(), no puede ir en WHERE — esos números no existen todavía cuando WHERE se ejecuta.

-- "clientes que gastan mucho: solo contar pedidos de 2026,
--  y solo mostrar clientes que gastaron 100+"
SELECT user_id, SUM(amount) AS total
FROM orders
WHERE created_at >= '2026-01-01'   -- filtrar FILAS primero
GROUP BY user_id
HAVING SUM(amount) >= 100;         -- luego filtrar GRUPOS

Vale la pena interiorizar: la base de datos ejecuta las cláusulas en este orden — FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. Por eso tampoco puedes usar un alias de SELECT dentro de WHERE: el alias todavía no ha nacido.

07INSERT, UPDATE, DELETE — y la historia de terror

Escribir datos son tres verbos. Dos de ellos — UPDATE y DELETE — comparten el disparo al pie más infame en bases de datos: sin cláusula WHERE, afectan a todas las filas de la tabla. Sin confirmación. Sin "¿estás seguro?". Todo DBA tiene una historia; los buenos la convirtieron en un hábito en vez de un recuerdo.

INSERT INTO users (name, country) VALUES ('Kai', 'JP');

UPDATE users SET country = 'DE' WHERE id = 2;   -- una fila ✓
UPDATE users SET country = 'DE';                -- ⚠ TODAS las filas. se fueron.

-- el hábito profesional: envolver escrituras arriesgadas en una transacción
BEGIN;
DELETE FROM orders WHERE created_at < '2020-01-01';
-- verificar: SELECT COUNT(*) FROM orders;  ¿contento?
COMMIT;      -- o ROLLBACK; para deshacer todo
El ritual que salva carreras
Antes de cualquier UPDATE/DELETE serio: ejecútalo como un SELECT primero con el mismo WHERE, verifica el conteo de filas, y luego cambia el verbo. Treinta segundos de ritual contra un evento que actualiza tu currículum. Y en producción, hazlo dentro de BEGIN … COMMIT para tener botón de deshacer.

08Índices: por qué las queries son lentas

Sin un índice, WHERE email = '…' lee cada fila de la tabla — un full scan. Un índice es una guía telefónica ordenada para una columna: la base de datos salta directo a la entrada. Cuando una query es misteriosamente lenta, la respuesta es un índice el 90% de las veces.

CREATE INDEX idx_users_email ON users (email);

-- antes: WHERE email = '[email protected]'  → escanea 10,000,000 filas
-- después:  misma query              → ~3 búsquedas

-- ver lo que la base de datos REALMENTE hace:
EXPLAIN SELECT * FROM users WHERE email = '[email protected]';
-- "Seq Scan" = lectura completa, añade un índice. "Index Scan" = ✓
¿Por qué no indexar todo?
Cada índice debe actualizarse en cada INSERT/UPDATE — estás comprando velocidad de lectura con velocidad de escritura y disco. Indexa las columnas por las que realmente filtras y haces join (user_id, email, created_at), y deja que EXPLAIN te diga cuándo te equivocas.

09Chuleta

Las queries que realmente reutilizarás, organizadas por intención.

-- explorar una tabla desconocida
SELECT * FROM t LIMIT 10;

-- top 10 por ingresos
SELECT user_id, SUM(amount) AS total
FROM orders GROUP BY user_id
ORDER BY total DESC LIMIT 10;

-- filas en A sin pareja en B
SELECT a.* FROM a LEFT JOIN b ON b.a_id = a.id
WHERE b.id IS NULL;

-- encontrar duplicados
SELECT email, COUNT(*) FROM users
GROUP BY email HAVING COUNT(*) > 1;

-- escritura destructiva segura
BEGIN;  -- ...UPDATE/DELETE con WHERE...  COMMIT; -- o ROLLBACK;

-- ¿por qué es lento?
EXPLAIN <tu query>;   -- "Seq Scan" en tabla grande = índice faltante

Eso es SQL funcional. Los dialectos (Postgres, MySQL, SQLite) difieren en las esquinas, pero todo en esta página funciona en todos lados. Siguiente en el stack: Git si no lo has hecho, y la línea de comandos, donde ambos viven.