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

  1. El negocio: qué es TiendaVerde y cómo opera
  2. Diagrama entidad-relación
  3. Las nueve tablas, una a una
  4. Decisiones de diseño que conviene conocer
  5. El script de creación de tablas
  6. El script de carga de datos
  7. Cómo cargar la base de datos
  8. Consultas de verificación
  9. Errores Comunes y Consejos
  10. Ejercicios
  11. Conclusión

  1. 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_id pueda ser NULL, y será protagonista de las lecciones de LEFT JOIN y 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: pendientepagadoenviadoentregado, con la posibilidad de cancelado en 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?

  1. 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).

  1. 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 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) 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 FK → categorias.id. ON DELETE RESTRICT
proveedor_id INTEGER FK → proveedores.id. ON DELETE RESTRICT
precio NUMERIC(10,2) No Precio de venta al público, en euros
coste NUMERIC(10,2) 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 de LEFT 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) Ciudad de residencia
pais VARCHAR(60) No España, Portugal o Francia
fecha_registro DATE No Alta en la tienda
referido_por_id INTEGER 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 FK → empleados.id (reflexiva). NULL solo en dirección general. ON DELETE SET NULL
salario NUMERIC(10,2) Salario bruto anual en euros
fecha_contratacion DATE No Fecha de incorporación
ciudad VARCHAR(80) 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 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 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.

  1. 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

  1. 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:

  1. Las tablas se crean en orden de dependencias. productos referencia a categorias y proveedores, así que esas dos van antes. El borrado (DROP) se hace en el orden inverso.
  2. 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.

  1. 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.

  1. 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)

psql -h localhost -U curso_sql -d tiendaverde -f tiendaverde.sql

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

psql -h localhost -U curso_sql -d tiendaverde
tiendaverde=> \i tiendaverde.sql

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.

  1. Consultas de verificación

Comprueba que todo está donde debe estar.

8.1. Las nueve tablas existen

tiendaverde=> \dt

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

SELECT id, nombre, precio, stock, activo FROM productos WHERE id <= 5;
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
SELECT estado, COUNT(*) AS pedidos FROM pedidos GROUP BY estado ORDER BY pedidos DESC;
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 INSERT cortado 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_unitario no coincida con precio. En las líneas 1 y 4 es intencionado: son precios históricos anteriores a la subida de tarifas.
  • Interpretar descuento como porcentaje. Es una fracción: 0.10 significa 10 %. El importe de una línea es cantidad * 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 setval final. Sin él, el primer INSERT sin id explí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 tabla antes 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.sql te 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:

  1. ¿Existen las nueve tablas?
  2. ¿Cuántas filas tiene cada una?
  3. ¿Qué columnas, tipos y claves foráneas tiene lineas_pedido?
  4. ¿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.

  1. ¿Cuál es la puntuación media del producto "Aceite de oliva virgen extra 500 ml"?
  2. ¿Qué comercial gestionó el pedido número 12 y quién es su jefe?
  3. ¿Cuánto facturó el pedido 8, gastos de envío incluidos?
  4. ¿De qué país es el proveedor del producto más caro del catálogo?
  5. ¿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

tiendaverde=> \dt

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:

tiendaverde=> \d 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:

SELECT COUNT(*) FROM pedidos WHERE empleado_id IS NULL;
count
10

Solución 2

# Pregunta Camino entre tablas Columnas clave
1 Puntuación media de un producto productosresenas productos.nombre, resenas.producto_id, resenas.puntuacion
2 Comercial del pedido 12 y su jefe pedidosempleadosempleados (self join) pedidos.empleado_id, empleados.id, empleados.jefe_id
3 Facturación del pedido 8 pedidoslineas_pedido lineas_pedido.cantidad, precio_unitario, descuento, más pedidos.gastos_envio
4 País del proveedor del producto más caro productosproveedores productos.precio, productos.proveedor_id, proveedores.pais
5 Clientes referidos por Lucía clientesclientes (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".

INSERT 0 1

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:

DELETE 1
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_pedido y dos relaciones reflexivas en clientes y empleados.
  • 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, NULL y 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

Módulo 2: Consultas básicas de SQL

Módulo 3: Trabajando con múltiples tablas

Módulo 4: Filtrado avanzado de datos

Módulo 5: Manipulación de datos

Módulo 6: Funciones avanzadas de SQL

Módulo 7: Subconsultas y consultas anidadas

Módulo 8: Índices y optimización de rendimiento

Módulo 9: Transacciones y concurrencia

Módulo 10: Temas avanzados

Módulo 11: SQL en la práctica

Módulo 12: Proyecto final

© Copyright 2026. Todos los derechos reservados