Lección 3 de 514 min
SQL: hablar con las bases de datos
Consultar, filtrar, unir y agrupar datos, modificarlos con seguridad y definir tablas con sus restricciones.
Tu tienda online guarda clientes, productos y pedidos en una base de datos. Un día te preguntan: «¿cuánto ha gastado cada cliente este año, y quiénes no han comprado nunca?». No hace falta escribir un bucle que recorra miles de filas: basta con una consulta de pocas líneas en SQL (Structured Query Language).
SQL es el lenguaje de las bases de datos relacionales como PostgreSQL, MySQL, SQLite o SQL Server. Es declarativo: describes el resultado que quieres y la base de datos decide cómo conseguirlo. Cada sistema tiene su dialecto, con pequeñas diferencias, pero lo que ves en esta lección funciona casi igual en todos. En los módulos de bases de datos y de ORM y acceso a datos irás más a fondo: índices, modelado y cómo usar SQL desde tu código.
Leer datos
Una base de datos relacional guarda la información en tablas: cada fila es un registro (un cliente) y cada columna, uno de sus datos (el nombre, el email). En los ejemplos usaremos dos tablas: clientes y pedidos, donde cada pedido guarda en cliente_id el id del cliente que lo hizo.
SELECT, WHERE, ORDER BY y LIMIT
Pedir filas a una tabla: qué columnas, cuáles filas, en qué orden y cuántas.
- Tipo:
- Declarativo
- Estándar:
- ISO/IEC 9075
Una consulta SELECT dice qué datos quieres, no cómo buscarlos. Eliges las columnas tras SELECT, la tabla tras FROM y filtras las filas con WHERE, combinando condiciones con AND y OR, listas con IN, rangos con BETWEEN o patrones de texto con LIKE.
ORDER BY ordena el resultado (ASC, de menor a mayor, es el valor por defecto; DESC, al revés) y LIMIT se queda con las primeras filas. Sin ORDER BY, la base de datos devuelve las filas en el orden que le resulte cómodo, y ese orden puede cambiar de un día para otro.
Úsalo cuando
Siempre que necesites leer datos: es, con diferencia, la sentencia que más vas a escribir.
Ten en cuenta
NULL significa «valor desconocido» y no se compara con =: WHERE email = NULL nunca devuelve nada, así que usa IS NULL. Además, LIMIT no existe en todos los dialectos: SQL Server usa TOP y el estándar define FETCH FIRST.
-- Los 5 clientes de Valencia dados de alta más recientemente
SELECT nombre, email
FROM clientes
WHERE ciudad = 'Valencia'
AND email IS NOT NULL
ORDER BY fecha_alta DESC
LIMIT 5;JOIN: INNER y LEFT
Combinar filas de dos tablas a través de una columna que las relaciona.
- Otros tipos:
- RIGHT, FULL, CROSS
La información se reparte en tablas para no repetirla: los datos del cliente se guardan una sola vez, aunque tenga cien pedidos. JOIN vuelve a juntarlas, emparejando filas según la condición que escribes tras ON, normalmente pedidos.cliente_id = clientes.id.
INNER JOIN (o simplemente JOIN) devuelve solo las parejas que encajan: un cliente sin pedidos desaparece del resultado. LEFT JOIN conserva todas las filas de la tabla de la izquierda y, cuando no encuentra pareja, rellena las columnas de la derecha con NULL.
Úsalo cuando
INNER JOIN cuando solo te interesan los datos que existen en ambas tablas; LEFT JOIN cuando necesitas ver también lo que falta, como los clientes que nunca han comprado.
Ten en cuenta
Con una condición de ON equivocada, cada fila se combina con muchas otras y el resultado se multiplica. Y en un LEFT JOIN, filtrar la tabla de la derecha en el WHERE elimina las filas con NULL y lo convierte, en la práctica, en un INNER JOIN: pon ese filtro en el ON.
-- Todos los clientes, con o sin pedidos
SELECT c.nombre, p.id AS pedido, p.total
FROM clientes AS c
LEFT JOIN pedidos AS p ON p.cliente_id = c.id
ORDER BY c.nombre;
-- nombre | pedido | total
-- Ana | 101 | 35.00
-- Ana | 107 | 12.50
-- Luis | NULL | NULL <- sin pedidos: con INNER JOIN no saldríaGROUP BY, agregados y HAVING
Resumir muchas filas en una por grupo: contar, sumar, promediar.
Las funciones de agregado reducen un conjunto de filas a un solo valor: COUNT, SUM, AVG, MIN y MAX. Solas resumen toda la tabla; con GROUP BY calculan un resultado por cada grupo, por ejemplo, el total gastado por cada cliente. Ojo: COUNT(*) cuenta filas, pero COUNT(email) solo cuenta las que no tienen NULL en esa columna.
WHERE filtra filas antes de agrupar y HAVING filtra grupos después. Por eso solo HAVING puede usar el resultado de un agregado: «clientes que han gastado más de 100» se escribe con HAVING SUM(total) > 100.
Úsalo cuando
Para informes y estadísticas: ventas por mes, usuarios por país, el producto más vendido.
Ten en cuenta
Cada columna del SELECT debe estar en el GROUP BY o dentro de un agregado: si no, ¿qué valor del grupo debería mostrar? PostgreSQL lo rechaza con un error, y algunos sistemas, como SQLite, eligen un valor cualquiera sin avisar.
-- Clientes que han gastado más de 100 en 2026
SELECT cliente_id,
COUNT(*) AS pedidos,
SUM(total) AS gastado
FROM pedidos
WHERE fecha >= '2026-01-01' -- filtra filas
GROUP BY cliente_id
HAVING SUM(total) > 100 -- filtra grupos
ORDER BY gastado DESC;Se escribe en un orden y se evalúa en otro
Lógicamente, la base de datos procesa primero FROM y los JOIN, después WHERE, GROUP BY, HAVING, SELECT y, al final, ORDER BY y LIMIT. Por eso no puedes usar en el WHERE un alias que defines en el SELECT, pero sí en el ORDER BY.
Cambiar datos y definir tablas
Leer es la mitad del trabajo. La otra mitad es escribir: añadir filas, cambiarlas, borrarlas y, antes de todo eso, crear las tablas con reglas que protejan los datos.
INSERT, UPDATE y DELETE
Añadir, modificar y borrar filas. Sin WHERE, todas a la vez.
- Familia:
- DML (manipulación de datos)
INSERT INTO añade filas indicando las columnas y sus valores. UPDATE cambia columnas de las filas que cumplen el WHERE, y DELETE FROM borra las filas que lo cumplen. Las tres informan de cuántas filas han afectado, un dato que conviene comprobar.
El peligro está en que el WHERE es opcional. UPDATE productos SET precio = 0; pone a cero todos los precios de la tienda, y DELETE FROM pedidos; vacía la tabla entera. La base de datos no pregunta «¿estás seguro?»: lo hace al instante.
Úsalo cuando
Para cualquier cambio en los datos. En una aplicación los lanzará tu código, a menudo a través de un ORM o de un query builder, que verás en el módulo de acceso a datos.
Ten en cuenta
Antes de un UPDATE o un DELETE a mano, ejecuta un SELECT con el mismo WHERE para ver qué filas vas a tocar, y hazlo dentro de una transacción para poder deshacerlo.
INSERT INTO clientes (nombre, email, ciudad)
VALUES ('Ana', 'ana@ejemplo.com', 'Valencia');
-- Sube un 10 % los productos de una categoría
UPDATE productos
SET precio = precio * 1.10
WHERE categoria = 'libros';
-- Borra los carritos abandonados antes de octubre
DELETE FROM carritos
WHERE actualizado < '2026-10-01';CREATE TABLE y restricciones
Definir la forma de los datos y las reglas que la base de datos hará cumplir.
- Familia:
- DDL (definición de datos)
Las sentencias de DDL (Data Definition Language) crean y cambian la estructura: CREATE TABLE crea una tabla, ALTER TABLE añade o modifica columnas y DROP TABLE borra la tabla entera. Cada columna tiene un tipo, como INTEGER, VARCHAR, NUMERIC o DATE, con nombres que varían un poco entre sistemas.
Las restricciones son reglas que la base de datos comprueba en cada escritura. PRIMARY KEY identifica cada fila de forma única; NOT NULL obliga a rellenar un campo; UNIQUE impide valores repetidos, como dos cuentas con el mismo email; y FOREIGN KEY garantiza que un cliente_id apunte a un cliente que existe. Con CHECK añades reglas propias, como un total que no sea negativo.
Úsalo cuando
Al diseñar las tablas y en cada cambio de estructura, que en un proyecto real se guarda como una migración versionada con Git, como verás en el módulo de acceso a datos.
Ten en cuenta
Pon las reglas en la base de datos, no solo en tu código: una restricción protege los datos aunque escriba otro programa, un script o una persona desde la consola. Y cuidado con DROP TABLE: se lleva la tabla con todos sus datos.
CREATE TABLE clientes (
id INTEGER PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE
);
CREATE TABLE pedidos (
id INTEGER PRIMARY KEY,
cliente_id INTEGER NOT NULL,
total NUMERIC(10, 2) NOT NULL CHECK (total >= 0),
FOREIGN KEY (cliente_id) REFERENCES clientes (id)
);Pruébalo
Puedes practicar sin instalar un servidor: SQLite guarda la base de datos entera en un solo archivo. Crea las tablas clientes y pedidos, inserta unas filas (un cliente sin pedidos incluido) y compara el resultado de INNER JOIN y LEFT JOIN.
Trabajar con seguridad
Dos hábitos separan el SQL de prueba del SQL de producción: agrupar los cambios que deben ocurrir juntos y no mezclar nunca los datos del usuario con el texto de la consulta.
Transacciones
Varios cambios que se aplican todos juntos o no se aplica ninguno.
- Garantías:
- ACID
Una transferencia bancaria son dos UPDATE: restar de una cuenta y sumar a otra. Si el servidor se cae entre los dos, el dinero desaparece. Una transacción los agrupa: empiezas con BEGIN, confirmas con COMMIT y, si algo falla, deshaces todo con ROLLBACK.
Las bases de datos relacionales garantizan las propiedades ACID: atomicidad (todo o nada), consistencia (se siguen cumpliendo las restricciones), aislamiento (las transacciones simultáneas no se pisan) y durabilidad (lo confirmado sobrevive a un apagón).
Úsalo cuando
Siempre que una operación toque varias filas o tablas que deben quedar coherentes: un pedido con sus líneas, un pago con su saldo, un alta de usuario con su perfil.
Ten en cuenta
Mantén las transacciones cortas: mientras están abiertas pueden bloquear filas que otras peticiones necesitan. No dejes una abierta mientras esperas la respuesta de una API externa.
BEGIN;
UPDATE cuentas SET saldo = saldo - 50 WHERE id = 1;
UPDATE cuentas SET saldo = saldo + 50 WHERE id = 2;
-- Si todo ha ido bien:
COMMIT;
-- Si algo ha fallado, en su lugar:
-- ROLLBACK;Consultas parametrizadas e inyección SQL
Enviar los datos aparte de la consulta para que nunca se ejecuten como código.
- Marcadores:
- $1, ?, :nombre
La inyección SQL ocurre cuando construyes una consulta pegando texto que escribe el usuario. Si alguien pone ' OR '1'='1 en el campo de email, la condición se vuelve siempre verdadera y la consulta devuelve todos los usuarios; con otras entradas podría leer contraseñas o borrar tablas.
La solución son las consultas parametrizadas: escribes la consulta con marcadores ($1 en PostgreSQL, ? en MySQL o SQLite) y envías los valores por separado. La base de datos sabe qué parte es código y qué parte son datos, así que una comilla en el email es solo un carácter más. Los ORM y los query builders lo hacen por ti.
Úsalo cuando
Siempre que un valor venga de fuera: formularios, URLs, cabeceras, archivos subidos o respuestas de otras APIs. No hay excepciones.
Ten en cuenta
Los marcadores sirven para valores, no para nombres de tablas o columnas: si el usuario elige por qué columna ordenar, compara su elección con una lista de nombres permitidos. Escapar comillas a mano no es una alternativa segura.
// MAL: el texto del usuario pasa a formar parte del SQL
const sql = `SELECT * FROM usuarios WHERE email = '${email}'`;
// Con email = "' OR '1'='1", la consulta queda así:
// SELECT * FROM usuarios WHERE email = '' OR '1'='1'
// BIEN: la consulta y los datos viajan por separado
const { rows } = await db.query(
"SELECT * FROM usuarios WHERE email = $1",
[email],
);Atención
Lo verás de nuevo en el módulo de seguridad web: la inyección lleva muchos años entre las vulnerabilidades más frecuentes de la web, y casi siempre se evita con una sola regla: nunca construyas SQL concatenando texto.
Interactivo
Las cuatro sentencias de datos
Qué hace cada una y qué pasa si se te olvida el WHERE.
| Característica | SELECT | INSERT | UPDATE |
|---|---|---|---|
| Qué hace | Lee filas | Añade filas nuevas | Cambia columnas de filas existentes |
| Sin WHERE | Lee toda la tabla | No lleva WHERE | Cambia todas las filas |
| Devuelve | Las filas que cumplen la condición | Cuántas filas ha añadido | Cuántas filas ha cambiado |
| Error típico | Confiar en el orden sin poner ORDER BY | Chocar con una restricción NOT NULL o UNIQUE | Un WHERE olvidado o demasiado amplio |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Marketing quiere ver todos los clientes junto a sus pedidos, incluidos los que no han comprado nunca, para enviarles un cupón de bienvenida.
Para recordar
- SQL es declarativo: describes qué datos quieres y la base de datos decide cómo obtenerlos.
INNER JOINse queda con las coincidencias;LEFT JOINconserva todas las filas de la izquierda y rellena conNULLlo que falta.WHEREfiltra filas antes de agrupar yHAVINGfiltra grupos después.- Un
UPDATEoDELETEsinWHEREafecta a toda la tabla: compruébalo antes con unSELECTy trabaja dentro de una transacción. - Nunca construyas SQL concatenando texto del usuario: usa consultas parametrizadas.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.Tienes 3 clientes y uno de ellos no ha hecho ningún pedido. ¿Cuántos clientes distintos aparecen al combinar
clientesconpedidosmedianteINNER JOINy medianteLEFT JOIN?2.¿Qué hace
UPDATE usuarios SET activo = false;?3.¿Por qué falla
SELECT ciudad, COUNT(*) FROM clientes WHERE COUNT(*) > 10 GROUP BY ciudad?4.Tu formulario de inicio de sesión construye la consulta concatenando el email que escribe el usuario. ¿Qué arreglo es el correcto?
5.En una transferencia, el primer
UPDATEresta el dinero y el segundo falla. ¿Qué consigues si ambos iban dentro de una transacción?