Ejercicios — Bases de Datos I

Enunciado + pista + solución escondible. Usá SQLite o cualquier RDBMS para verificar.


Cap. 1 — Modelo relacional

1.1 — Identificar tipos de claves (básico)

En esta tabla Empleado(id, dui, nombre, depto_id, jefe_id), clasificá:

a) ¿Clave primaria candidata? b) ¿Claves foráneas?

✅ Solución

a) id y dui ambos son candidatas (únicas, no nulas). Solo una se elige como PK; la otra queda como UNIQUE. b) depto_idDepartamento.id y jefe_idEmpleado.id (FK auto-referencial).

1.2 — Cardinalidades (intermedio)

¿Qué relación tiene cada par? (1:1, 1:N, N:M)

a) Persona ↔ DUI. b) Cliente ↔ Pedido (un cliente puede tener varios pedidos; un pedido es de un cliente). c) Estudiante ↔ Materia (cada estudiante cursa varias materias; cada materia tiene muchos estudiantes). d) Empleado ↔ Departamento (un empleado pertenece a un depto; un depto tiene muchos empleados).

✅ Solución

a) 1:1. b) 1:N. c) N:M (resuelve con tabla intermedia Inscripcion(estudiante_id, materia_id)). d) 1:N.


Cap. 2 — SQL básico

Schema para los ejercicios:

CREATE TABLE Cliente (
    id INTEGER PRIMARY KEY,
    nombre TEXT NOT NULL,
    ciudad TEXT,
    fecha_alta DATE
);
CREATE TABLE Pedido (
    id INTEGER PRIMARY KEY,
    cliente_id INTEGER REFERENCES Cliente(id),
    fecha DATE,
    total REAL
);
CREATE TABLE LineaPedido (
    pedido_id INTEGER REFERENCES Pedido(id),
    producto TEXT,
    cantidad INTEGER,
    precio_unit REAL,
    PRIMARY KEY (pedido_id, producto)
);

2.1 — SELECT básico

Devolvé los clientes de San Miguel ordenados alfabéticamente.

✅ Solución
SELECT * FROM Cliente
WHERE ciudad = 'San Miguel'
ORDER BY nombre;

2.2 — JOIN (intermedio)

Mostrá pedido_id, fecha y nombre del cliente, para todos los pedidos del 2024.

✅ Solución
SELECT p.id, p.fecha, c.nombre
FROM Pedido p
JOIN Cliente c ON p.cliente_id = c.id
WHERE p.fecha >= '2024-01-01' AND p.fecha < '2025-01-01';

2.3 — Agregación (intermedio)

Total facturado por ciudad en 2024.

✅ Solución
SELECT c.ciudad, SUM(p.total) AS facturacion
FROM Cliente c
JOIN Pedido p ON p.cliente_id = c.id
WHERE p.fecha BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY c.ciudad
ORDER BY facturacion DESC;

2.4 — Subconsulta (avanzado)

Clientes que NO hicieron pedidos en 2024.

💡 Pista

NOT IN o LEFT JOIN ... WHERE ... IS NULL o NOT EXISTS.

✅ Solución
-- Versión NOT EXISTS (más eficiente)
SELECT *
FROM Cliente c
WHERE NOT EXISTS (
    SELECT 1 FROM Pedido p
    WHERE p.cliente_id = c.id
      AND p.fecha BETWEEN '2024-01-01' AND '2024-12-31'
);

-- Equivalente con LEFT JOIN
SELECT c.*
FROM Cliente c
LEFT JOIN Pedido p
       ON p.cliente_id = c.id
      AND p.fecha BETWEEN '2024-01-01' AND '2024-12-31'
WHERE p.id IS NULL;

Trampa común: NOT IN (SELECT ...) falla silenciosamente si la subconsulta devuelve NULLs. NOT EXISTS es más robusto.

2.5 — Top N por grupo (avanzado)

Para cada cliente, el producto más vendido en cantidad acumulada.

✅ Solución

Usando window function (estándar SQL moderno):

SELECT cliente_id, producto, total_qty
FROM (
  SELECT p.cliente_id,
         lp.producto,
         SUM(lp.cantidad) AS total_qty,
         RANK() OVER (
           PARTITION BY p.cliente_id
           ORDER BY SUM(lp.cantidad) DESC
         ) AS rk
  FROM Pedido p
  JOIN LineaPedido lp ON lp.pedido_id = p.id
  GROUP BY p.cliente_id, lp.producto
) t
WHERE rk = 1;

Sin window functions hay que hacer self-join: 10× más feo pero portable a MySQL viejo.


Cap. 3 — Modelado E-R

3.1 — Modelar una pupusería (intermedio)

Diseñá un esquema E-R para una pupusería con:

✅ Solución

Entidades: Sucursal, Producto, Empleado, Pedido, LineaPedido.

Relaciones:

  • Sucursal — Producto (N:M, con atributo "precio") → tabla Precio(sucursal_id, producto_id, precio).
  • Sucursal — Empleado (1:N).
  • Sucursal — Pedido (1:N).
  • Pedido — Producto (N:M, vía LineaPedido con cantidad).

Schema:

CREATE TABLE Sucursal (id PK, nombre, ciudad);
CREATE TABLE Producto (id PK, nombre, descripcion);
CREATE TABLE Precio (sucursal_id FK, producto_id FK, precio,
                     PK (sucursal_id, producto_id));
CREATE TABLE Empleado (id PK, nombre, sucursal_id FK, rol);
CREATE TABLE Pedido (id PK, sucursal_id FK, fecha, cliente);
CREATE TABLE LineaPedido (pedido_id FK, producto_id FK, cantidad,
                          PK (pedido_id, producto_id));

3.2 — Atributos derivados (básico)

Pedido tiene total. ¿Es buena práctica almacenarlo o calcularlo cada vez?

✅ Solución

Depende:

  • Almacenarlo (denormalización): rápido de leer, pero hay que mantenerlo consistente con las líneas (trigger o recalcular tras cada cambio).
  • Calcularlo (vista o SUM): siempre consistente, pero costoso con muchas líneas.

Regla práctica: calculá por default. Almacená solo si:

  1. Hay alta lectura y poca escritura.
  2. El total se "congela" en el momento del pedido (no cambia con re-precios).

Para audits y facturas oficiales, almacenar es lo correcto: tu factura debe reflejar el precio al momento, no el actual.


Cap. 4 — Normalización

4.1 — Identificar forma normal (básico)

Para Estudiante(id, nombre, materia, profesor, oficina_profesor):

a) ¿Está en 1NF? b) ¿2NF? c) ¿3NF?

✅ Solución

a) Si no hay valores compuestos / repetidos (un solo materia y profesor por fila), sí está en 1NF.

b) No. Asumiendo PK = (id, materia): nombre depende solo de id (dependencia parcial). Violación 2NF.

c) No. oficina_profesor depende de profesor, no de la PK (dependencia transitiva). Violación 3NF.

Solución (3NF):

Estudiante(id, nombre)
Profesor(profesor_id, nombre, oficina)
Materia(materia_id, nombre, profesor_id FK)
Inscripcion(estudiante_id FK, materia_id FK, PK juntas)

4.2 — Cuándo desnormalizar (intermedio)

Tu sistema OLTP está 3NF. Las consultas de dashboard tardan 30 segundos. ¿Qué hacés?

✅ Solución

Opciones, en orden:

  1. Índices. El problema suele ser falta de índice. Mucho más rápido y barato que desnormalizar.
  2. Vista materializada. Pre-computar el dashboard cada N minutos. Útil si la latencia tolerada es minutos.
  3. Réplica de lectura desnormalizada. Una "tabla resumen" sincronizada por trigger o cron.
  4. Data warehouse separado. Para BI a gran escala. ETL desde OLTP a un esquema estrella.

No desnormalices el OLTP "porque sí". Mantiene integridad mucho mejor que después arreglar inconsistencias.


Reto integrador

R.1 — Sistema de pupusería con datos reales

Implementá la base de datos del cap. 3.1 en SQLite o PostgreSQL:

  1. Crear todas las tablas con FKs y constraints.
  2. Insertar al menos: 3 sucursales, 5 productos, 5 empleados, 20 pedidos con 1-4 líneas cada uno.
  3. Responder con SQL:
    • a) Ventas totales por sucursal en el último mes.
    • b) Top 3 productos por cantidad vendida en cada sucursal.
    • c) Empleados que atendieron más de 5 pedidos.
    • d) Productos sin ventas en los últimos 30 días.
    • e) Promedio de líneas por pedido por sucursal.
  4. Crear 3 índices justificados con EXPLAIN.
  5. Exportar el schema completo + queries en un archivo .sql reproducible.
💡 Pista de arquitectura
  • Usá SQLite si querés algo sin instalar (sqlite3 pupuseria.db).
  • Las queries con GROUP BY + JOIN necesitan window functions o subconsultas. Modernizá tu SQL.
  • EXPLAIN ANALYZE es estándar en PG; en SQLite es EXPLAIN QUERY PLAN.

Para más práctica: pgexercises.com (todo en SQL, gratis).