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_id → Departamento.id y jefe_id → Empleado.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:
- Sucursales en varias ciudades.
- Productos con precio variable por sucursal.
- Empleados asignados a una sucursal.
- Pedidos con varios productos cada uno.
✅ Solución
Entidades: Sucursal, Producto, Empleado, Pedido, LineaPedido.
Relaciones:
Sucursal — Producto(N:M, con atributo "precio") → tablaPrecio(sucursal_id, producto_id, precio).Sucursal — Empleado(1:N).Sucursal — Pedido(1:N).Pedido — Producto(N:M, víaLineaPedidocon 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:
- Hay alta lectura y poca escritura.
- 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:
- Índices. El problema suele ser falta de índice. Mucho más rápido y barato que desnormalizar.
- Vista materializada. Pre-computar el dashboard cada N minutos. Útil si la latencia tolerada es minutos.
- Réplica de lectura desnormalizada. Una "tabla resumen" sincronizada por trigger o cron.
- 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:
- Crear todas las tablas con FKs y constraints.
- Insertar al menos: 3 sucursales, 5 productos, 5 empleados, 20 pedidos con 1-4 líneas cada uno.
- 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.
- Crear 3 índices justificados con
EXPLAIN. - Exportar el schema completo + queries en un archivo
.sqlreproducible.
💡 Pista de arquitectura
- Usá SQLite si querés algo sin instalar (
sqlite3 pupuseria.db). - Las queries con
GROUP BY+JOINnecesitan window functions o subconsultas. Modernizá tu SQL. EXPLAIN ANALYZEes estándar en PG; en SQLite esEXPLAIN QUERY PLAN.
Para más práctica: pgexercises.com (todo en SQL, gratis).