Saltar al contenido
Bases de datos con SQL
Todos los cursos Ir al ejercicio →

Proyecto: base de datos de una tienda

  • ⏱ 90 minutos
  • 📶 Nivel: principiante
  • 🧩 Incluye ejercicio

💡 🎯 Al terminar esta lección podrás…

  • Diseñar un modelo de datos con varias tablas relacionadas.
  • Crear la base con un script reproducible y operarla con transacciones.
  • Escribir informes con JOIN, GROUP BY y HAVING.
  • Aplicar medidas básicas de seguridad y copias.

1.El encargo

Una pequeña tienda de tecnología de tu ciudad lleva sus ventas en un cuaderno y quiere pasarlas a una base de datos. Te pide poder:

Es justo lo que has aprendido en las ocho lecciones. Aquí lo reúnes en un único proyecto, que funciona en SQLite (y con pocos cambios en PostgreSQL).

1. Diseñar2. Crear3. Operar4. Informar5. Proteger

2.Paso 1: diseñar el modelo

Antes de escribir SQL, decide qué entidades hay y cómo se relacionan. Del encargo salen cuatro:

TablaQué guardaClave y restricciones
productosCatálogo: nombre, categoría, precio, stockid PK; NOT NULL en casi todo; stock DEFAULT 0
clientesNombre y correoid PK; email UNIQUE
pedidosUna compra: quién y cuándocliente_id FK a clientes
lineas_pedidoQué productos y cuántos hay en cada pedidoPK compuesta (pedido_id, producto_id); dos FK; CHECK (cantidad > 0)

Las relaciones son: un cliente tiene muchos pedidos (uno a muchos) y un pedido contiene muchos productos y un producto está en muchos pedidos (muchos a muchos, resuelto con lineas_pedido).

clientes 1a muchospedidos1 a muchoslineas_pedidomuchos a 1productos

💡 Consejo · Una decisión de diseño

El precio del producto está en productos, no en la línea del pedido. Pero entonces, si el precio cambia mañana, los informes de ventas pasadas cambiarían. En una tienda real guardarías también el precio vendido en lineas_pedido. Es un buen reto para el ejercicio.

3.Paso 2: crear la base de datos

Guarda todo en un único archivo tienda.sql. El script se puede ejecutar las veces que quieras: las líneas DROP TABLE IF EXISTS (en orden inverso a las dependencias) empiezan siempre desde cero. Este código se ha ejecutado de verdad en SQLite: crea 4 tablas con 8 productos, 4 clientes, 4 pedidos y 7 líneas.

tienda.sql
PRAGMA foreign_keys = ON;

DROP TABLE IF EXISTS lineas_pedido;
DROP TABLE IF EXISTS pedidos;
DROP TABLE IF EXISTS clientes;
DROP TABLE IF EXISTS productos;

CREATE TABLE productos (
  id        INTEGER PRIMARY KEY,
  nombre    TEXT    NOT NULL,
  categoria TEXT    NOT NULL,
  precio    NUMERIC NOT NULL,
  stock     INTEGER NOT NULL DEFAULT 0
);

CREATE TABLE clientes (
  id     INTEGER PRIMARY KEY,
  nombre TEXT NOT NULL,
  email  TEXT NOT NULL UNIQUE
);

CREATE TABLE pedidos (
  id         INTEGER PRIMARY KEY,
  cliente_id INTEGER NOT NULL REFERENCES clientes(id),
  fecha      TEXT    NOT NULL
);

CREATE TABLE lineas_pedido (
  pedido_id   INTEGER NOT NULL REFERENCES pedidos(id),
  producto_id INTEGER NOT NULL REFERENCES productos(id),
  cantidad    INTEGER NOT NULL CHECK (cantidad > 0),
  PRIMARY KEY (pedido_id, producto_id)
);

INSERT INTO productos (id, nombre, categoria, precio, stock) VALUES
  (1, 'Teclado',     'Periféricos', 120.00,  15),
  (2, 'Ratón',       'Periféricos',  45.50,  40),
  (3, 'Monitor FHD', 'Pantallas',   650.00,   8),
  (4, 'Auriculares', 'Audio',        89.90,   0),
  (5, 'Webcam',      'Periféricos', 150.00,  12),
  (6, 'Altavoz',     'Audio',        99.00,  25),
  (7, 'Monitor 4K',  'Pantallas',   899.00,   3),
  (8, 'Cable HDMI',  'Accesorios',   15.00, 100);

INSERT INTO clientes (id, nombre, email) VALUES
  (1, 'Ana Torres',  'ana@correo.com'),
  (2, 'Luis Pérez',  'luis@correo.com'),
  (3, 'Marta Ruiz',  'marta@correo.com'),
  (4, 'Diego Soto',  'diego@correo.com');

INSERT INTO pedidos (id, cliente_id, fecha) VALUES
  (1, 1, '2026-09-01'),
  (2, 1, '2026-09-15'),
  (3, 2, '2026-09-20'),
  (4, 3, '2026-10-02');

INSERT INTO lineas_pedido (pedido_id, producto_id, cantidad) VALUES
  (1, 1, 1), (1, 2, 1),
  (2, 5, 1),
  (3, 7, 1), (3, 8, 2),
  (4, 6, 2), (4, 2, 3);
Cargarlo
sqlite3 tienda.db ".read tienda.sql"

Comprueba que todo entró bien:

Resultado esperado: conteo de filas

productosclientespedidoslineas
8447

4.Paso 3: registrar un pedido con una transacción

Registrar un pedido toca varias tablas: crear el pedido, añadir sus líneas y descontar el stock. Si algo falla a medias, la tienda quedaría con datos incoherentes. Por eso se hace dentro de una transacción (lección 3): todo se guarda, o no se guarda nada.

Diego compra 1 Monitor FHD y 2 Ratones
BEGIN;

INSERT INTO pedidos (id, cliente_id, fecha)
VALUES (5, 4, '2026-10-06');

INSERT INTO lineas_pedido (pedido_id, producto_id, cantidad)
VALUES (5, 3, 1), (5, 2, 2);

UPDATE productos SET stock = stock - 1 WHERE id = 3;
UPDATE productos SET stock = stock - 2 WHERE id = 2;

COMMIT;

Si cualquiera de las instrucciones falla, ejecutas ROLLBACK; en lugar de COMMIT; y la base queda como estaba. Después de la transacción:

Stock actualizado (antes: Ratón 40, Monitor FHD 8)

idnombrestock
2Ratón38
3Monitor FHD7

El pedido nuevo

pedidonombretotal
5Diego Soto741.00

📌 Nota · PostgreSQL

Allí lo normal es no escribir el id: lo genera la base (columna identity) y lo recuperas con INSERT … RETURNING id para usarlo en las líneas. En SQLite también puedes usar last_insert_rowid().

5.Paso 4: los informes

Los informes son consultas que combinan todo lo aprendido: JOIN, GROUP BY, HAVING y LEFT JOIN … IS NULL. Los resultados que ves son los de la base original (sin el pedido nuevo del paso anterior).

Pedidos con cliente e importe

Pedidos con cliente e importe
SELECT p.id AS pedido, c.nombre, p.fecha,
       SUM(l.cantidad * pr.precio) AS total
FROM pedidos p
JOIN clientes c       ON c.id        = p.cliente_id
JOIN lineas_pedido l  ON l.pedido_id = p.id
JOIN productos pr     ON pr.id       = l.producto_id
GROUP BY p.id
ORDER BY p.id;

Resultado esperado

pedidonombrefechatotal
1Ana Torres2026-09-01165.50
2Ana Torres2026-09-15150.00
3Luis Pérez2026-09-20929.00
4Marta Ruiz2026-10-02334.50

Gasto por cliente (incluye a quien no compró)

Gasto por cliente (incluye a quien no compró)
SELECT c.nombre,
       COUNT(DISTINCT p.id) AS pedidos,
       COALESCE(SUM(l.cantidad * pr.precio), 0) AS gasto
FROM clientes c
LEFT JOIN pedidos p       ON p.cliente_id = c.id
LEFT JOIN lineas_pedido l ON l.pedido_id  = p.id
LEFT JOIN productos pr    ON pr.id        = l.producto_id
GROUP BY c.id
ORDER BY gasto DESC;

Resultado esperado

nombrepedidosgasto
Luis Pérez1929.00
Marta Ruiz1334.50
Ana Torres2315.50
Diego Soto00.00

Productos más vendidos

Productos más vendidos
SELECT pr.nombre AS producto,
       SUM(l.cantidad) AS unidades,
       SUM(l.cantidad * pr.precio) AS ingresos
FROM lineas_pedido l
JOIN productos pr ON pr.id = l.producto_id
GROUP BY pr.id
ORDER BY unidades DESC, ingresos DESC;

Resultado esperado

productounidadesingresos
Ratón4182.00
Altavoz2198.00
Cable HDMI230.00
Monitor 4K1899.00
Webcam1150.00
Teclado1120.00

Ingresos por categoría

Ingresos por categoría
SELECT pr.categoria,
       SUM(l.cantidad * pr.precio) AS ingresos
FROM lineas_pedido l
JOIN productos pr ON pr.id = l.producto_id
GROUP BY pr.categoria
ORDER BY ingresos DESC;

Resultado esperado

categoriaingresos
Pantallas899.00
Periféricos452.00
Audio198.00
Accesorios30.00

Productos que nunca se han vendido

Productos que nunca se han vendido
SELECT pr.nombre, pr.stock
FROM productos pr
LEFT JOIN lineas_pedido l ON l.producto_id = pr.id
WHERE l.pedido_id IS NULL;

Resultado esperado

nombrestock
Monitor FHD8
Auriculares0

Ingresos por mes

Ingresos por mes
SELECT substr(p.fecha, 1, 7) AS mes,
       COUNT(DISTINCT p.id) AS pedidos,
       SUM(l.cantidad * pr.precio) AS ingresos
FROM pedidos p
JOIN lineas_pedido l ON l.pedido_id = p.id
JOIN productos pr    ON pr.id       = l.producto_id
GROUP BY mes
ORDER BY mes;

Resultado esperado

mespedidosingresos
2026-0931244.50
2026-101334.50

⚠️ Importante · Fechas en PostgreSQL

El informe por mes usa substr(fecha, 1, 7) porque en el proyecto la fecha es texto AAAA-MM-DD. En PostgreSQL con una columna DATE usarías to_char(fecha, 'YYYY-MM') o date_trunc('month', fecha).

6.Paso 5: proteger la base

Un proyecto terminado no es solo el que consulta bien. Antes de darlo por cerrado revisa:

7.Pruébalo: panel de informes

Este panel reúne los informes del proyecto. Elige uno para ver su consulta y su resultado (ejecutado en SQLite real sobre los datos de tienda.sql).

📈 Panel de informes de la tienda

    

8.Errores frecuentes

📒 Apuntes para llevar

  • Un proyecto de base de datos sigue cinco pasos: diseñar, crear, operar, informar y proteger.
  • Diseña primero: entidades, relaciones (uno a muchos, muchos a muchos), claves y restricciones.
  • Guarda el esquema y los datos en un script .sql que se pueda ejecutar de nuevo; es la documentación de tu diseño.
  • Las operaciones que tocan varias tablas van en una transacción: BEGIN; … COMMIT; o ROLLBACK;.
  • Los informes combinan JOIN, GROUP BY, HAVING y LEFT JOIN … IS NULL.
  • Protege: claves foráneas activas, consultas parametrizadas, rol con pocos permisos, hash de contraseñas y copias probadas.
  • Con pocos cambios (identificadores, fechas, ILIKE) el mismo proyecto funciona en PostgreSQL.

9.Glosario

TérminoSignificado
Modelo de datosDescripción de las tablas, sus columnas y las relaciones entre ellas.
EsquemaConjunto de definiciones de las tablas y restricciones de una base de datos.
Script SQLArchivo .sql con instrucciones que se pueden ejecutar para reconstruir la base.
TransacciónGrupo de operaciones que se aplican todas o ninguna.
InformeConsulta que resume datos para tomar decisiones (ventas, stock, clientes…).
Ticket promedioImporte medio de cada pedido.
Datos huérfanosFilas que apuntan a otra fila que ya no existe; las claves foráneas lo evitan.
RespaldoCopia de seguridad de la base que permite restaurarla tras un fallo.

🧠 Comprueba lo aprendido

Elige una respuesta en cada pregunta. Verás al instante si acertaste y por qué.

¿Cuál es el primer paso de un proyecto de base de datos?

En la tienda, ¿cómo se relacionan pedidos y productos?

¿Por qué se registra un pedido dentro de una transacción?

¿Qué informe usa LEFT JOIN … WHERE l.pedido_id IS NULL en el proyecto?

¿Qué debe hacer la aplicación para evitar la inyección SQL?

Tu progreso se guarda en este navegador y se refleja en el índice del curso.

💡 🚀 ¡Ahora te toca!

La práctica fija lo aprendido. Pasa al ejercicio de esta lección.

Ir al ejercicio