Llega el momento de convertir la teoría en algo tangible. En esta lección conocerás a fondo TiendaVerde, la tienda online ficticia de productos ecológicos que será el escenario de todos los ejemplos y ejercicios de los once módulos restantes. Verás su modelo de negocio, su diagrama entidad-relación completo, la descripción de sus nueve tablas columna a columna y —lo más importante— el script SQL listo para copiar y ejecutar que crea las tablas y carga los datos. Al terminar tendrás la base de datos funcionando en tu equipo y sabrás qué hay dentro, de modo que a partir del módulo 2 podrás concentrarte en aprender SQL en lugar de en entender el contexto.
Esta lección no es un tutorial de DDL: verás el script y entenderás qué hace cada bloque, pero la sintaxis completa de CREATE TABLE, los tipos de restricción y las migraciones se estudian en el módulo 5.
Contenido
- El negocio: qué es TiendaVerde y cómo opera
- Diagrama entidad-relación
- Las nueve tablas, una a una
- Decisiones de diseño que conviene conocer
- El script de creación de tablas
- El script de carga de datos
- Cómo cargar la base de datos
- Consultas de verificación
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- El negocio: qué es TiendaVerde y cómo opera
TiendaVerde es una tienda online de productos ecológicos con sede en Valencia, fundada a finales de 2024 y operativa desde principios de 2025.
Qué vende. Un catálogo de unas veinte referencias repartidas en seis categorías: alimentación ecológica, cosmética natural, hogar sostenible, bebidas, higiene personal y complementos alimenticios. Compra a cinco proveedores de España, Portugal, Francia y Alemania.
A quién vende. Particulares de España, Portugal y Francia. Muchos clientes llegan por recomendación de otro cliente, algo que la empresa registra para su programa de referidos.
Cómo opera.
- Dos canales de venta. Los pedidos que entran por la web no tienen comercial asignado; los que entran por teléfono los gestiona un comercial del equipo. Esta distinción es la razón de que
pedidos.empleado_idpueda serNULL, y será protagonista de las lecciones deLEFT JOINy de valores nulos. - Un equipo de ocho personas con jerarquía: dirección general, dos responsables de área, comerciales, atención al cliente, almacén y análisis de datos.
- Ciclo de vida del pedido:
pendiente→pagado→enviado→entregado, con la posibilidad decanceladoen cualquier punto. - Cuatro métodos de pago: tarjeta, transferencia, PayPal y contrareembolso.
- Gastos de envío variables según destino, con envío gratuito a partir de cierto importe.
- Reseñas de clientes con puntuación de 1 a 5 sobre los productos que han comprado.
- Devoluciones asociadas a un pedido, con motivo e importe reembolsado.
Preguntas que la dirección se hace todos los meses (y que sabrás responder al acabar el curso): ¿qué productos se venden más?, ¿qué país deja más margen?, ¿qué clientes no compran desde hace meses?, ¿qué comercial cierra más pedidos?, ¿qué productos acumulan malas reseñas?, ¿cuánto nos cuestan las devoluciones?
- Diagrama entidad-relación
erDiagram
CATEGORIAS ||--o{ PRODUCTOS : "clasifica"
PROVEEDORES ||--o{ PRODUCTOS : "suministra"
CLIENTES ||--o{ PEDIDOS : "realiza"
EMPLEADOS ||--o{ PEDIDOS : "gestiona"
PEDIDOS ||--o{ LINEAS_PEDIDO : "contiene"
PRODUCTOS ||--o{ LINEAS_PEDIDO : "aparece en"
PRODUCTOS ||--o{ RESENAS : "recibe"
CLIENTES ||--o{ RESENAS : "escribe"
PEDIDOS ||--o{ DEVOLUCIONES : "origina"
CLIENTES ||--o{ CLIENTES : "refiere a"
EMPLEADOS ||--o{ EMPLEADOS : "es jefe de"
CATEGORIAS {
int id PK
varchar nombre UK
text descripcion
}
PROVEEDORES {
int id PK
varchar nombre
varchar pais
varchar email
boolean activo
}
PRODUCTOS {
int id PK
varchar nombre
int categoria_id FK
int proveedor_id FK
numeric precio
numeric coste
int stock
boolean activo
date fecha_alta
}
CLIENTES {
int id PK
varchar nombre
varchar apellidos
varchar email UK
varchar ciudad
varchar pais
date fecha_registro
int referido_por_id FK
}
EMPLEADOS {
int id PK
varchar nombre
varchar apellidos
varchar puesto
int jefe_id FK
numeric salario
date fecha_contratacion
varchar ciudad
}
PEDIDOS {
int id PK
int cliente_id FK
int empleado_id FK
date fecha_pedido
varchar estado
varchar metodo_pago
numeric gastos_envio
}
LINEAS_PEDIDO {
int id PK
int pedido_id FK
int producto_id FK
int cantidad
numeric precio_unitario
numeric descuento
}
RESENAS {
int id PK
int producto_id FK
int cliente_id FK
smallint puntuacion
text comentario
date fecha
}
DEVOLUCIONES {
int id PK
int pedido_id FK
varchar motivo
date fecha
numeric importe
}
Reconocerás en él todo lo de la lección anterior: nueve relaciones 1:N, una N:M resuelta con la tabla puente lineas_pedido, y dos relaciones reflexivas (clientes.referido_por_id y empleados.jefe_id).
- Las nueve tablas, una a una
3.1. categorias — clasificación del catálogo
| Columna | Tipo | Nulo | Descripción |
|---|---|---|---|
id |
INTEGER identity |
No | PK. Identificador de la categoría |
nombre |
VARCHAR(60) |
No | Nombre visible. UNIQUE: no puede haber dos categorías iguales |
descripcion |
TEXT |
Sí | Texto descriptivo para la página de categoría |
6 filas. Valores de nombre: Alimentación, Cosmética natural, Hogar sostenible, Bebidas, Higiene personal, Complementos.
3.2. proveedores — quién suministra los productos
| Columna | Tipo | Nulo | Descripción |
|---|---|---|---|
id |
INTEGER identity |
No | PK |
nombre |
VARCHAR(120) |
No | Razón social del proveedor |
pais |
VARCHAR(60) |
No | País de origen: España, Portugal, Francia, Alemania |
email |
VARCHAR(120) |
Sí | Contacto comercial |
activo |
BOOLEAN |
No | FALSE si ya no se le compra. Por defecto TRUE |
5 filas. El proveedor 5 (EcoNordic Supplies) está inactivo pero conserva productos en catálogo: útil para practicar filtros y JOIN.
3.3. productos — el catálogo
| Columna | Tipo | Nulo | Descripción |
|---|---|---|---|
id |
INTEGER identity |
No | PK |
nombre |
VARCHAR(150) |
No | Nombre comercial con formato/gramaje |
categoria_id |
INTEGER |
Sí | FK → categorias.id. ON DELETE RESTRICT |
proveedor_id |
INTEGER |
Sí | FK → proveedores.id. ON DELETE RESTRICT |
precio |
NUMERIC(10,2) |
No | Precio de venta al público, en euros |
coste |
NUMERIC(10,2) |
Sí | Coste de compra. precio - coste es el margen bruto |
stock |
INTEGER |
No | Unidades disponibles. Por defecto 0 |
activo |
BOOLEAN |
No | FALSE si está descatalogado (borrado lógico) |
fecha_alta |
DATE |
No | Fecha de incorporación al catálogo |
20 filas. Precios de 1,95 € a 22,00 €. Puntos que necesitarás en módulos posteriores:
- El producto 13 (Velas de cera de soja) tiene stock 0.
- El producto 20 (Cápsulas de espirulina) tiene
activo = FALSE. - Los productos 13, 19 y 20 no se han vendido nunca: no aparecen en
lineas_pedido. Son imprescindibles para las lecciones deLEFT JOIN(03-03) y de nulos (04-03).
3.4. clientes — quién compra
| Columna | Tipo | Nulo | Descripción |
|---|---|---|---|
id |
INTEGER identity |
No | PK |
nombre |
VARCHAR(60) |
No | Nombre de pila |
apellidos |
VARCHAR(90) |
No | Apellidos |
email |
VARCHAR(120) |
No | UNIQUE. Clave natural del cliente |
ciudad |
VARCHAR(80) |
Sí | Ciudad de residencia |
pais |
VARCHAR(60) |
No | España, Portugal o Francia |
fecha_registro |
DATE |
No | Alta en la tienda |
referido_por_id |
INTEGER |
Sí | FK → clientes.id (reflexiva). NULL = llegó por su cuenta. ON DELETE SET NULL |
15 filas. 11 de España, 2 de Portugal, 2 de Francia. Ocho clientes tienen referido_por_id. Los clientes 13, 14 y 15 no han hecho ningún pedido: son necesarios para las lecciones de LEFT JOIN y NOT EXISTS.
3.5. empleados — el equipo
| Columna | Tipo | Nulo | Descripción |
|---|---|---|---|
id |
INTEGER identity |
No | PK |
nombre |
VARCHAR(60) |
No | Nombre de pila |
apellidos |
VARCHAR(90) |
No | Apellidos |
puesto |
VARCHAR(80) |
No | Cargo |
jefe_id |
INTEGER |
Sí | FK → empleados.id (reflexiva). NULL solo en dirección general. ON DELETE SET NULL |
salario |
NUMERIC(10,2) |
Sí | Salario bruto anual en euros |
fecha_contratacion |
DATE |
No | Fecha de incorporación |
ciudad |
VARCHAR(80) |
Sí | Ciudad de trabajo |
8 filas con esta jerarquía:
Rosa Alcázar Vives (1) — Directora general ├── Andrés Company Talens (2) — Responsable de ventas │ ├── Óscar Peris Blasco (4) — Comercial │ ├── Laia Puig Sanchis (5) — Comercial │ └── Marc Estévez Roig (6) — Atención al cliente ├── Beatriz Nadal Ripoll (3) — Responsable de logística │ └── Irene Salvador Mira (7) — Operaria de almacén └── Daniel Vercher Lluch (8) — Analista de datos
Solo los empleados 4, 5 y 6 tienen pedidos asignados. Los otros cinco no aparecen nunca en pedidos.
3.6. pedidos — la cabecera de cada compra
| Columna | Tipo | Nulo | Descripción |
|---|---|---|---|
id |
INTEGER identity |
No | PK |
cliente_id |
INTEGER |
No | FK → clientes.id. ON DELETE RESTRICT |
empleado_id |
INTEGER |
Sí | FK → empleados.id. NULL = pedido web sin comercial. ON DELETE SET NULL |
fecha_pedido |
DATE |
No | Fecha en que se realizó |
estado |
VARCHAR(20) |
No | pendiente, pagado, enviado, entregado, cancelado |
metodo_pago |
VARCHAR(20) |
No | tarjeta, transferencia, paypal, contrareembolso |
gastos_envio |
NUMERIC(10,2) |
No | Portes cobrados. 0.00 cuando el envío fue gratuito |
20 filas repartidas entre marzo de 2025 y febrero de 2026. Distribución por estado: 14 entregado, 2 enviado, 2 pagado, 1 pendiente, 1 cancelado. Diez pedidos no tienen empleado asignado (empleado_id IS NULL).
Tanto estado como metodo_pago están protegidos con una restricción CHECK que impide valores fuera del dominio.
3.7. lineas_pedido — el detalle de cada compra
| Columna | Tipo | Nulo | Descripción |
|---|---|---|---|
id |
INTEGER identity |
No | PK |
pedido_id |
INTEGER |
No | FK → pedidos.id. ON DELETE CASCADE |
producto_id |
INTEGER |
No | FK → productos.id. ON DELETE RESTRICT |
cantidad |
INTEGER |
No | Unidades. Siempre > 0 |
precio_unitario |
NUMERIC(10,2) |
No | Precio en el momento de la venta, no el actual |
descuento |
NUMERIC(4,2) |
No | Fracción entre 0.00 y 1.00 (0.10 = 10 %). Por defecto 0.00 |
47 filas. Es la tabla puente de la relación N:M entre pedidos y productos, y la tabla más consultada del curso: el importe de una línea se calcula como cantidad * precio_unitario * (1 - descuento).
3.8. resenas — opiniones de clientes
| Columna | Tipo | Nulo | Descripción |
|---|---|---|---|
id |
INTEGER identity |
No | PK |
producto_id |
INTEGER |
No | FK → productos.id. ON DELETE CASCADE |
cliente_id |
INTEGER |
No | FK → clientes.id. ON DELETE CASCADE |
puntuacion |
SMALLINT |
No | De 1 a 5, protegido con CHECK |
comentario |
TEXT |
Sí | Texto libre de la reseña |
fecha |
DATE |
No | Fecha de publicación |
12 filas. Todas corresponden a clientes que compraron ese producto, y su fecha es siempre posterior a la del pedido. La mayoría de los productos no tiene ninguna reseña, lo que hace la tabla ideal para practicar LEFT JOIN y HAVING.
3.9. devoluciones — reembolsos
| Columna | Tipo | Nulo | Descripción |
|---|---|---|---|
id |
INTEGER identity |
No | PK |
pedido_id |
INTEGER |
No | FK → pedidos.id. ON DELETE CASCADE |
motivo |
VARCHAR(200) |
No | Razón de la devolución |
fecha |
DATE |
No | Fecha de tramitación |
importe |
NUMERIC(10,2) |
No | Cantidad reembolsada en euros |
3 filas, asociadas a los pedidos 6, 10 y 13.
- Decisiones de diseño que conviene conocer
Algunas elecciones del esquema no son evidentes y las verás aparecer una y otra vez en los ejercicios:
| Decisión | Motivo |
|---|---|
La tabla se llama resenas, sin ñ |
Los identificadores deben ser portables y ASCII. Los datos sí llevan tildes y ñ; los nombres de objeto, no |
precio_unitario duplica información de productos.precio |
Es desnormalización deliberada: guarda el precio histórico. Dos líneas antiguas (pedidos 1 y 2) tienen un precio menor que el actual, precisamente para que puedas comprobarlo |
descuento es una fracción (0.10), no un porcentaje (10) |
Evita multiplicar y dividir por 100 en cada consulta: el importe es cantidad * precio_unitario * (1 - descuento) |
No hay columna total en pedidos |
Se calcula sumando las líneas. Guardarlo sería un agregado precalculado que habría que mantener sincronizado (módulo 10) |
Todos los importes son NUMERIC(10,2) |
Dinero exacto, nunca coma flotante (lección 01-04) |
Todas las fechas son DATE |
Son fechas de calendario, no instantes con zona horaria |
Todas las PK son id subrogado |
Uniformidad y estabilidad (lección 01-05) |
| Hay huecos deliberados en los datos | Clientes sin pedidos, productos sin vender, pedidos sin empleado, productos sin reseñas: el curso los necesita |
- El script de creación de tablas
Crea un fichero llamado tiendaverde.sql y copia en él los dos bloques de esta sección y la siguiente, en orden.
El primer bloque borra las tablas si ya existen (para que puedas volver a ejecutar el script cuantas veces quieras) y las crea en orden de dependencias: una tabla no puede referenciar a otra que aún no exista.
-- =====================================================================
-- TiendaVerde - Base de datos del curso de SQL
-- PostgreSQL 16
-- Bloque 1: creación del esquema
-- =====================================================================
-- Borrado previo, en orden inverso a las dependencias.
-- CASCADE elimina también las restricciones que apuntan a estas tablas.
DROP TABLE IF EXISTS devoluciones CASCADE;
DROP TABLE IF EXISTS resenas CASCADE;
DROP TABLE IF EXISTS lineas_pedido CASCADE;
DROP TABLE IF EXISTS pedidos CASCADE;
DROP TABLE IF EXISTS empleados CASCADE;
DROP TABLE IF EXISTS clientes CASCADE;
DROP TABLE IF EXISTS productos CASCADE;
DROP TABLE IF EXISTS proveedores CASCADE;
DROP TABLE IF EXISTS categorias CASCADE;
-- ---------------------------------------------------------------------
-- 1. Tablas sin dependencias
-- ---------------------------------------------------------------------
CREATE TABLE categorias (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
nombre VARCHAR(60) NOT NULL UNIQUE,
descripcion TEXT
);
CREATE TABLE proveedores (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
nombre VARCHAR(120) NOT NULL,
pais VARCHAR(60) NOT NULL,
email VARCHAR(120),
activo BOOLEAN NOT NULL DEFAULT TRUE
);
-- ---------------------------------------------------------------------
-- 2. Tablas que dependen de las anteriores
-- ---------------------------------------------------------------------
CREATE TABLE productos (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
nombre VARCHAR(150) NOT NULL,
categoria_id INTEGER REFERENCES categorias(id) ON DELETE RESTRICT,
proveedor_id INTEGER REFERENCES proveedores(id) ON DELETE RESTRICT,
precio NUMERIC(10,2) NOT NULL CHECK (precio >= 0),
coste NUMERIC(10,2) CHECK (coste >= 0),
stock INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0),
activo BOOLEAN NOT NULL DEFAULT TRUE,
fecha_alta DATE NOT NULL DEFAULT CURRENT_DATE
);
-- Relación reflexiva: un cliente puede haber sido referido por otro cliente
CREATE TABLE clientes (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
nombre VARCHAR(60) NOT NULL,
apellidos VARCHAR(90) NOT NULL,
email VARCHAR(120) NOT NULL UNIQUE,
ciudad VARCHAR(80),
pais VARCHAR(60) NOT NULL,
fecha_registro DATE NOT NULL DEFAULT CURRENT_DATE,
referido_por_id INTEGER REFERENCES clientes(id) ON DELETE SET NULL
);
-- Relación reflexiva: jerarquía organizativa
CREATE TABLE empleados (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
nombre VARCHAR(60) NOT NULL,
apellidos VARCHAR(90) NOT NULL,
puesto VARCHAR(80) NOT NULL,
jefe_id INTEGER REFERENCES empleados(id) ON DELETE SET NULL,
salario NUMERIC(10,2) CHECK (salario >= 0),
fecha_contratacion DATE NOT NULL,
ciudad VARCHAR(80)
);
CREATE TABLE pedidos (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
cliente_id INTEGER NOT NULL REFERENCES clientes(id) ON DELETE RESTRICT,
empleado_id INTEGER REFERENCES empleados(id) ON DELETE SET NULL,
fecha_pedido DATE NOT NULL,
estado VARCHAR(20) NOT NULL
CHECK (estado IN ('pendiente','pagado','enviado','entregado','cancelado')),
metodo_pago VARCHAR(20) NOT NULL
CHECK (metodo_pago IN ('tarjeta','transferencia','paypal','contrareembolso')),
gastos_envio NUMERIC(10,2) NOT NULL DEFAULT 0 CHECK (gastos_envio >= 0)
);
-- ---------------------------------------------------------------------
-- 3. Tabla puente de la relación N:M entre pedidos y productos
-- ---------------------------------------------------------------------
CREATE TABLE lineas_pedido (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
pedido_id INTEGER NOT NULL REFERENCES pedidos(id) ON DELETE CASCADE,
producto_id INTEGER NOT NULL REFERENCES productos(id) ON DELETE RESTRICT,
cantidad INTEGER NOT NULL CHECK (cantidad > 0),
precio_unitario NUMERIC(10,2) NOT NULL CHECK (precio_unitario >= 0),
descuento NUMERIC(4,2) NOT NULL DEFAULT 0
CHECK (descuento >= 0 AND descuento <= 1)
);
-- ---------------------------------------------------------------------
-- 4. Tablas satélite
-- ---------------------------------------------------------------------
CREATE TABLE resenas (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
producto_id INTEGER NOT NULL REFERENCES productos(id) ON DELETE CASCADE,
cliente_id INTEGER NOT NULL REFERENCES clientes(id) ON DELETE CASCADE,
puntuacion SMALLINT NOT NULL CHECK (puntuacion BETWEEN 1 AND 5),
comentario TEXT,
fecha DATE NOT NULL
);
CREATE TABLE devoluciones (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
pedido_id INTEGER NOT NULL REFERENCES pedidos(id) ON DELETE CASCADE,
motivo VARCHAR(200) NOT NULL,
fecha DATE NOT NULL,
importe NUMERIC(10,2) NOT NULL CHECK (importe >= 0)
);Qué hace cada parte, en dos minutos
Sin entrar en la sintaxis (módulo 5), estos son los elementos que verás repetirse:
| Elemento | Qué significa |
|---|---|
GENERATED BY DEFAULT AS IDENTITY |
El id se genera solo si no lo indicas. Es la forma estándar moderna de un autoincremental |
PRIMARY KEY |
Clave primaria: única y no nula |
NOT NULL |
La columna es obligatoria |
UNIQUE |
No puede haber dos filas con el mismo valor (categorias.nombre, clientes.email) |
REFERENCES tabla(id) |
Clave foránea: el valor debe existir en la tabla referenciada |
ON DELETE RESTRICT / CASCADE / SET NULL |
Qué hacer si se borra el padre (lección 01-05) |
CHECK (...) |
Restricción de dominio: rechaza valores fuera del rango permitido |
DEFAULT valor |
Valor que se usa si no indicas ninguno |
Fíjate en dos detalles del orden:
- Las tablas se crean en orden de dependencias.
productosreferencia acategoriasyproveedores, así que esas dos van antes. El borrado (DROP) se hace en el orden inverso. - Las tablas reflexivas se referencian a sí mismas dentro de su propia definición (
clientes.referido_por_id REFERENCES clientes(id)). PostgreSQL lo permite sin problema.
- El script de carga de datos
Añade este segundo bloque al final del mismo fichero tiendaverde.sql.
-- =====================================================================
-- Bloque 2: carga de datos
-- =====================================================================
-- ---------------------------------------------------------------------
-- categorias (6)
-- ---------------------------------------------------------------------
INSERT INTO categorias (id, nombre, descripcion) VALUES
(1, 'Alimentación', 'Productos ecológicos de alimentación seca y conservas'),
(2, 'Cosmética natural', 'Cosmética con ingredientes naturales y sin parabenos'),
(3, 'Hogar sostenible', 'Limpieza y menaje con materiales reutilizables'),
(4, 'Bebidas', 'Infusiones, zumos y fermentados ecológicos'),
(5, 'Higiene personal', 'Higiene diaria con envases reducidos o compostables'),
(6, 'Complementos', 'Suplementos alimenticios de origen vegetal');
-- ---------------------------------------------------------------------
-- proveedores (5) - el 5 está inactivo pero conserva productos
-- ---------------------------------------------------------------------
INSERT INTO proveedores (id, nombre, pais, email, activo) VALUES
(1, 'Huerta del Turia', 'España', '[email protected]', TRUE),
(2, 'BioSierra Ibérica', 'España', '[email protected]', TRUE),
(3, 'Verde Atlántico', 'Portugal', '[email protected]', TRUE),
(4, 'Maison Nature', 'Francia', '[email protected]', TRUE),
(5, 'EcoNordic Supplies', 'Alemania', '[email protected]', FALSE);
-- ---------------------------------------------------------------------
-- productos (20)
-- 13 -> stock 0 ; 20 -> activo FALSE ; 13, 19 y 20 nunca vendidos
-- ---------------------------------------------------------------------
INSERT INTO productos (id, nombre, categoria_id, proveedor_id, precio, coste, stock, activo, fecha_alta) VALUES
( 1, 'Aceite de oliva virgen extra 500 ml', 1, 1, 12.50, 7.80, 120, TRUE, '2025-01-15'),
( 2, 'Arroz integral ecológico 1 kg', 1, 1, 3.90, 2.10, 200, TRUE, '2025-01-15'),
( 3, 'Miel de azahar cruda 500 g', 1, 2, 9.75, 5.40, 80, TRUE, '2025-01-15'),
( 4, 'Pasta de espelta 500 g', 1, 2, 2.80, 1.35, 150, TRUE, '2025-01-20'),
( 5, 'Tomate triturado ecológico 400 g', 1, 1, 1.95, 0.90, 300, TRUE, '2025-01-20'),
( 6, 'Crema facial de aloe vera 50 ml', 2, 4, 18.90, 9.50, 60, TRUE, '2025-01-20'),
( 7, 'Champú sólido de romero 80 g', 2, 4, 8.40, 3.60, 95, TRUE, '2025-02-01'),
( 8, 'Aceite corporal de almendras 200 ml', 2, 3, 14.25, 7.10, 45, TRUE, '2025-02-01'),
( 9, 'Bálsamo labial de caléndula 15 ml', 2, 4, 4.60, 1.80, 130, TRUE, '2025-02-01'),
(10, 'Detergente ecológico concentrado 1 L', 3, 5, 11.20, 6.00, 70, TRUE, '2025-02-10'),
(11, 'Estropajo vegetal de luffa (pack 3)', 3, 3, 5.50, 2.20, 110, TRUE, '2025-02-10'),
(12, 'Bolsas reutilizables de algodón (pack 5)', 3, 3, 9.90, 4.30, 85, TRUE, '2025-02-10'),
(13, 'Velas de cera de soja (pack 2)', 3, 5, 13.75, 6.90, 0, TRUE, '2025-03-01'),
(14, 'Infusión de manzanilla ecológica 20 uds', 4, 2, 3.25, 1.40, 180, TRUE, '2025-02-20'),
(15, 'Té verde matcha ceremonial 30 g', 4, 3, 22.00, 12.50, 40, TRUE, '2025-02-20'),
(16, 'Kombucha de jengibre 750 ml', 4, 1, 4.95, 2.30, 60, TRUE, '2025-03-15'),
(17, 'Zumo de naranja prensado en frío 1 L', 4, 1, 5.40, 2.60, 90, TRUE, '2025-03-15'),
(18, 'Cepillo de dientes de bambú', 5, 5, 3.50, 1.20, 240, TRUE, '2025-04-01'),
(19, 'Desodorante natural en barra 50 g', 5, 4, 7.80, 3.30, 75, TRUE, '2025-05-10'),
(20, 'Cápsulas de espirulina 120 uds', 6, 5, 16.40, 8.70, 55, FALSE, '2025-06-01');
-- ---------------------------------------------------------------------
-- clientes (15) - los 13, 14 y 15 no han hecho ningún pedido
-- ---------------------------------------------------------------------
INSERT INTO clientes (id, nombre, apellidos, email, ciudad, pais, fecha_registro, referido_por_id) VALUES
( 1, 'Lucía', 'Martínez Soler', '[email protected]', 'Valencia', 'España', '2025-01-10', NULL),
( 2, 'Carlos', 'Ferrer Ibáñez', '[email protected]', 'Valencia', 'España', '2025-01-22', 1),
( 3, 'Marta', 'Sanchis Gil', '[email protected]', 'Castellón', 'España', '2025-02-03', 1),
( 4, 'Javier', 'Ortega Ruiz', '[email protected]', 'Madrid', 'España', '2025-02-14', NULL),
( 5, 'Ana', 'Belmonte Roca', '[email protected]', 'Barcelona', 'España', '2025-02-27', 2),
( 6, 'Pau', 'Llorens Vidal', '[email protected]', 'Valencia', 'España', '2025-03-09', NULL),
( 7, 'Sofia', 'Moreira Costa', '[email protected]', 'Lisboa', 'Portugal', '2025-03-21', NULL),
( 8, 'Tiago', 'Almeida Nunes', '[email protected]', 'Oporto', 'Portugal', '2025-04-04', 7),
( 9, 'Camille', 'Dubois', '[email protected]', 'Lyon', 'Francia', '2025-04-18', NULL),
(10, 'Julien', 'Moreau', '[email protected]', 'París', 'Francia', '2025-05-02', 9),
(11, 'Elena', 'Navarro Puig', '[email protected]', 'Alicante', 'España', '2025-05-16', 6),
(12, 'Diego', 'Ramos Herrera', '[email protected]', 'Sevilla', 'España', '2025-06-01', NULL),
(13, 'Núria', 'Bosch Ferrer', '[email protected]', 'Barcelona', 'España', '2025-06-20', 5),
(14, 'Hugo', 'Iglesias Pardo', '[email protected]', 'Zaragoza', 'España', '2025-09-12', NULL),
(15, 'Inés', 'Carrasco Vega', '[email protected]', 'Valencia', 'España', '2026-01-08', 1);
-- ---------------------------------------------------------------------
-- empleados (8) - jerarquía vía jefe_id ; solo 4, 5 y 6 gestionan pedidos
-- ---------------------------------------------------------------------
INSERT INTO empleados (id, nombre, apellidos, puesto, jefe_id, salario, fecha_contratacion, ciudad) VALUES
(1, 'Rosa', 'Alcázar Vives', 'Directora general', NULL, 62000.00, '2024-09-01', 'Valencia'),
(2, 'Andrés', 'Company Talens', 'Responsable de ventas', 1, 41000.00, '2024-10-15', 'Valencia'),
(3, 'Beatriz', 'Nadal Ripoll', 'Responsable de logística', 1, 39500.00, '2024-11-02', 'Valencia'),
(4, 'Óscar', 'Peris Blasco', 'Comercial', 2, 28500.00, '2025-01-13', 'Valencia'),
(5, 'Laia', 'Puig Sanchis', 'Comercial', 2, 27800.00, '2025-02-17', 'Castellón'),
(6, 'Marc', 'Estévez Roig', 'Atención al cliente', 2, 24500.00, '2025-03-24', 'Valencia'),
(7, 'Irene', 'Salvador Mira', 'Operaria de almacén', 3, 22000.00, '2025-04-07', 'Valencia'),
(8, 'Daniel', 'Vercher Lluch', 'Analista de datos', 1, 35000.00, '2025-06-16', 'Valencia');
-- ---------------------------------------------------------------------
-- pedidos (20) - 10 sin empleado asignado (pedidos web)
-- ---------------------------------------------------------------------
INSERT INTO pedidos (id, cliente_id, empleado_id, fecha_pedido, estado, metodo_pago, gastos_envio) VALUES
( 1, 1, NULL, '2025-03-04', 'entregado', 'tarjeta', 4.95),
( 2, 2, 4, '2025-03-12', 'entregado', 'transferencia', 0.00),
( 3, 3, NULL, '2025-04-02', 'entregado', 'tarjeta', 4.95),
( 4, 4, 5, '2025-04-19', 'entregado', 'paypal', 4.95),
( 5, 1, NULL, '2025-05-07', 'entregado', 'tarjeta', 0.00),
( 6, 5, 4, '2025-05-23', 'cancelado', 'tarjeta', 4.95),
( 7, 6, NULL, '2025-06-11', 'entregado', 'contrareembolso', 6.50),
( 8, 7, 5, '2025-06-28', 'entregado', 'tarjeta', 9.90),
( 9, 8, NULL, '2025-07-15', 'entregado', 'paypal', 9.90),
(10, 9, 4, '2025-08-03', 'entregado', 'tarjeta', 12.50),
(11, 2, NULL, '2025-09-09', 'entregado', 'tarjeta', 0.00),
(12, 10, 5, '2025-10-01', 'entregado', 'transferencia', 12.50),
(13, 11, NULL, '2025-10-22', 'entregado', 'tarjeta', 4.95),
(14, 12, 6, '2025-11-14', 'entregado', 'paypal', 4.95),
(15, 1, NULL, '2025-12-02', 'entregado', 'tarjeta', 0.00),
(16, 4, 4, '2025-12-19', 'enviado', 'tarjeta', 4.95),
(17, 7, NULL, '2026-01-13', 'enviado', 'paypal', 9.90),
(18, 5, 5, '2026-01-27', 'pagado', 'transferencia', 4.95),
(19, 6, NULL, '2026-02-09', 'pagado', 'tarjeta', 4.95),
(20, 9, 6, '2026-02-21', 'pendiente', 'contrareembolso', 12.50);
-- ---------------------------------------------------------------------
-- lineas_pedido (47)
-- Las líneas 1 y 4 llevan el precio HISTÓRICO, anterior a la subida
-- de tarifas de abril de 2025: por eso no coincide con productos.precio
-- ---------------------------------------------------------------------
INSERT INTO lineas_pedido (id, pedido_id, producto_id, cantidad, precio_unitario, descuento) VALUES
( 1, 1, 1, 2, 11.95, 0.00),
( 2, 1, 2, 3, 3.90, 0.00),
( 3, 1, 14, 2, 3.25, 0.00),
( 4, 2, 6, 1, 17.50, 0.00),
( 5, 2, 9, 2, 4.60, 0.00),
( 6, 3, 5, 6, 1.95, 0.10),
( 7, 3, 4, 4, 2.80, 0.00),
( 8, 3, 2, 2, 3.90, 0.00),
( 9, 4, 15, 1, 22.00, 0.00),
(10, 4, 3, 1, 9.75, 0.00),
(11, 5, 10, 1, 11.20, 0.00),
(12, 5, 11, 2, 5.50, 0.00),
(13, 5, 12, 1, 9.90, 0.00),
(14, 6, 1, 1, 12.50, 0.00),
(15, 6, 8, 1, 14.25, 0.00),
(16, 7, 16, 4, 4.95, 0.00),
(17, 7, 17, 2, 5.40, 0.00),
(18, 8, 1, 3, 12.50, 0.05),
(19, 8, 3, 2, 9.75, 0.00),
(20, 8, 14, 3, 3.25, 0.00),
(21, 9, 7, 2, 8.40, 0.00),
(22, 9, 9, 3, 4.60, 0.00),
(23, 9, 18, 4, 3.50, 0.00),
(24, 10, 6, 2, 18.90, 0.10),
(25, 10, 8, 1, 14.25, 0.00),
(26, 11, 2, 5, 3.90, 0.00),
(27, 11, 5, 8, 1.95, 0.15),
(28, 12, 15, 2, 22.00, 0.00),
(29, 12, 14, 4, 3.25, 0.00),
(30, 12, 16, 2, 4.95, 0.00),
(31, 13, 12, 2, 9.90, 0.00),
(32, 13, 18, 3, 3.50, 0.00),
(33, 14, 1, 1, 12.50, 0.00),
(34, 14, 4, 3, 2.80, 0.00),
(35, 14, 17, 2, 5.40, 0.00),
(36, 15, 3, 2, 9.75, 0.00),
(37, 15, 7, 1, 8.40, 0.00),
(38, 15, 11, 1, 5.50, 0.00),
(39, 16, 10, 2, 11.20, 0.05),
(40, 16, 12, 1, 9.90, 0.00),
(41, 17, 1, 2, 12.50, 0.00),
(42, 17, 15, 1, 22.00, 0.00),
(43, 18, 6, 1, 18.90, 0.00),
(44, 18, 9, 2, 4.60, 0.00),
(45, 19, 16, 6, 4.95, 0.10),
(46, 20, 2, 4, 3.90, 0.00),
(47, 20, 18, 2, 3.50, 0.00);
-- ---------------------------------------------------------------------
-- resenas (12) - siempre de clientes que compraron ese producto
-- ---------------------------------------------------------------------
INSERT INTO resenas (id, producto_id, cliente_id, puntuacion, comentario, fecha) VALUES
( 1, 1, 1, 5, 'Aceite excelente, sabor intenso y envase muy cuidado.', '2025-03-15'),
( 2, 2, 1, 4, 'Buen arroz, aunque tarda algo más en cocer de lo habitual.', '2025-03-16'),
( 3, 6, 2, 5, 'La crema deja la piel muy suave. Repetiré sin duda.', '2025-03-25'),
( 4, 5, 3, 3, 'Correcto por el precio, nada espectacular.', '2025-04-12'),
( 5, 15, 4, 5, 'Matcha de calidad ceremonial de verdad, color impecable.', '2025-05-02'),
( 6, 16, 6, 2, 'Demasiado jengibre para mi gusto, casi no se puede beber.', '2025-06-20'),
( 7, 1, 7, 5, 'Lo compro cada mes, insuperable relación calidad-precio.', '2025-07-08'),
( 8, 18, 8, 4, 'Cumple perfectamente, aunque las cerdas son algo duras.', '2025-07-26'),
( 9, 6, 9, 4, 'Muy buena hidratación, el envío a Francia tardó bastante.', '2025-08-14'),
(10, 2, 2, 5, 'Grano suelto y sabor limpio, mejor que el del supermercado.', '2025-09-19'),
(11, 12, 11, 3, 'Bolsas resistentes pero más pequeñas de lo que esperaba.', '2025-11-03'),
(12, 10, 4, 4, 'Rinde muchísimo, un litro dura meses.', '2026-01-10');
-- ---------------------------------------------------------------------
-- devoluciones (3)
-- ---------------------------------------------------------------------
INSERT INTO devoluciones (id, pedido_id, motivo, fecha, importe) VALUES
(1, 6, 'Pedido cancelado por el cliente antes del envío', '2025-05-25', 26.75),
(2, 10, 'Producto dañado durante el transporte', '2025-08-11', 34.02),
(3, 13, 'El formato no corresponde a lo esperado', '2025-10-30', 19.80);
-- ---------------------------------------------------------------------
-- Sincronizar las secuencias de identidad con los ids ya insertados,
-- para que los futuros INSERT sin id no choquen con las claves usadas.
-- ---------------------------------------------------------------------
SELECT setval(pg_get_serial_sequence('categorias', 'id'), (SELECT MAX(id) FROM categorias));
SELECT setval(pg_get_serial_sequence('proveedores', 'id'), (SELECT MAX(id) FROM proveedores));
SELECT setval(pg_get_serial_sequence('productos', 'id'), (SELECT MAX(id) FROM productos));
SELECT setval(pg_get_serial_sequence('clientes', 'id'), (SELECT MAX(id) FROM clientes));
SELECT setval(pg_get_serial_sequence('empleados', 'id'), (SELECT MAX(id) FROM empleados));
SELECT setval(pg_get_serial_sequence('pedidos', 'id'), (SELECT MAX(id) FROM pedidos));
SELECT setval(pg_get_serial_sequence('lineas_pedido', 'id'), (SELECT MAX(id) FROM lineas_pedido));
SELECT setval(pg_get_serial_sequence('resenas', 'id'), (SELECT MAX(id) FROM resenas));
SELECT setval(pg_get_serial_sequence('devoluciones', 'id'), (SELECT MAX(id) FROM devoluciones));Explicación del script de carga por bloques
| Bloque | Qué carga | Detalle que importa |
|---|---|---|
categorias |
6 categorías | Ninguna dependencia: van primero |
proveedores |
5 proveedores | El 5 (EcoNordic) tiene activo = FALSE pero mantiene productos |
productos |
20 productos | El 13 con stock 0, el 20 descatalogado; 13, 19 y 20 nunca se venderán |
clientes |
15 clientes | Ocho con referido_por_id; los 13, 14 y 15 sin pedidos |
empleados |
8 empleados | La 1 tiene jefe_id NULL; solo 4, 5 y 6 aparecen en pedidos |
pedidos |
20 pedidos | Marzo 2025 – febrero 2026; 10 con empleado_id NULL |
lineas_pedido |
47 líneas | Dos con precio histórico distinto del actual |
resenas |
12 reseñas | Fecha siempre posterior a la del pedido correspondiente |
devoluciones |
3 devoluciones | Asociadas a los pedidos 6, 10 y 13 |
setval(...) |
— | Ajusta las secuencias tras insertar ids explícitos |
Ese último bloque merece una explicación. Como hemos insertado los id a mano, el contador interno de cada tabla sigue en 1. Si mañana insertaras un cliente sin indicar id, PostgreSQL intentaría asignarle el 1 y fallaría por clave duplicada. setval adelanta el contador al máximo ya usado. Es un detalle habitual al cargar datos iniciales, y lo verás de nuevo en el módulo 5.
Nota sobre los INSERT múltiples: cada sentencia inserta muchas filas con una sola instrucción, separando las tuplas por comas. Es mucho más rápido que un INSERT por fila, y la sintaxis completa se estudia en la lección 05-02.
- Cómo cargar la base de datos
Si aún no la has creado, repasa la lección 01-02. Con la base tiendaverde ya existente, hay dos formas de ejecutar el script.
Desde la línea de comandos (recomendada)
Salida esperada (resumida):
DROP TABLE
...
CREATE TABLE
CREATE TABLE
...
INSERT 0 6
INSERT 0 5
INSERT 0 20
INSERT 0 15
INSERT 0 8
INSERT 0 20
INSERT 0 47
INSERT 0 12
INSERT 0 3
setval
--------
6
...Cada INSERT 0 N te dice cuántas filas ha insertado esa sentencia. Si ves los números 6, 5, 20, 15, 8, 20, 47, 12 y 3 en ese orden, la carga ha ido bien.
Desde dentro de psql
Con Docker, si el fichero está en tu máquina y PostgreSQL en el contenedor, puedes copiarlo dentro o canalizarlo directamente:
docker exec -i pg-curso psql -U postgres -d tiendaverde < tiendaverde.sql
Si algo falla
| Error | Causa y solución |
|---|---|
permission denied for schema public |
Falta GRANT ALL ON SCHEMA public TO curso_sql; ejecutado como superusuario |
database "tiendaverde" does not exist |
Créala primero (lección 01-02) |
| Caracteres raros en lugar de tildes | La base no está en UTF-8. Recréala con ENCODING 'UTF8' |
relation "categorias" already exists |
Estás ejecutando solo el bloque 2. Ejecuta el fichero completo: el DROP TABLE IF EXISTS inicial lo resuelve |
El script es idempotente: puedes ejecutarlo tantas veces como quieras y siempre te dejará la base en el mismo estado. Si en algún módulo haces experimentos con UPDATE o DELETE y quieres volver al punto de partida, basta con volver a lanzarlo.
- Consultas de verificación
Comprueba que todo está donde debe estar.
8.1. Las nueve tablas existen
Debes ver las nueve: categorias, clientes, devoluciones, empleados, lineas_pedido, pedidos, productos, proveedores, resenas.
8.2. Número de filas por tabla
SELECT 'categorias' AS tabla, COUNT(*) AS filas FROM categorias
UNION ALL SELECT 'proveedores', COUNT(*) FROM proveedores
UNION ALL SELECT 'productos', COUNT(*) FROM productos
UNION ALL SELECT 'clientes', COUNT(*) FROM clientes
UNION ALL SELECT 'empleados', COUNT(*) FROM empleados
UNION ALL SELECT 'pedidos', COUNT(*) FROM pedidos
UNION ALL SELECT 'lineas_pedido', COUNT(*) FROM lineas_pedido
UNION ALL SELECT 'resenas', COUNT(*) FROM resenas
UNION ALL SELECT 'devoluciones', COUNT(*) FROM devoluciones;Resultado esperado:
| tabla | filas |
|---|---|
| categorias | 6 |
| proveedores | 5 |
| productos | 20 |
| clientes | 15 |
| empleados | 8 |
| pedidos | 20 |
| lineas_pedido | 47 |
| resenas | 12 |
| devoluciones | 3 |
(No te preocupes por la sintaxis de UNION ALL ni de COUNT: se estudian en los módulos 3 y 4. Aquí es solo una herramienta de verificación.)
8.3. Los "huecos" deliberados están donde deben
SELECT
(SELECT COUNT(*) FROM clientes WHERE id NOT IN (SELECT cliente_id FROM pedidos)) AS clientes_sin_pedidos,
(SELECT COUNT(*) FROM productos WHERE id NOT IN (SELECT producto_id FROM lineas_pedido)) AS productos_sin_vender,
(SELECT COUNT(*) FROM pedidos WHERE empleado_id IS NULL) AS pedidos_sin_empleado,
(SELECT COUNT(*) FROM clientes WHERE referido_por_id IS NULL) AS clientes_no_referidos,
(SELECT COUNT(*) FROM empleados WHERE jefe_id IS NULL) AS empleados_sin_jefe;| clientes_sin_pedidos | productos_sin_vender | pedidos_sin_empleado | clientes_no_referidos | empleados_sin_jefe |
|---|---|---|---|---|
| 3 | 3 | 10 | 7 | 1 |
Si estos cinco números coinciden, tu base de datos es exactamente la que usarán todas las lecciones siguientes.
8.4. Un vistazo a los datos
| id | nombre | precio | stock | activo |
|---|---|---|---|---|
| 1 | Aceite de oliva virgen extra 500 ml | 12.50 | 120 | true |
| 2 | Arroz integral ecológico 1 kg | 3.90 | 200 | true |
| 3 | Miel de azahar cruda 500 g | 9.75 | 80 | true |
| 4 | Pasta de espelta 500 g | 2.80 | 150 | true |
| 5 | Tomate triturado ecológico 400 g | 1.95 | 300 | true |
| estado | pedidos |
|---|---|
| entregado | 14 |
| enviado | 2 |
| pagado | 2 |
| cancelado | 1 |
| pendiente | 1 |
Errores Comunes y Consejos
- Ejecutar los bloques en desorden. Las tablas deben crearse antes que sus dependientes y los datos cargarse en el mismo orden. Ejecuta siempre el fichero completo.
- Copiar el script a medias. Un
INSERTcortado por la mitad deja la base incoherente. Copia bloque a bloque y comprueba los contadores de filas. - Modificar los datos y no poder volver atrás. El script es idempotente: relánzalo y vuelves al estado inicial. Guárdalo en un sitio localizable.
- Sorprenderse de que
precio_unitariono coincida conprecio. En las líneas 1 y 4 es intencionado: son precios históricos anteriores a la subida de tarifas. - Interpretar
descuentocomo porcentaje. Es una fracción:0.10significa 10 %. El importe de una línea escantidad * precio_unitario * (1 - descuento). - Esperar reseñas de todos los productos. Solo 9 de los 20 productos tienen reseñas, y eso es deliberado.
- Olvidar el
setvalfinal. Sin él, el primerINSERTsinidexplícito fallará por clave duplicada. - Consejo: ten el diagrama a mano. Vuelve a esta lección cada vez que dudes de qué tabla contiene qué columna; te ahorrará muchos
column does not exist. - Consejo: usa
\d tablaantes de cada ejercicio. Es más rápido que buscar en el texto. - Consejo: crea una copia de seguridad.
pg_dump -U curso_sql -d tiendaverde -f copia.sqlte permite restaurar en segundos si rompes algo.
Ejercicios
Ejercicio 1
Carga la base de datos en tu equipo y verifica la instalación respondiendo a estas cuatro preguntas con los comandos y consultas de la sección 8:
- ¿Existen las nueve tablas?
- ¿Cuántas filas tiene cada una?
- ¿Qué columnas, tipos y claves foráneas tiene
lineas_pedido? - ¿Cuántos pedidos no tienen empleado asignado?
Ejercicio 2
Sin ejecutar nada, y usando solo el diagrama y las descripciones de tablas, di qué tablas y qué columnas necesitarías para responder a cada pregunta de negocio. No escribas la consulta: indica el camino entre tablas.
- ¿Cuál es la puntuación media del producto "Aceite de oliva virgen extra 500 ml"?
- ¿Qué comercial gestionó el pedido número 12 y quién es su jefe?
- ¿Cuánto facturó el pedido 8, gastos de envío incluidos?
- ¿De qué país es el proveedor del producto más caro del catálogo?
- ¿Qué clientes fueron referidos por Lucía Martínez Soler?
Ejercicio 3
Predice qué ocurrirá con cada una de estas operaciones sobre la base recién cargada. Después ejecútalas y comprueba tu predicción (recuerda que puedes recargar el script para volver al estado inicial).
-- a)
INSERT INTO categorias (nombre, descripcion) VALUES ('Bebidas', 'Duplicada');
-- b)
INSERT INTO resenas (producto_id, cliente_id, puntuacion, comentario, fecha)
VALUES (1, 14, 7, 'Genial', '2026-03-01');
-- c)
INSERT INTO lineas_pedido (pedido_id, producto_id, cantidad, precio_unitario, descuento)
VALUES (20, 13, 1, 13.75, 0.00);
-- d)
DELETE FROM pedidos WHERE id = 5;
SELECT COUNT(*) FROM lineas_pedido;Soluciones
Solución 1
Nueve tablas listadas. Para el recuento de filas, la consulta con UNION ALL de la sección 8.2, que debe devolver 6, 5, 20, 15, 8, 20, 47, 12 y 3.
Para la estructura de lineas_pedido:
Muestra las seis columnas (id, pedido_id, producto_id, cantidad, precio_unitario, descuento), sus tipos, la PK sobre id y las dos claves foráneas: pedido_id → pedidos(id) ON DELETE CASCADE y producto_id → productos(id) ON DELETE RESTRICT.
Y para los pedidos sin empleado:
| count |
|---|
| 10 |
Solución 2
| # | Pregunta | Camino entre tablas | Columnas clave |
|---|---|---|---|
| 1 | Puntuación media de un producto | productos → resenas |
productos.nombre, resenas.producto_id, resenas.puntuacion |
| 2 | Comercial del pedido 12 y su jefe | pedidos → empleados → empleados (self join) |
pedidos.empleado_id, empleados.id, empleados.jefe_id |
| 3 | Facturación del pedido 8 | pedidos → lineas_pedido |
lineas_pedido.cantidad, precio_unitario, descuento, más pedidos.gastos_envio |
| 4 | País del proveedor del producto más caro | productos → proveedores |
productos.precio, productos.proveedor_id, proveedores.pais |
| 5 | Clientes referidos por Lucía | clientes → clientes (self join) |
clientes.id, clientes.referido_por_id, clientes.nombre |
Fíjate en que dos de las cinco preguntas requieren unir una tabla consigo misma: es lo que veremos como SELF JOIN en la lección 03-06, y es la consecuencia directa de las relaciones reflexivas del esquema.
Solución 3
a) Falla por la restricción UNIQUE de categorias.nombre:
ERROR: duplicate key value violates unique constraint "categorias_nombre_key" DETAIL: Key (nombre)=(Bebidas) already exists.
Es la protección de la clave natural que mencionamos en la lección 01-05: aunque la PK sea id, el UNIQUE impide duplicar el nombre.
b) Falla por la restricción CHECK de la puntuación:
ERROR: new row for relation "resenas" violates check constraint "resenas_puntuacion_check" DETAIL: Failing row contains (13, 1, 14, 7, Genial, 2026-03-01).
El dominio de puntuacion es 1-5, y el tipo SMALLINT por sí solo no lo garantiza: hace falta el CHECK. Observa además que el cliente 14 no ha comprado nunca ese producto; la base de datos no impide eso, porque ninguna restricción lo exige. Es un buen recordatorio de que las restricciones solo protegen aquello que declaras explícitamente.
c) Funciona. El producto 13 (Velas de cera de soja) existe y el pedido 20 también, así que la FK se satisface. Ahora lineas_pedido tendría 48 filas y el producto 13 dejaría de estar "sin vender".
Ojo: esto rompe uno de los huecos deliberados del conjunto de datos. Si lo ejecutas, recarga el script antes de continuar con el módulo 3, o algunos resultados de las lecciones de LEFT JOIN no coincidirán. Repara además en que la base no ha comprobado si hay stock (el producto 13 tiene 0 unidades): esa regla es lógica de negocio y no está declarada como restricción.
d) Funciona, y borra en cascada. El pedido 5 tiene tres líneas (ids 11, 12 y 13), y lineas_pedido.pedido_id está declarada ON DELETE CASCADE:
| count |
|---|
| 44 |
Una sola sentencia ha eliminado cuatro filas en dos tablas. Es exactamente el comportamiento que anticipamos en la lección 01-05 y la razón por la que CASCADE debe reservarse para relaciones de composición real. Recarga el script para recuperar el estado original.
Conclusión
Con esta lección cierras el módulo 1 y, sobre todo, tienes ya el terreno preparado:
- Conoces TiendaVerde como negocio: qué vende, a quién, por qué canales y con qué equipo.
- Sabes leer su diagrama entidad-relación: nueve tablas, nueve relaciones 1:N, una N:M resuelta con
lineas_pedidoy dos relaciones reflexivas enclientesyempleados. - Tienes la descripción columna a columna de las nueve tablas, con sus tipos, claves y significados.
- Has ejecutado el script completo y verificado los recuentos: 6 categorías, 5 proveedores, 20 productos, 15 clientes, 8 empleados, 20 pedidos, 47 líneas, 12 reseñas y 3 devoluciones.
- Entiendes los huecos deliberados del conjunto de datos —3 clientes sin pedidos, 3 productos sin vender, 10 pedidos sin empleado, 11 productos sin reseñas— y por qué las lecciones de
LEFT JOIN,NULLy agregación los necesitan. - Sabes que el script es idempotente: siempre puedes volver al estado inicial relanzándolo.
Has completado el módulo 1. A estas alturas entiendes qué es SQL y qué lugar ocupa, tienes PostgreSQL 16 funcionando, dominas las reglas de escritura del lenguaje, sabes cómo se estructuran y tipan los datos, comprendes por qué las tablas se relacionan como lo hacen y tienes cargada la base de datos que te acompañará hasta el proyecto final. En el módulo 2, Consultas básicas de SQL, empezarás por fin a interrogar a TiendaVerde: la instrucción SELECT y cómo elegir columnas, los alias y las columnas calculadas, el filtrado con WHERE, la eliminación de duplicados con DISTINCT, la ordenación con ORDER BY y la limitación de resultados con LIMIT. La primera consulta real está a una lección de distancia.
Curso de SQL
Módulo 1: Introducción a SQL
- ¿Qué es SQL?
- Configurando tu entorno SQL
- Sintaxis básica de SQL
- Entendiendo bases de datos y tablas
- El modelo relacional: claves primarias y foráneas
- La base de datos del curso: TiendaVerde
Módulo 2: Consultas básicas de SQL
- Instrucción SELECT
- Alias, expresiones y columnas calculadas
- Filtrando datos con WHERE
- DISTINCT y eliminación de duplicados
- Ordenando datos con ORDER BY
- Limitando resultados con LIMIT
Módulo 3: Trabajando con múltiples tablas
- Operaciones JOIN
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN y CROSS JOIN
- Uniones de conjuntos: UNION, INTERSECT y EXCEPT
Módulo 4: Filtrado avanzado de datos
- Usando LIKE para coincidencia de patrones
- Operadores IN y BETWEEN
- Valores NULL y IS NULL
- Funciones de agregación: COUNT, SUM, AVG, MIN y MAX
- Agregando datos con GROUP BY
- Cláusula HAVING
Módulo 5: Manipulación de datos
- Creando tablas y restricciones con CREATE TABLE
- Instrucción INSERT
- Instrucción UPDATE
- Instrucción DELETE
- Instrucción UPSERT (MERGE)
- Modificando el esquema: ALTER TABLE y migraciones seguras
Módulo 6: Funciones avanzadas de SQL
- Funciones de cadena
- Funciones numéricas
- Funciones de fecha y hora
- Conversión de tipos y manejo de NULL: CAST y COALESCE
- Expresiones condicionales
Módulo 7: Subconsultas y consultas anidadas
- Introducción a subconsultas
- Subconsultas correlacionadas
- EXISTS y NOT EXISTS
- Usando subconsultas en cláusulas SELECT, FROM y WHERE
- Subconsultas o JOIN: cuál elegir
Módulo 8: Índices y optimización de rendimiento
- Entendiendo los índices
- Creación y gestión de índices
- Tipos de índice y cuándo no indexar
- Técnicas de optimización de consultas
- Análisis del rendimiento de consultas
Módulo 9: Transacciones y concurrencia
- Introducción a las transacciones
- Propiedades ACID
- Instrucciones de control de transacciones
- Niveles de aislamiento y anomalías de concurrencia
- Manejo de concurrencia: bloqueos e interbloqueos
Módulo 10: Temas avanzados
- Vistas
- Expresiones de tabla comunes (CTE)
- Funciones de ventana
- Procedimientos almacenados
- Triggers
- JSON y datos semiestructurados
Módulo 11: SQL en la práctica
- Casos de uso en el mundo real
- Mejores prácticas
- Seguridad: inyección SQL, permisos y roles
- SQL para análisis de datos
- SQL en desarrollo web
