Módulo 25: Casos prácticos de diseño lógico
Objetivos del módulo
- Aplicar de principio a fin el proceso de diseño: requisitos → E/R → esquema relacional → normalización → esquema final
- Justificar cada decisión de transformación (claves, claves ajenas, nulos, jerarquías, entidades débiles, relaciones N:M y 1:1)
- Comprobar la forma normal de cada tabla a partir de sus dependencias funcionales y corregir el diseño cuando haga falta
- Expresar el esquema final con todas sus restricciones (PK, FK con su acción ante el borrado, UK, NOT NULL y CHECK) y dibujar su grafo relacional
- Reconstruir el diseño completo del esquema EMPRESA y compararlo con su script de creación
- Conocer los fundamentos del diseño físico: tipos de datos, índices, particionamiento, estimación de volumen, desnormalización controlada y vistas
1. Metodología de los casos
Todos los casos siguen el mismo guion, que es el que deberías seguir en un proyecto real o en un examen:
flowchart LR
a["1. Enunciado y<br/>requisitos"] --> b["2. Diagrama E/R"]
b --> c["3. Transformación<br/>al relacional"]
c --> d["4. DF y forma<br/>normal de cada tabla"]
d --> e["5. Ajustes de<br/>normalización"]
e --> f["6. Esquema final<br/>con restricciones"]
f --> g["7. Grafo<br/>relacional"]
Convenciones que usaremos en el esquema final:
- Clave primaria subrayada; claves ajenas en cursiva, detalladas debajo con su acción ante el borrado.
- NO ACTION (también llamada RESTRICT): no se puede borrar la fila padre si tiene hijas. Es el comportamiento por defecto de Oracle.
- CASCADE: al borrar la fila padre se borran las hijas (
ON DELETE CASCADE). - SET NULL: al borrar la fila padre, la clave ajena de las hijas pasa a nulo (
ON DELETE SET NULL). - Los atributos que admiten nulos se marcan con (N); el resto son NOT NULL.
⚠️ Importante: Oracle solo admite acciones referenciales ante el borrado (
ON DELETE CASCADEyON DELETE SET NULL). No existeON UPDATE CASCADE: por eso las claves primarias deben ser estables (un código interno mejor que un dato que pueda cambiar, como un correo electrónico o una matrícula de coche que se re-matricula).
💡 Consejo: Cuando un requisito no se puede expresar con PK, FK, UK, NOT NULL o CHECK (por ejemplo, “un socio no puede tener más de cinco préstamos activos”), anótalo como restricción adicional. En Oracle se implementa normalmente con disparadores, que se estudian en el Curso de Oracle PL/SQL.
2. Caso 1: Biblioteca municipal
2.1 Enunciado
La red de bibliotecas de un ayuntamiento quiere informatizar su catálogo y el préstamo:
- De cada libro se guarda su ISBN, título, año de publicación y número de páginas. Cada libro lo publica una editorial (código, nombre y ciudad).
- Un libro puede tener varios autores (código, nombre y nacionalidad) y es importante el orden en que firman. Solo se registran autores con algún libro en el catálogo.
- De cada libro hay uno o varios ejemplares físicos, numerados 1, 2, 3… dentro de cada libro. De cada ejemplar se guarda la fecha de adquisición, su estado (BUENO, DETERIORADO o BAJA) y su ubicación: la estantería y la sala donde está. Cada estantería tiene un código único en toda la red y está en una sola sala.
- De cada socio se guarda número de socio, DNI (único), nombre, apellidos, correo electrónico (opcional y único), teléfono y fecha de alta.
- Un socio se lleva en préstamo un ejemplar en una fecha, con una fecha prevista de devolución; la fecha real de devolución queda vacía hasta que lo devuelve. A lo largo del tiempo, un mismo socio puede llevarse el mismo ejemplar varias veces.
- Un socio puede reservar un libro (no un ejemplar concreto) en una fecha.
- Reglas de negocio: un socio no puede tener más de cinco préstamos sin devolver, y un ejemplar no puede estar prestado dos veces a la vez.
2.2 Diagrama E/R
Primer borrador del analista (la sala y la estantería se modelaron como atributos del ejemplar):
flowchart LR
classDef debil stroke-width:4px
classDef derivado stroke-dasharray: 5 5
EDITORIAL[EDITORIAL] ---|"(1,1)"| publica{publica}
publica ---|"(0,n)"| LIBRO[LIBRO]
AUTOR[AUTOR] ---|"(1,n)"| escribe{escribe}
escribe ---|"(1,n)"| LIBRO
escribe --- esc_orden([orden])
LIBRO ---|"(1,1)"| tiene{tiene}:::debil
tiene ---|"(1,n)"| EJEMPLAR[EJEMPLAR]:::debil
SOCIO[SOCIO] ---|"(1,1)"| realiza{realiza}
realiza ---|"(0,n)"| PRESTAMO[PRESTAMO]
EJEMPLAR ---|"(1,1)"| sobre{sobre}
sobre ---|"(0,n)"| PRESTAMO
SOCIO ---|"(0,n)"| reserva{reserva}
reserva ---|"(0,n)"| LIBRO
reserva --- res_fecha(((fecha_reserva)))
LIBRO --- lib_isbn([<u>isbn</u>])
LIBRO --- lib_titulo([titulo])
LIBRO --- lib_anio([anio_publicacion])
LIBRO --- lib_pag([num_paginas])
EDITORIAL --- ed_id([<u>editorial_id</u>])
EDITORIAL --- ed_nombre([nombre])
EDITORIAL --- ed_ciudad([ciudad])
AUTOR --- au_id([<u>autor_id</u>])
AUTOR --- au_nombre([nombre])
AUTOR --- au_nac([nacionalidad])
EJEMPLAR --- ej_num([<u>num_ejemplar</u> parcial])
EJEMPLAR --- ej_fecha([fecha_adquisicion])
EJEMPLAR --- ej_estado([estado])
EJEMPLAR --- ej_est([estanteria])
EJEMPLAR --- ej_sala([sala])
SOCIO --- so_num([<u>num_socio</u>])
SOCIO --- so_dni([dni])
SOCIO --- so_nom([nombre])
SOCIO --- so_ape([apellidos])
SOCIO --- so_email(["email (0,1)"])
SOCIO --- so_tel([telefono])
SOCIO --- so_alta([fecha_alta])
PRESTAMO --- pr_id([<u>prestamo_id</u>])
PRESTAMO --- pr_fp([fecha_prestamo])
PRESTAMO --- pr_fprev([fecha_prevista])
PRESTAMO --- pr_fd(["fecha_devolucion (0,1)"])
Decisiones de modelado:
- EJEMPLAR es débil respecto de LIBRO: su número (1, 2, 3…) solo identifica dentro de cada libro. El identificador parcial
num_ejemplarse completa con el ISBN. - PRESTAMO es una entidad, no una relación N:M entre SOCIO y EJEMPLAR, porque el mismo par socio-ejemplar puede repetirse en fechas distintas (requisito 5). Se le da un identificador propio (
prestamo_id). La alternativa de identificar el préstamo por (ejemplar, fecha_prestamo) también sería válida. - reserva es N:M entre SOCIO y LIBRO; como un socio puede reservar el mismo libro en ocasiones distintas,
fecha_reservase trata como un atributo multivaluado de la relación que entrará en la clave.
2.3 Transformación al modelo relacional
| Elemento E/R | Regla | Resultado |
|---|---|---|
| EDITORIAL, AUTOR, SOCIO | Entidad → tabla | EDITORIALES, AUTORES, SOCIOS |
| LIBRO + publica (1:N) | Entidad → tabla; la clave del lado 1 pasa al lado N | LIBROS con FK editorial_id NOT NULL (participación mínima 1) |
| escribe (N:M con atributo) | Nueva tabla con las dos claves | AUTORIAS(isbn, autor_id, orden) |
| EJEMPLAR (débil) | Tabla con la clave del fuerte + identificador parcial | EJEMPLARES(isbn, num_ejemplar, …) |
| PRESTAMO + realiza + sobre | Entidad → tabla; dos relaciones 1:N → dos FK | PRESTAMOS con FK a SOCIOS y FK compuesta a EJEMPLARES |
| reserva (N:M con multivaluado) | Nueva tabla; la fecha entra en la clave | RESERVAS(num_socio, isbn, fecha_reserva) |
Primer esquema (antes de normalizar):
EJEMPLARES (*isbn*, num_ejemplar, fecha_adquisicion, estado, estanteria, sala)
(las demás tablas se muestran en el esquema final).
2.4 Dependencias funcionales y forma normal
| Tabla | DF (recubrimiento mínimo) | Claves | Forma normal |
|---|---|---|---|
| EDITORIALES | editorial_id → nombre, ciudad | editorial_id | FNBC |
| LIBROS | isbn → titulo, anio_publicacion, num_paginas, editorial_id | isbn | FNBC |
| AUTORES | autor_id → nombre, nacionalidad | autor_id | FNBC |
| AUTORIAS | isbn, autor_id → orden; isbn, orden → autor_id | (isbn, autor_id), (isbn, orden) | FNBC |
| EJEMPLARES | isbn, num_ejemplar → fecha_adquisicion, estado, estanteria; estanteria → sala | (isbn, num_ejemplar) | 2FN |
| SOCIOS | num_socio → todo; dni → num_socio; email → num_socio | num_socio, dni, email | FNBC |
| PRESTAMOS | prestamo_id → todo; isbn, num_ejemplar, fecha_prestamo → prestamo_id | prestamo_id, (isbn, num_ejemplar, fecha_prestamo) | FNBC |
| RESERVAS | (ninguna no trivial) | (num_socio, isbn, fecha_reserva) | FNBC |
Observaciones:
- En AUTORIAS hay dos claves candidatas porque dos autores no pueden ocupar la misma posición en un libro.
- En PRESTAMOS se ha supuesto que un ejemplar no se presta dos veces el mismo día; eso convierte (isbn, num_ejemplar, fecha_prestamo) en clave alternativa (UK).
- En SOCIOS,
emailes clave candidata aunque admita nulos: Oracle permite varios nulos en una columna UNIQUE.
2.5 Ajuste de normalización
EJEMPLARES no está en 3FN: estanteria → sala, con estanteria no superclave y sala no prima (dependencia transitiva desde la clave). La anomalía es clara: si una estantería se traslada de sala, hay que actualizar todos sus ejemplares. Es una entidad oculta: ESTANTERIA. Se separa:
ESTANTERIAS (estanteria_id, sala)
EJEMPLARES (*isbn*, num_ejemplar, fecha_adquisicion, estado, estanteria_id (N))
Descomposición sin pérdida (la intersección estanteria_id es clave de ESTANTERIAS) y con preservación.
2.6 Esquema relacional final
EDITORIALES (editorial_id, nombre, ciudad (N))
- UK: nombre
LIBROS (isbn, titulo, anio_publicacion (N), num_paginas (N), editorial_id)
editorial_id → EDITORIALES(editorial_id)NO ACTION- CHECK: num_paginas > 0
AUTORES (autor_id, nombre, nacionalidad (N))
AUTORIAS (*isbn*, *autor_id*, orden)
isbn → LIBROS(isbn)CASCADE (si se borra un libro del catálogo, sus autorías no tienen sentido)autor_id → AUTORES(autor_id)NO ACTION- UK: (isbn, orden); CHECK: orden ≥ 1
ESTANTERIAS (estanteria_id, sala)
EJEMPLARES (*isbn*, num_ejemplar, fecha_adquisicion, estado, estanteria_id (N))
isbn → LIBROS(isbn)NO ACTION (un libro con ejemplares no se borra: los ejemplares se dan de baja)estanteria_id → ESTANTERIAS(estanteria_id)SET NULL (si se desmonta una estantería, el ejemplar queda sin ubicar)- CHECK: estado IN (‘BUENO’, ‘DETERIORADO’, ‘BAJA’); num_ejemplar ≥ 1
SOCIOS (num_socio, dni, nombre, apellidos, email (N), telefono (N), fecha_alta)
- UK: dni; UK: email
PRESTAMOS (prestamo_id, num_socio, isbn, num_ejemplar, fecha_prestamo, fecha_prevista, fecha_devolucion (N))
num_socio → SOCIOS(num_socio)NO ACTION (el historial de préstamos impide borrar socios)(isbn, num_ejemplar) → EJEMPLARES(isbn, num_ejemplar)NO ACTION- UK: (isbn, num_ejemplar, fecha_prestamo)
- CHECK: fecha_prevista ≥ fecha_prestamo; fecha_devolucion ≥ fecha_prestamo
RESERVAS (*num_socio*, *isbn*, fecha_reserva)
num_socio → SOCIOS(num_socio)CASCADE;isbn → LIBROS(isbn)CASCADE
Restricciones adicionales (no expresables con las anteriores): máximo cinco préstamos con fecha_devolucion nula por socio; un ejemplar no puede tener dos préstamos sin devolver; no se presta un ejemplar en estado BAJA.
2.7 Grafo relacional
flowchart LR
LIBROS -->|editorial_id| EDITORIALES
AUTORIAS -->|isbn| LIBROS
AUTORIAS -->|autor_id| AUTORES
EJEMPLARES -->|isbn| LIBROS
EJEMPLARES -->|estanteria_id| ESTANTERIAS
PRESTAMOS -->|num_socio| SOCIOS
PRESTAMOS -->|"isbn, num_ejemplar"| EJEMPLARES
RESERVAS -->|num_socio| SOCIOS
RESERVAS -->|isbn| LIBROS
3. Caso 2: Taller mecánico
3.1 Enunciado
Un taller quiere gestionar sus reparaciones (es una ampliación del ejercicio 1 del módulo 19):
- Clientes: DNI, nombre, teléfono y correo (opcional).
- Vehículos: matrícula, número de bastidor (único), color, año, marca y modelo. Cada vehículo pertenece a un cliente; un cliente puede tener varios. Cada modelo es de una marca, y puede haber modelos con el mismo nombre en marcas distintas. Para pedir recambios se necesita saber el tipo de combustible, que depende del modelo.
- Reparaciones: número (único), fecha de entrada, fecha de salida (vacía mientras está en el taller), kilómetros y descripción de la avería. Cada reparación es de un vehículo.
- Mecánicos: código, nombre, especialidad (MOTOR, ELECTRICIDAD, CARROCERIA, GENERAL) y precio por hora actual. En una reparación trabajan uno o varios mecánicos; se anotan las horas de cada uno y el precio por hora aplicado (el precio del mecánico puede subir con el tiempo).
- Piezas: referencia, descripción, precio actual y stock. En una reparación se usan varias piezas; se anota la cantidad y el precio aplicado.
- Facturas: cuando termina, cada reparación genera exactamente una factura, con número propio (serie legal correlativa), fecha y forma de pago (EFECTIVO, TARJETA, TRANSFERENCIA).
3.2 Diagrama E/R
flowchart LR
CLIENTE[CLIENTE] ---|"(1,1)"| posee{posee}
posee ---|"(1,n)"| VEHICULO[VEHICULO]
VEHICULO ---|"(1,1)"| sufre{sufre}
sufre ---|"(0,n)"| REPARACION[REPARACION]
MECANICO[MECANICO] ---|"(1,n)"| trabaja{trabaja}
trabaja ---|"(0,n)"| REPARACION
trabaja --- tr_horas([horas])
trabaja --- tr_precio([precio_hora_aplicado])
REPARACION ---|"(0,n)"| usa{usa}
usa ---|"(0,n)"| PIEZA[PIEZA]
usa --- us_cant([cantidad])
usa --- us_precio([precio_aplicado])
REPARACION ---|"(1,1)"| genera{genera}
genera ---|"(0,1)"| FACTURA[FACTURA]
CLIENTE --- cl_dni([<u>dni</u>])
CLIENTE --- cl_nom([nombre])
CLIENTE --- cl_tel([telefono])
CLIENTE --- cl_email(["email (0,1)"])
VEHICULO --- ve_mat([<u>matricula</u>])
VEHICULO --- ve_bas([bastidor])
VEHICULO --- ve_col([color])
VEHICULO --- ve_anio([anio])
VEHICULO --- ve_marca([marca])
VEHICULO --- ve_modelo([modelo])
VEHICULO --- ve_comb([combustible])
REPARACION --- re_num([<u>num_reparacion</u>])
REPARACION --- re_fe([fecha_entrada])
REPARACION --- re_fs(["fecha_salida (0,1)"])
REPARACION --- re_km([km])
REPARACION --- re_av([averia])
MECANICO --- me_cod([<u>cod_mecanico</u>])
MECANICO --- me_nom([nombre])
MECANICO --- me_esp([especialidad])
MECANICO --- me_ph([precio_hora])
PIEZA --- pi_ref([<u>referencia</u>])
PIEZA --- pi_desc([descripcion])
PIEZA --- pi_precio([precio])
PIEZA --- pi_stock([stock])
FACTURA --- fa_num([<u>num_factura</u>])
FACTURA --- fa_fecha([fecha])
FACTURA --- fa_pago([forma_pago])
En este primer diagrama, la marca, el modelo y el combustible aparecen como simples atributos de VEHICULO; la normalización mostrará que no es una buena idea. Lectura de la relación 1:1 genera: cada factura corresponde a exactamente una reparación (1,1) y cada reparación tiene como mucho una factura (0,1), porque mientras está en el taller aún no se ha facturado.
3.3 Transformación
| Elemento | Decisión | Justificación |
|---|---|---|
| posee (1:N) | FK dni_cliente NOT NULL en VEHICULOS | Todo vehículo tiene propietario |
| sufre (1:N) | FK matricula NOT NULL en REPARACIONES | |
| trabaja (N:M) | Tabla TRABAJOS(num_reparacion, cod_mecanico, horas, precio_hora_aplicado) | N:M con atributos |
| usa (N:M) | Tabla CONSUMOS(num_reparacion, referencia, cantidad, precio_aplicado) | N:M con atributos |
| genera (1:1, (1,1)-(0,1)) | FK num_reparacion en FACTURAS, NOT NULL y UNIQUE | La FK va al lado opcional para no tener nulos; UNIQUE garantiza el 1:1 |
| Clave de VEHICULO | matricula como PK, bastidor como UK | Un vehículo puede re-matricularse; si fuera frecuente convendría una clave interna (vehiculo_id), ya que Oracle no propaga cambios de PK |
La alternativa de fusionar FACTURA y REPARACION en una sola tabla se descarta: la factura tiene su propia numeración legal y sus atributos serían nulos durante toda la reparación.
3.4 Dependencias funcionales y forma normal
Primer borrador de VEHICULOS:
VEHICULOS (matricula, bastidor, color, anio, marca, modelo, combustible, dni_cliente)
matricula → bastidor, color, anio, marca, modelo, combustible, dni_cliente
bastidor → matricula
marca, modelo → combustible
Observa que modelo ↛ marca (hay modelos homónimos en marcas distintas): el modelo se identifica por el par (marca, modelo). Claves candidatas: matricula y bastidor. La DF marca, modelo → combustible tiene un determinante que no es superclave y un implicado no primo: es una dependencia transitiva (matricula → marca, modelo → combustible) y VEHICULOS no está en 3FN. Si un fabricante corrige el combustible de un modelo, habría que cambiar todos sus vehículos.
Ajuste: el par (marca, modelo) es una entidad oculta. Se crea MODELOS con una clave sustituta modelo_id y UK (marca_id, nombre), y de paso MARCAS, para evitar incoherencias de texto libre (“Seat” frente a “SEAT”). VEHICULOS conserva solo modelo_id.
| Tabla | DF | Claves | FN |
|---|---|---|---|
| TRABAJOS | num_reparacion, cod_mecanico → horas, precio_hora_aplicado | (num_reparacion, cod_mecanico) | FNBC |
| CONSUMOS | num_reparacion, referencia → cantidad, precio_aplicado | (num_reparacion, referencia) | FNBC |
| FACTURAS | num_factura → todo; num_reparacion → num_factura | num_factura, num_reparacion | FNBC |
| MECANICOS | cod_mecanico → nombre, especialidad, precio_hora | cod_mecanico | FNBC |
⚠️ Importante:
precio_hora_aplicadoen TRABAJOS no es redundante conMECANICOS.precio_hora: el primero es el precio histórico de esa reparación y el segundo el actual. No existe la DFcod_mecanico → precio_hora_aplicado. Lo mismo ocurre conprecio_aplicadoen CONSUMOS. Es el mismo patrón queprecio_unitarioen LINEAS_PEDIDO del esquema EMPRESA.
💡 Consejo: El importe de la factura no se guarda: se calcula sumando horas × precio_hora_aplicado de TRABAJOS y cantidad × precio_aplicado de CONSUMOS. Si por rendimiento o por exigencia legal se quisiera congelar, sería una desnormalización controlada.
3.5 Esquema relacional final
CLIENTES (dni, nombre, telefono, email (N))
- UK: email
MARCAS (marca_id, nombre)
- UK: nombre
MODELOS (modelo_id, marca_id, nombre, combustible (N))
marca_id → MARCAS(marca_id)NO ACTION- UK: (marca_id, nombre); CHECK: combustible IN (‘GASOLINA’, ‘DIESEL’, ‘HIBRIDO’, ‘ELECTRICO’)
VEHICULOS (matricula, bastidor, color (N), anio (N), modelo_id, dni_cliente)
modelo_id → MODELOS(modelo_id)NO ACTION;dni_cliente → CLIENTES(dni)NO ACTION- UK: bastidor; CHECK: anio ≥ 1900
REPARACIONES (num_reparacion, matricula, fecha_entrada, fecha_salida (N), km, averia)
matricula → VEHICULOS(matricula)NO ACTION- CHECK: fecha_salida ≥ fecha_entrada; km ≥ 0
MECANICOS (cod_mecanico, nombre, especialidad, precio_hora)
- CHECK: especialidad IN (‘MOTOR’, ‘ELECTRICIDAD’, ‘CARROCERIA’, ‘GENERAL’); precio_hora > 0
TRABAJOS (*num_reparacion*, *cod_mecanico*, horas, precio_hora_aplicado)
num_reparacion → REPARACIONES(num_reparacion)CASCADE;cod_mecanico → MECANICOS(cod_mecanico)NO ACTION- CHECK: horas > 0; precio_hora_aplicado > 0
PIEZAS (referencia, descripcion, precio, stock)
- CHECK: precio ≥ 0; stock ≥ 0
CONSUMOS (*num_reparacion*, *referencia*, cantidad, precio_aplicado)
num_reparacion → REPARACIONES(num_reparacion)CASCADE;referencia → PIEZAS(referencia)NO ACTION- CHECK: cantidad > 0; precio_aplicado ≥ 0
FACTURAS (num_factura, num_reparacion, fecha, forma_pago)
num_reparacion → REPARACIONES(num_reparacion)NO ACTION (una reparación facturada no se puede borrar: la factura es un documento legal)- UK: num_reparacion; CHECK: forma_pago IN (‘EFECTIVO’, ‘TARJETA’, ‘TRANSFERENCIA’)
Observa la combinación de acciones: TRABAJOS y CONSUMOS se borran en cascada con su reparación, pero FACTURAS bloquea el borrado. Resultado: solo se pueden borrar reparaciones no facturadas (por ejemplo, un presupuesto rechazado), y entonces se llevan por delante sus trabajos y consumos.
Restricciones adicionales: solo se factura una reparación con fecha_salida no nula; la fecha de la factura no puede ser anterior a la salida; al usar una pieza debe haber stock suficiente.
3.6 Grafo relacional
flowchart LR
MODELOS -->|marca_id| MARCAS
VEHICULOS -->|modelo_id| MODELOS
VEHICULOS -->|dni_cliente| CLIENTES
REPARACIONES -->|matricula| VEHICULOS
TRABAJOS -->|num_reparacion| REPARACIONES
TRABAJOS -->|cod_mecanico| MECANICOS
CONSUMOS -->|num_reparacion| REPARACIONES
CONSUMOS -->|referencia| PIEZAS
FACTURAS -->|"num_reparacion (UK)"| REPARACIONES
4. Caso 3: Hospital
4.1 Enunciado
- Pacientes: número de historia clínica, DNI (único, pero los recién nacidos aún no tienen), nombre, apellidos, fecha de nacimiento y uno o varios teléfonos de contacto.
- Personal sanitario: código de empleado, nombre, apellidos y fecha de contratación. Todo el personal es médico o enfermero, nunca ambas cosas. De los médicos se guarda el número de colegiado (único) y su especialidad (código y nombre; cada médico tiene una). De los enfermeros, el turno (MAÑANA, TARDE, NOCHE) y la planta a la que están asignados.
- Plantas (número y nombre) y habitaciones: cada habitación se numera dentro de su planta (la 1 de la planta 3, la 2 de la planta 3…) y tiene un número de camas.
- Ingresos: un paciente ingresa en una habitación en una fecha, a cargo de un médico responsable; la fecha de alta se registra al salir. Un paciente puede ingresar muchas veces a lo largo de su vida.
- Citas de consultas externas: un médico atiende a un paciente en una fecha y hora, con un motivo. Un médico no atiende a dos pacientes a la vez, ni un paciente tiene dos citas a la vez. Cada día, cada médico pasa consulta en un único despacho.
4.2 Diagrama E/R
Primera parte: personal y jerarquía.
flowchart TB
PERSONAL[PERSONAL] --- j1{{"es un<br/>(total, exclusiva)"}}
j1 --- MEDICO[MEDICO]
j1 --- ENFERMERO[ENFERMERO]
PERSONAL --- pe_cod([<u>cod_empleado</u>])
PERSONAL --- pe_nom([nombre])
PERSONAL --- pe_ape([apellidos])
PERSONAL --- pe_fc([fecha_contrato])
MEDICO --- me_col([num_colegiado])
ENFERMERO --- en_turno([turno])
MEDICO ---|"(0,n)"| tiene_esp{tiene}
tiene_esp ---|"(1,1)"| ESPECIALIDAD[ESPECIALIDAD]
ENFERMERO ---|"(0,n)"| asignado{asignado}
asignado ---|"(1,1)"| PLANTA[PLANTA]
ESPECIALIDAD --- es_cod([<u>cod_especialidad</u>])
ESPECIALIDAD --- es_nom([nombre])
⚠️ Importante: Fíjate en dónde va cada pareja. Un médico tiene exactamente una especialidad, así que el (1,1) va junto a ESPECIALIDAD; una especialidad tiene de 0 a n médicos, así que el (0,n) va junto a MEDICO. Colocarlas al revés es el fallo más frecuente con la convención del curso (módulo 06). Antes de transformar, relee cada pareja con la frase “una ocurrencia de la otra entidad se relaciona con (mín, máx) ocurrencias de esta”.
Segunda parte: pacientes, ingresos y citas.
flowchart LR
classDef debil stroke-width:4px
PLANTA[PLANTA] ---|"(1,1)"| contiene{contiene}:::debil
contiene ---|"(1,n)"| HABITACION[HABITACION]:::debil
PACIENTE[PACIENTE] ---|"(1,1)"| ingresa{ingresa}
ingresa ---|"(0,n)"| INGRESO[INGRESO]
HABITACION ---|"(1,1)"| aloja{aloja}
aloja ---|"(0,n)"| INGRESO
MEDICO[MEDICO] ---|"(1,1)"| responsable{responsable}
responsable ---|"(0,n)"| INGRESO
MEDICO ---|"(0,n)"| cita{cita}
cita ---|"(0,n)"| PACIENTE
cita --- ci_fh(((fecha_hora)))
cita --- ci_mot([motivo])
cita --- ci_desp([despacho])
PLANTA --- pl_num([<u>num_planta</u>])
PLANTA --- pl_nom([nombre])
HABITACION --- ha_num([<u>num_habitacion</u> parcial])
HABITACION --- ha_camas([num_camas])
PACIENTE --- pa_nh([<u>num_historia</u>])
PACIENTE --- pa_dni(["dni (0,1)"])
PACIENTE --- pa_nom([nombre])
PACIENTE --- pa_ape([apellidos])
PACIENTE --- pa_fn([fecha_nac])
PACIENTE --- pa_tel(((telefono)))
INGRESO --- in_id([<u>ingreso_id</u>])
INGRESO --- in_fi([fecha_ingreso])
INGRESO --- in_fa(["fecha_alta (0,1)"])
4.3 Transformación
| Elemento | Decisión | Justificación |
|---|---|---|
| Jerarquía PERSONAL (total, exclusiva) | Tres tablas: PERSONAL, MEDICOS y ENFERMEROS con la misma PK, que en los subtipos es también FK | Los subtipos tienen atributos y relaciones propias (solo MEDICO participa en citas e ingresos; solo ENFERMERO en asignado). Una sola tabla tendría muchas columnas nulas y no podría declarar las FK como NOT NULL |
| Teléfonos (multivaluado) | Tabla TELEFONOS_PACIENTE | 1FN |
| HABITACION (débil) | PK (num_planta, num_habitacion) | Identificador parcial + clave del fuerte |
| INGRESO | Entidad con clave propia | Un paciente ingresa muchas veces |
| cita (N:M con fecha_hora multivaluado) | Tabla CITAS con (cod_medico, fecha, hora) como PK | La fecha y hora diferencian las citas repetidas |
dni de paciente | UK que admite nulos | Los recién nacidos aún no lo tienen |
Con tres tablas para la jerarquía no se garantiza la cobertura total ni la exclusividad: nada impide que un código esté en MEDICOS y en ENFERMEROS a la vez, ni que esté solo en PERSONAL. Una técnica declarativa para la exclusividad es añadir a PERSONAL una columna discriminante tipo (‘M’ o ‘E’) con UK (cod_empleado, tipo), y en cada subtipo una columna tipo fija con CHECK (tipo = ‘M’) y una FK compuesta (cod_empleado, tipo) → PERSONAL(cod_empleado, tipo). La totalidad (que todo empleado tenga fila en su subtipo) requiere ya un disparador.
4.4 Dependencias funcionales y forma normal
El primer esquema de CITAS, obtenido directamente de la relación cita, es:
CITAS (*cod_medico*, fecha, hora, num_historia, motivo, despacho)
Sus DF, según el requisito 5:
cod_medico, fecha, hora → num_historia, motivo, despacho
num_historia, fecha, hora → cod_medico
cod_medico, fecha → despacho
Claves: {cod_medico, fecha, hora} y {num_historia, fecha, hora}. La DF cod_medico, fecha → despacho es parcial (despacho no es primo y depende de una parte de una clave): CITAS está en 1FN pero no en 2FN. El despacho se repetiría en todas las citas del médico ese día. El resto de tablas:
| Tabla | DF relevantes | Claves | FN |
|---|---|---|---|
| PACIENTES | num_historia → todo; dni → num_historia | num_historia, dni | FNBC |
| PERSONAL / MEDICOS / ENFERMEROS | cod_empleado → todo; num_colegiado → cod_empleado | cod_empleado (y num_colegiado en MEDICOS) | FNBC |
| HABITACIONES | num_planta, num_habitacion → num_camas | (num_planta, num_habitacion) | FNBC |
| INGRESOS | ingreso_id → todo | ingreso_id | FNBC |
4.5 Ajuste de normalización
Se separa el despacho en una tabla de turnos de consulta (es el ejercicio 11 del módulo 20):
TURNOS_CONSULTA (*cod_medico*, fecha, despacho)
CITAS (*cod_medico*, fecha, hora, num_historia, motivo), con UK (num_historia, fecha, hora) y FK (cod_medico, fecha) → TURNOS_CONSULTA.
La FK aporta además una regla de negocio gratis: no se puede dar una cita a un médico un día que no pasa consulta.
4.6 Esquema relacional final
ESPECIALIDADES (cod_especialidad, nombre)
- UK: nombre
PLANTAS (num_planta, nombre)
HABITACIONES (*num_planta*, num_habitacion, num_camas)
num_planta → PLANTAS(num_planta)CASCADE- CHECK: num_camas BETWEEN 1 AND 4
PERSONAL (cod_empleado, nombre, apellidos, fecha_contrato)
MEDICOS (*cod_empleado*, num_colegiado, cod_especialidad)
cod_empleado → PERSONAL(cod_empleado)CASCADEcod_especialidad → ESPECIALIDADES(cod_especialidad)NO ACTION- UK: num_colegiado
ENFERMEROS (*cod_empleado*, turno, num_planta)
cod_empleado → PERSONAL(cod_empleado)CASCADEnum_planta → PLANTAS(num_planta)NO ACTION- CHECK: turno IN (‘MAÑANA’, ‘TARDE’, ‘NOCHE’)
PACIENTES (num_historia, dni (N), nombre, apellidos, fecha_nac)
- UK: dni
TELEFONOS_PACIENTE (*num_historia*, telefono)
num_historia → PACIENTES(num_historia)CASCADE
INGRESOS (ingreso_id, num_historia, num_planta, num_habitacion, cod_medico, fecha_ingreso, fecha_alta (N))
num_historia → PACIENTES(num_historia)NO ACTION(num_planta, num_habitacion) → HABITACIONES(num_planta, num_habitacion)NO ACTIONcod_medico → MEDICOS(cod_empleado)NO ACTION- CHECK: fecha_alta ≥ fecha_ingreso
TURNOS_CONSULTA (*cod_medico*, fecha, despacho)
cod_medico → MEDICOS(cod_empleado)CASCADE
CITAS (*cod_medico*, *fecha*, hora, num_historia, motivo (N))
(cod_medico, fecha) → TURNOS_CONSULTA(cod_medico, fecha)CASCADEnum_historia → PACIENTES(num_historia)CASCADE- UK: (num_historia, fecha, hora)
¿Por qué CASCADE de PERSONAL a MEDICOS? Porque el subtipo no tiene sentido sin el supertipo. Pero como MEDICOS es referenciado por INGRESOS con NO ACTION, en la práctica no se podrá borrar a un médico con ingresos a su cargo: Oracle comprueba toda la cadena.
Restricciones adicionales: un paciente no puede tener dos ingresos sin alta a la vez; el número de pacientes ingresados a la vez en una habitación no puede superar sus camas; exclusividad y totalidad de la jerarquía (ver 4.3).
4.7 Grafo relacional
flowchart LR
MEDICOS -->|cod_empleado| PERSONAL
ENFERMEROS -->|cod_empleado| PERSONAL
MEDICOS -->|cod_especialidad| ESPECIALIDADES
ENFERMEROS -->|num_planta| PLANTAS
HABITACIONES -->|num_planta| PLANTAS
TELEFONOS_PACIENTE -->|num_historia| PACIENTES
INGRESOS -->|num_historia| PACIENTES
INGRESOS -->|"num_planta, num_habitacion"| HABITACIONES
INGRESOS -->|cod_medico| MEDICOS
TURNOS_CONSULTA -->|cod_medico| MEDICOS
CITAS -->|"cod_medico, fecha"| TURNOS_CONSULTA
CITAS -->|num_historia| PACIENTES
5. Caso 4: Liga deportiva
5.1 Enunciado
Una federación gestiona una liga de fútbol:
- Cada temporada (por ejemplo, “2026-27”) tiene una fecha de inicio y de fin.
- Equipos: código, nombre (único), ciudad, año de fundación y estadio en el que juegan como local. De cada estadio se guarda nombre, ciudad y aforo; dos equipos pueden compartir estadio.
- Jugadores: código, nombre, fecha de nacimiento, nacionalidad y posición. En cada temporada, un jugador juega en un único equipo con un dorsal; dos jugadores del mismo equipo no pueden tener el mismo dorsal en la misma temporada.
- Partidos: se disputan en jornadas; en cada jornada un equipo juega un solo partido. En cada partido hay un equipo local y otro visitante (distintos), un árbitro, una fecha y el resultado oficial (goles de cada equipo, según el acta). Los partidos se juegan siempre en el estadio del equipo local. Liga a doble vuelta: cada pareja de equipos se enfrenta una vez en cada campo.
- Goles: de cada gol se guarda el minuto, el jugador que lo marca y el tipo (NORMAL, PENALTI, PROPIA).
- Árbitros: código, nombre y colegio arbitral.
5.2 Diagrama E/R
flowchart LR
classDef debil stroke-width:4px
EQUIPO[EQUIPO] ---|"(0,n)"| juega_en{juega_en}
juega_en ---|"(1,1)"| ESTADIO[ESTADIO]
EQUIPO ---|"(1,1) local"| es_local{local}
es_local ---|"(0,n)"| PARTIDO[PARTIDO]
EQUIPO ---|"(1,1) visitante"| es_visit{visitante}
es_visit ---|"(0,n)"| PARTIDO
ARBITRO[ARBITRO] ---|"(1,1)"| arbitra{arbitra}
arbitra ---|"(0,n)"| PARTIDO
TEMPORADA[TEMPORADA] ---|"(1,1)"| de_temp{pertenece}
de_temp ---|"(0,n)"| PARTIDO
PARTIDO ---|"(1,1)"| tiene_gol{tiene}:::debil
tiene_gol ---|"(0,n)"| GOL[GOL]:::debil
JUGADOR[JUGADOR] ---|"(1,1)"| marca{marca}
marca ---|"(0,n)"| GOL
JUGADOR ---|"(0,n)"| ficha{ficha}
EQUIPO ---|"(0,1)"| ficha
TEMPORADA ---|"(0,n)"| ficha
ficha --- fi_dorsal([dorsal])
PARTIDO --- pa_id([<u>partido_id</u>])
PARTIDO --- pa_jor([jornada])
PARTIDO --- pa_fecha([fecha])
PARTIDO --- pa_gl([goles_local])
PARTIDO --- pa_gv([goles_visitante])
PARTIDO --- pa_est([estadio])
GOL --- go_num([<u>num_gol</u> parcial])
GOL --- go_min([minuto])
GOL --- go_tipo([tipo])
EQUIPO --- eq_id([<u>equipo_id</u>])
EQUIPO --- eq_nom([nombre])
JUGADOR --- ju_id([<u>jugador_id</u>])
TEMPORADA --- te_id([<u>temporada_id</u>])
(Se omiten algunos atributos simples para no sobrecargar el diagrama.) Puntos clave:
- Dos relaciones entre EQUIPO y PARTIDO, con papeles local y visitante.
- ficha es una relación ternaria JUGADOR-EQUIPO-TEMPORADA. La cardinalidad (0,1) junto a EQUIPO expresa que, para un par jugador-temporada, hay como mucho un equipo: es una ternaria N:1:N.
- GOL es débil respecto de PARTIDO (se numeran 1, 2, 3… en cada partido).
- El borrador incluyó
estadiocomo atributo de PARTIDO.
5.3 Transformación
| Elemento | Resultado | Justificación |
|---|---|---|
| juega_en (N:1) | EQUIPOS.estadio_id NOT NULL | |
| local, visitante | Dos FK en PARTIDOS: equipo_local, equipo_visitante | Una FK por papel |
| arbitra, pertenece | FK arbitro_id, temporada_id | |
| ficha (ternaria N:1:N) | FICHAS(jugador_id, temporada_id, equipo_id, dorsal) | La clave de una ternaria con un lado 1 es la de los otros dos lados |
| GOL (débil) | GOLES(partido_id, num_gol, minuto, tipo, jugador_id) |
5.4 Dependencias funcionales y forma normal
PARTIDOS (borrador): (partido_id, temporada_id, jornada, fecha, equipo_local, equipo_visitante, arbitro_id, goles_local, goles_visitante, estadio)
partido_id → todos los demás
temporada_id, jornada, equipo_local → partido_id
temporada_id, jornada, equipo_visitante → partido_id
temporada_id, equipo_local, equipo_visitante → partido_id (doble vuelta)
equipo_local → estadio (requisito 4)
Claves candidatas: partido_id, {temporada_id, jornada, equipo_local}, {temporada_id, jornada, equipo_visitante} y {temporada_id, equipo_local, equipo_visitante}. ¡Cuatro! La DF equipo_local → estadio tiene un determinante que no es superclave y estadio no es primo: no está en 3FN (es transitiva: partido_id → equipo_local → estadio).
FICHAS: jugador_id, temporada_id → equipo_id, dorsal y equipo_id, temporada_id, dorsal → jugador_id. Claves: {jugador_id, temporada_id} y {equipo_id, temporada_id, dorsal}. Los dos determinantes son clave: FNBC.
GOLES: partido_id, num_gol → minuto, tipo, jugador_id. FNBC.
5.5 Ajustes de normalización
- Eliminar
estadiode PARTIDOS: se obtiene a través del equipo local (EQUIPOS.estadio_id). Si el reglamento admitiera excepciones (campo neutral en una final, cierre del estadio por sanción), la DF no existiría y la columna debería quedarse en PARTIDOS: la decisión depende exclusivamente del requisito. - ¿Son derivados los goles del partido? Podrían calcularse contando GOLES y viendo el equipo de cada goleador en FICHAS (cuidado con los goles en propia puerta). Pero el requisito dice que el resultado oficial es el del acta, y el detalle de goles puede no estar completo. Se mantienen
goles_localygoles_visitantecomo datos propios, no derivados. Si se decidiera derivarlos, guardarlos sería una desnormalización.
5.6 Esquema relacional final
TEMPORADAS (temporada_id, fecha_inicio, fecha_fin)
- CHECK: fecha_fin > fecha_inicio
ESTADIOS (estadio_id, nombre, ciudad, aforo)
- CHECK: aforo > 0
EQUIPOS (equipo_id, nombre, ciudad, anio_fundacion (N), estadio_id)
estadio_id → ESTADIOS(estadio_id)NO ACTION- UK: nombre
JUGADORES (jugador_id, nombre, fecha_nac, nacionalidad, posicion)
- CHECK: posicion IN (‘PORTERO’, ‘DEFENSA’, ‘CENTROCAMPISTA’, ‘DELANTERO’)
ARBITROS (arbitro_id, nombre, colegio)
FICHAS (*jugador_id*, *temporada_id*, equipo_id, dorsal)
jugador_id → JUGADORESNO ACTION;temporada_id → TEMPORADASNO ACTION;equipo_id → EQUIPOSNO ACTION- UK: (equipo_id, temporada_id, dorsal); CHECK: dorsal BETWEEN 1 AND 99
PARTIDOS (partido_id, temporada_id, jornada, fecha, equipo_local, equipo_visitante, arbitro_id, goles_local (N), goles_visitante (N))
temporada_id → TEMPORADASNO ACTION;equipo_local → EQUIPOS(equipo_id)NO ACTION;equipo_visitante → EQUIPOS(equipo_id)NO ACTION;arbitro_id → ARBITROSNO ACTION- UK: (temporada_id, jornada, equipo_local); UK: (temporada_id, jornada, equipo_visitante); UK: (temporada_id, equipo_local, equipo_visitante)
- CHECK: equipo_local <> equipo_visitante; goles_local ≥ 0; goles_visitante ≥ 0; jornada ≥ 1
GOLES (*partido_id*, num_gol, jugador_id, minuto, tipo)
partido_id → PARTIDOSCASCADE;jugador_id → JUGADORESNO ACTION- CHECK: minuto BETWEEN 1 AND 130; tipo IN (‘NORMAL’, ‘PENALTI’, ‘PROPIA’)
Los goles se dejan nulos mientras el partido no se ha jugado. Restricciones adicionales: un equipo no puede jugar como local en un partido y como visitante en otro de la misma jornada (las UK solo lo impiden dentro del mismo papel); el goleador debe tener ficha esa temporada en uno de los dos equipos; la fecha del partido debe estar dentro de la temporada.
5.7 Grafo relacional
flowchart LR
EQUIPOS -->|estadio_id| ESTADIOS
FICHAS -->|jugador_id| JUGADORES
FICHAS -->|temporada_id| TEMPORADAS
FICHAS -->|equipo_id| EQUIPOS
PARTIDOS -->|temporada_id| TEMPORADAS
PARTIDOS -->|equipo_local| EQUIPOS
PARTIDOS -->|equipo_visitante| EQUIPOS
PARTIDOS -->|arbitro_id| ARBITROS
GOLES -->|partido_id| PARTIDOS
GOLES -->|jugador_id| JUGADORES
6. Caso 5: Plataforma de vídeo en streaming
6.1 Enunciado
- Cuentas: identificador, correo (único), hash de la contraseña, país y fecha de alta.
- Planes: código, nombre (único), precio mensual actual, calidad máxima (HD, FULLHD, 4K) y número de pantallas simultáneas. Una cuenta tiene a lo largo del tiempo varias suscripciones, cada una a un plan, con fecha de inicio, fecha de fin (vacía si está activa) y el precio mensual que paga (que se mantiene aunque el plan suba).
- Cada cuenta tiene de 1 a 5 perfiles, numerados dentro de la cuenta, con nombre y si es infantil.
- Contenidos: identificador, título, año y edad mínima recomendada (0, 7, 12, 16, 18). Cada contenido es una película (con duración) o una serie (con estado EN_EMISION o FINALIZADA). Las series tienen temporadas (numeradas dentro de la serie, con año) y las temporadas, episodios (numerados dentro de la temporada, con título y duración).
- Cada contenido tiene uno o varios géneros (código y nombre).
- Visualizaciones: un perfil ve una película o un episodio en un momento dado, y se guarda hasta qué segundo llegó.
- Valoraciones: un perfil puede valorar un contenido de 1 a 5, una sola vez (se puede cambiar).
6.2 Diagrama E/R
flowchart TB
classDef debil stroke-width:4px
CUENTA[CUENTA] ---|"(1,1)"| tiene_p{tiene}:::debil
tiene_p ---|"(1,5)"| PERFIL[PERFIL]:::debil
CUENTA ---|"(0,n)"| suscribe{suscribe}
suscribe ---|"(0,n)"| PLAN[PLAN]
suscribe --- su_fi(((fecha_inicio)))
suscribe --- su_ff(["fecha_fin (0,1)"])
suscribe --- su_precio([precio_pagado])
CONTENIDO[CONTENIDO] --- j1{{"es un<br/>(total, exclusiva)"}}
j1 --- PELICULA[PELICULA]
j1 --- SERIE[SERIE]
SERIE ---|"(1,1)"| tiene_t{tiene}:::debil
tiene_t ---|"(1,n)"| TEMPORADA[TEMPORADA]:::debil
TEMPORADA ---|"(1,1)"| tiene_e{tiene}:::debil
tiene_e ---|"(1,n)"| EPISODIO[EPISODIO]:::debil
CONTENIDO ---|"(1,n)"| clasifica{clasifica}
clasifica ---|"(1,n)"| GENERO[GENERO]
PERFIL ---|"(0,n)"| valora{valora}
valora ---|"(0,n)"| CONTENIDO
valora --- va_punt([puntuacion])
PERFIL ---|"(0,n)"| ve_peli{ve}
ve_peli ---|"(0,n)"| PELICULA
PERFIL ---|"(0,n)"| ve_epi{ve}
ve_epi ---|"(0,n)"| EPISODIO
PERFIL --- pf_num([<u>num_perfil</u> parcial])
TEMPORADA --- te_num([<u>num_temporada</u> parcial])
EPISODIO --- ep_num([<u>num_episodio</u> parcial])
CONTENIDO --- co_id([<u>contenido_id</u>])
CUENTA --- cu_id([<u>cuenta_id</u>])
Las dos relaciones ve comparten atributos (momento, segundo_parada), omitidos en el dibujo. Una cadena de debilidad CUENTA → PERFIL y SERIE → TEMPORADA → EPISODIO hace que la clave de EPISODIO sea (serie, num_temporada, num_episodio).
6.3 Transformación y decisiones
| Elemento | Decisión | Justificación |
|---|---|---|
| Jerarquía CONTENIDO | Supertipo + subtipos (tres tablas) | CONTENIDO participa en clasifica y valora; PELICULA y SERIE tienen relaciones propias |
| suscribe (N:M con fecha multivaluada) | SUSCRIPCIONES(cuenta_id, fecha_inicio, plan_id, …) | Una cuenta no inicia dos suscripciones el mismo día; el plan no forma parte de la clave |
| PERFIL débil | PK (cuenta_id, num_perfil) | |
| TEMPORADA, EPISODIO débiles | PK (serie_id, num_temporada) y (serie_id, num_temporada, num_episodio) | Cadena de identificación |
| Dos relaciones ve | Una tabla VISUALIZACIONES con dos referencias opcionales y un CHECK de exclusividad (arco exclusivo) | Comparten atributos y casi todas las consultas (“¿qué ha visto este perfil?”) las necesitan juntas |
Alternativas para ve y su valoración:
| Opción | Ventajas | Inconvenientes |
|---|---|---|
| Dos tablas (VER_PELICULAS, VER_EPISODIOS) | FK NOT NULL, sin CHECK especial | Consultas con UNION para el historial |
| Una tabla con arco exclusivo | Historial en una sola tabla | Columnas nulas y un CHECK |
| Supertipo VIDEO (película o episodio) | Una sola FK NOT NULL | Cambia la jerarquía: los episodios no son contenidos |
6.4 Dependencias funcionales y forma normal
El borrador de VISUALIZACIONES copiaba, “para no tener que hacer joins”, el título y la duración:
VISUALIZACIONES (visualizacion_id, cuenta_id, num_perfil, pelicula_id, serie_id, num_temporada, num_episodio, momento, segundo_parada, titulo, duracion)
Con pelicula_id → titulo, duracion (vía PELICULAS y CONTENIDOS) y serie_id, num_temporada, num_episodio → titulo, duracion, hay dependencias transitivas: no está en 3FN. Al cambiar el título de un episodio, habría millones de filas desactualizadas. Ajuste: eliminar titulo y duracion.
Otra comprobación importante, en SUSCRIPCIONES:
cuenta_id, fecha_inicio → plan_id, fecha_fin, precio_pagado
plan_id ↛ precio_pagado (el precio pagado es el del momento de contratar)
Al no existir plan_id → precio_pagado, no hay dependencia transitiva: FNBC. Si se hubiera copiado calidad_max del plan en la suscripción, sí la habría (plan_id → calidad_max).
| Tabla | Claves | FN |
|---|---|---|
| CUENTAS | cuenta_id, email | FNBC |
| PLANES | plan_id, nombre | FNBC |
| PERFILES | (cuenta_id, num_perfil) | FNBC |
| CONTENIDOS, PELICULAS, SERIES | contenido_id | FNBC |
| TEMPORADAS, EPISODIOS | sus PK compuestas | FNBC |
| CLASIFICACIONES | (contenido_id, genero_id) | FNBC |
| VALORACIONES | (cuenta_id, num_perfil, contenido_id) | FNBC |
| VISUALIZACIONES (ajustada) | visualizacion_id | FNBC |
6.5 Esquema relacional final
CUENTAS (cuenta_id, email, hash_password, pais, fecha_alta)
- UK: email
PLANES (plan_id, nombre, precio_mensual, calidad_max, pantallas)
- UK: nombre; CHECK: calidad_max IN (‘HD’, ‘FULLHD’, ‘4K’); pantallas BETWEEN 1 AND 6; precio_mensual ≥ 0
SUSCRIPCIONES (*cuenta_id*, fecha_inicio, plan_id, fecha_fin (N), precio_pagado)
cuenta_id → CUENTASCASCADE;plan_id → PLANESNO ACTION- CHECK: fecha_fin ≥ fecha_inicio; precio_pagado ≥ 0
PERFILES (*cuenta_id*, num_perfil, nombre, infantil)
cuenta_id → CUENTASCASCADE- CHECK: num_perfil BETWEEN 1 AND 5; infantil IN (‘S’, ‘N’)
CONTENIDOS (contenido_id, titulo, anio, edad_minima)
- CHECK: edad_minima IN (0, 7, 12, 16, 18)
PELICULAS (*contenido_id*, duracion_min)
contenido_id → CONTENIDOSCASCADE; CHECK: duracion_min > 0
SERIES (*contenido_id*, estado)
contenido_id → CONTENIDOSCASCADE; CHECK: estado IN (‘EN_EMISION’, ‘FINALIZADA’)
TEMPORADAS (*serie_id*, num_temporada, anio)
serie_id → SERIES(contenido_id)CASCADE
EPISODIOS (*serie_id*, *num_temporada*, num_episodio, titulo, duracion_min)
(serie_id, num_temporada) → TEMPORADASCASCADE
GENEROS (genero_id, nombre)
- UK: nombre
CLASIFICACIONES (*contenido_id*, *genero_id*)
contenido_id → CONTENIDOSCASCADE;genero_id → GENEROSNO ACTION
VALORACIONES (*cuenta_id*, *num_perfil*, *contenido_id*, puntuacion)
(cuenta_id, num_perfil) → PERFILESCASCADE;contenido_id → CONTENIDOSCASCADE- CHECK: puntuacion BETWEEN 1 AND 5
VISUALIZACIONES (visualizacion_id, cuenta_id, num_perfil, pelicula_id (N), serie_id (N), num_temporada (N), num_episodio (N), momento, segundo_parada)
(cuenta_id, num_perfil) → PERFILESCASCADEpelicula_id → PELICULAS(contenido_id)CASCADE(serie_id, num_temporada, num_episodio) → EPISODIOSCASCADE- CHECK (arco exclusivo): (pelicula_id IS NOT NULL AND serie_id IS NULL AND num_temporada IS NULL AND num_episodio IS NULL) OR (pelicula_id IS NULL AND serie_id IS NOT NULL AND num_temporada IS NOT NULL AND num_episodio IS NOT NULL)
- CHECK: segundo_parada ≥ 0
El atributo infantil se ha modelado como CHAR(1) con CHECK (‘S’, ‘N’) para que el diseño funcione en Oracle 19c y 21c.
⚠️ Importante: Disponible desde Oracle 23ai: el tipo de datos
BOOLEANen columnas de tabla. En versiones anteriores usaCHAR(1)con un CHECK oNUMBER(1)con valores 0 y 1.
Restricciones adicionales: como mucho una suscripción activa (fecha_fin nula) por cuenta; las suscripciones de una cuenta no se solapan; un perfil infantil no puede ver contenidos de edad mínima superior a 12.
6.6 Grafo relacional
flowchart LR
SUSCRIPCIONES -->|cuenta_id| CUENTAS
SUSCRIPCIONES -->|plan_id| PLANES
PERFILES -->|cuenta_id| CUENTAS
PELICULAS -->|contenido_id| CONTENIDOS
SERIES -->|contenido_id| CONTENIDOS
TEMPORADAS -->|serie_id| SERIES
EPISODIOS -->|"serie_id, num_temporada"| TEMPORADAS
CLASIFICACIONES -->|contenido_id| CONTENIDOS
CLASIFICACIONES -->|genero_id| GENEROS
VALORACIONES -->|"cuenta_id, num_perfil"| PERFILES
VALORACIONES -->|contenido_id| CONTENIDOS
VISUALIZACIONES -->|"cuenta_id, num_perfil"| PERFILES
VISUALIZACIONES -.->|"pelicula_id (N)"| PELICULAS
VISUALIZACIONES -.->|"serie_id, num_temporada, num_episodio (N)"| EPISODIOS
Las flechas discontinuas representan las dos claves ajenas opcionales del arco exclusivo.
7. Caso 6: El esquema EMPRESA completo
Este caso reconstruye el diseño del esquema que usarás en todo el bloque de SQL, y lo compara con su script de creación, empresa_crear.sql.
7.1 Enunciado
Una empresa que vende productos informáticos quiere gestionar su organización y sus ventas:
- Departamentos: código, nombre (único), localidad (opcional) y presupuesto (opcional, no negativo).
- Empleados: código, nombre, apellidos, correo (único), teléfono (opcional), fecha de alta, oficio, salario (positivo) y comisión (opcional, no negativa). Cada empleado pertenece como mucho a un departamento (un becario recién llegado puede no tenerlo) y tiene como mucho un jefe, que es otro empleado.
- Clientes: código, nombre, NIF (único), correo, teléfono, ciudad y provincia (estos cuatro, opcionales), fecha de alta (por defecto, la del día) y límite de crédito (por defecto 3000, no negativo). Un cliente puede tener asignado un empleado como comercial.
- Categorías de productos organizadas en jerarquía: cada categoría puede tener una categoría padre. Código y nombre (único).
- Productos: código, nombre, descripción (opcional), precio (no negativo), stock (por defecto 0, no negativo), fecha de alta (opcional) y si está descatalogado (S/N, por defecto N). Cada producto pertenece a una categoría.
- Pedidos: código, fecha (por defecto, la del día), fecha de envío (opcional, no anterior a la del pedido) y estado (PENDIENTE, ENVIADO, ENTREGADO, CANCELADO; por defecto PENDIENTE). Cada pedido es de un cliente y puede haberlo gestionado un empleado (los pedidos en línea no lo tienen).
- Líneas de pedido: cada pedido tiene varias líneas numeradas 1, 2, 3…; cada línea es de un producto, con cantidad (positiva), precio unitario de venta y porcentaje de descuento (0 a 100, por defecto 0). Si se borra un pedido, se borran sus líneas.
- Proyectos internos: código, nombre (único), departamento responsable (opcional), fecha de inicio, fecha de fin (opcional, no anterior al inicio) y presupuesto (opcional, no negativo).
- Asignaciones: un empleado puede trabajar en varios proyectos y un proyecto tener varios empleados; se guarda la fecha de asignación (por defecto, la del día), las horas dedicadas (por defecto 0, no negativas) y la función (opcional). Si se borra un proyecto, se borran sus asignaciones.
7.2 Diagrama E/R
Por claridad, el diagrama se divide en dos partes. Organización y proyectos:
flowchart LR
EMPLEADO[EMPLEADO] ---|"(0,n)"| pertenece{pertenece}
pertenece ---|"(0,1)"| DEPARTAMENTO[DEPARTAMENTO]
EMPLEADO ---|"(0,1) jefe"| dirige{dirige}
dirige ---|"(0,n) subordinado"| EMPLEADO
DEPARTAMENTO ---|"(0,1)"| desarrolla{desarrolla}
desarrolla ---|"(0,n)"| PROYECTO[PROYECTO]
EMPLEADO ---|"(0,n)"| trabaja{trabaja_en}
trabaja ---|"(0,n)"| PROYECTO
trabaja --- tr_fecha([fecha_asignacion])
trabaja --- tr_horas([horas])
trabaja --- tr_funcion(["funcion (0,1)"])
DEPARTAMENTO --- de_id([<u>dept_id</u>])
DEPARTAMENTO --- de_nom([nombre])
DEPARTAMENTO --- de_loc(["localidad (0,1)"])
DEPARTAMENTO --- de_pres(["presupuesto (0,1)"])
EMPLEADO --- em_id([<u>emp_id</u>])
EMPLEADO --- em_nom([nombre])
EMPLEADO --- em_ape([apellidos])
EMPLEADO --- em_email([email])
EMPLEADO --- em_tel(["telefono (0,1)"])
EMPLEADO --- em_alta([fecha_alta])
EMPLEADO --- em_ofi([oficio])
EMPLEADO --- em_sal([salario])
EMPLEADO --- em_com(["comision (0,1)"])
PROYECTO --- py_id([<u>proyecto_id</u>])
PROYECTO --- py_nom([nombre])
PROYECTO --- py_ini([fecha_inicio])
PROYECTO --- py_fin(["fecha_fin (0,1)"])
PROYECTO --- py_pres(["presupuesto (0,1)"])
Ventas:
flowchart LR
classDef debil stroke-width:4px
EMPLEADO[EMPLEADO] ---|"(0,1)"| atiende{atiende}
atiende ---|"(0,n)"| CLIENTE[CLIENTE]
CLIENTE ---|"(1,1)"| realiza{realiza}
realiza ---|"(0,n)"| PEDIDO[PEDIDO]
EMPLEADO ---|"(0,1)"| gestiona{gestiona}
gestiona ---|"(0,n)"| PEDIDO
PEDIDO ---|"(1,1)"| contiene{contiene}:::debil
contiene ---|"(1,n)"| LINEA[LINEA_PEDIDO]:::debil
LINEA ---|"(0,n)"| incluye{incluye}
incluye ---|"(1,1)"| PRODUCTO[PRODUCTO]
PRODUCTO ---|"(0,n)"| clasifica{clasifica}
clasifica ---|"(1,1)"| CATEGORIA[CATEGORIA]
CATEGORIA ---|"(0,1) padre"| subcat{subcategoria}
subcat ---|"(0,n) hija"| CATEGORIA
LINEA --- li_num([<u>num_linea</u> parcial])
LINEA --- li_cant([cantidad])
LINEA --- li_precio([precio_unitario])
LINEA --- li_desc([descuento])
PEDIDO --- pe_id([<u>pedido_id</u>])
PEDIDO --- pe_fecha([fecha_pedido])
PEDIDO --- pe_envio(["fecha_envio (0,1)"])
PEDIDO --- pe_estado([estado])
CLIENTE --- cl_id([<u>cliente_id</u>])
CLIENTE --- cl_nif([nif])
PRODUCTO --- pr_id([<u>producto_id</u>])
PRODUCTO --- pr_desc([descatalogado])
CATEGORIA --- ca_id([<u>categoria_id</u>])
(En el diagrama de ventas se omiten algunos atributos ya enumerados en el enunciado.) Observa, en la relación gestiona, el (0,1) junto a EMPLEADO: un pedido lo gestiona como mucho un empleado (los pedidos en línea 5014 y 5023 no tienen). Y en atiende: los clientes 1009 y 1013 no tienen comercial.
7.3 Transformación y comparación con el script
| Elemento E/R | Transformación | En empresa_crear.sql |
|---|---|---|
| DEPARTAMENTO | DEPARTAMENTOS(dept_id, nombre, localidad, presupuesto) | pk_departamentos, uq_dep_nombre, nn_dep_nombre, ck_dep_presupuesto |
| EMPLEADO | EMPLEADOS(emp_id, …, comision) | pk_empleados, uq_emp_email, ck_emp_salario, ck_emp_comision, NOT NULL en todos salvo telefono, comision, jefe_id y dept_id |
| pertenece (N:1, mínima 0) | FK dept_id nullable en EMPLEADOS | fk_emp_dept (sin acción: NO ACTION) |
| dirige (autorrelación 1:N) | FK jefe_id → EMPLEADOS en la misma tabla, nullable | fk_emp_jefe |
| CLIENTE + atiende | CLIENTES con FK comercial_id nullable | fk_cli_comercial, uq_cli_nif, DEFAULT en fecha_alta y limite_credito |
| CATEGORIA + subcategoria | FK reflexiva categoria_padre_id nullable | fk_cat_padre, uq_cat_nombre |
| PRODUCTO + clasifica (mínima 1) | FK categoria_id NOT NULL | fk_pro_categoria, nn_pro_categoria, ck_pro_descatalogado |
| PEDIDO + realiza + gestiona | FK cliente_id NOT NULL, empleado_id nullable | fk_ped_cliente, fk_ped_empleado, ck_ped_estado, ck_ped_fechas |
| LINEA_PEDIDO (débil) | PK (pedido_id, num_linea); FK a PEDIDOS con CASCADE | pk_lineas_pedido, fk_lin_pedido ... ON DELETE CASCADE |
| incluye (N:1, mínima 1) | FK producto_id NOT NULL | fk_lin_producto, nn_lin_producto |
| PROYECTO + desarrolla (mínima 0) | FK dept_id nullable | fk_pry_dept, uq_pry_nombre, ck_pry_fechas |
| trabaja_en (N:M con atributos) | ASIGNACIONES(emp_id, proyecto_id, fecha_asignacion, horas, funcion) | pk_asignaciones; fk_asi_proyecto ... ON DELETE CASCADE; fk_asi_emp NO ACTION |
El diseño lógico coincide exactamente con el script. Algunas decisiones que conviene entender:
num_lineaen la clave de LINEAS_PEDIDO, en lugar de (pedido_id, producto_id): permite repetir un producto en dos líneas de un pedido (por ejemplo, con descuentos distintos). Con (pedido_id, producto_id) como clave, esa posibilidad desaparecería y la tabla seguiría estando en FNBC.- CASCADE asimétrico en ASIGNACIONES: borrar un proyecto borra sus asignaciones, pero un empleado con asignaciones no se puede borrar (NO ACTION). Se protege el historial de horas trabajadas.
descatalogado CHAR(1)con CHECK (‘S’, ‘N’) en vez de BOOLEAN, para que el script funcione en Oracle 19c y 21c.ck_ped_fechas CHECK (fecha_envio >= fecha_pedido): confecha_envionula, la condición vale UNKNOWN y el CHECK se acepta (un CHECK solo rechaza las filas en las que la condición es FALSE). Por eso no hace falta escribirfecha_envio IS NULL OR ....
7.4 Dependencias funcionales y forma normal
| Tabla | DF no triviales (aparte de las de la PK) | Claves candidatas | FN |
|---|---|---|---|
| DEPARTAMENTOS | nombre → dept_id | dept_id, nombre | FNBC |
| EMPLEADOS | email → emp_id | emp_id, email | FNBC |
| CLIENTES | nif → cliente_id | cliente_id, nif | FNBC (ver nota) |
| CATEGORIAS | nombre → categoria_id | categoria_id, nombre | FNBC |
| PRODUCTOS | — | producto_id | FNBC |
| PEDIDOS | — | pedido_id | FNBC |
| LINEAS_PEDIDO | — | (pedido_id, num_linea) | FNBC |
| PROYECTOS | nombre → proyecto_id | proyecto_id, nombre | FNBC |
| ASIGNACIONES | — | (emp_id, proyecto_id) | FNBC |
Nota sobre CLIENTES: si se aceptara la DF ciudad → provincia, la tabla tendría una dependencia transitiva (cliente_id → ciudad → provincia) y estaría solo en 2FN. Pero, como viste en el módulo 19, el nombre de un municipio no determina la provincia (hay municipios homónimos en provincias distintas), así que no es una DF y la tabla está en FNBC. Si la empresa necesitara datos geográficos fiables, lo correcto sería una tabla MUNICIPIOS con código oficial y una FK desde CLIENTES.
Otras DF que no existen, aunque los datos de prueba no las contradigan o lo parezca:
oficio → salarioen EMPLEADOS: refutada por los datos (módulo 19).producto_id → precio_unitarioen LINEAS_PEDIDO: el precio de venta se congela en la línea.jefe_id → dept_id(que todos los subordinados de un jefe estén en el mismo departamento): refutada por los datos, porque los directores de los departamentos 20 a 60 dependen de la directora general (empleada 100, del departamento 10).
7.5 Lo que el esquema no garantiza
Estas reglas razonables no están en el script. Algunas se podrían añadir con un CHECK; otras no se pueden expresar con restricciones declarativas:
- Que un empleado no sea jefe de sí mismo sí se podría:
CHECK (jefe_id <> emp_id). Las cadenas de jefes circulares (A jefe de B y B jefe de A), no. - Que el comercial de un cliente tenga oficio VENDEDOR.
- Que un pedido ENTREGADO o ENVIADO tenga
fecha_envio: también se podría con un CHECK multicolumna (estado IN ('PENDIENTE', 'CANCELADO') OR fecha_envio IS NOT NULL). Comprueba en los datos que el pedido 5008 CANCELADO no tiene fecha de envío. - Que la fecha de asignación a un proyecto esté entre su inicio y su fin (abarca dos tablas).
✏️ Ejercicio: Revisa los pedidos de empresa_datos.sql y comprueba que ninguno incumpliría el CHECK sobre
estadoyfecha_enviopropuesto arriba (pista: fíjate en los pedidos 5008 y 5020 a 5024). En el módulo 25 aprenderás a añadir restricciones a una tabla existente conALTER TABLE.
7.6 Grafo relacional
flowchart LR
EMPLEADOS -->|dept_id| DEPARTAMENTOS
EMPLEADOS -->|jefe_id| EMPLEADOS
CLIENTES -->|comercial_id| EMPLEADOS
CATEGORIAS -->|categoria_padre_id| CATEGORIAS
PRODUCTOS -->|categoria_id| CATEGORIAS
PEDIDOS -->|cliente_id| CLIENTES
PEDIDOS -->|empleado_id| EMPLEADOS
LINEAS_PEDIDO -->|"pedido_id (CASCADE)"| PEDIDOS
LINEAS_PEDIDO -->|producto_id| PRODUCTOS
PROYECTOS -->|dept_id| DEPARTAMENTOS
ASIGNACIONES -->|emp_id| EMPLEADOS
ASIGNACIONES -->|"proyecto_id (CASCADE)"| PROYECTOS
El grafo también indica el orden de creación y de carga: primero las tablas a las que no apunta ninguna flecha de salida (DEPARTAMENTOS, CATEGORIAS), después las que dependen de ellas… Las autorrelaciones obligan a insertar primero a los jefes (por eso empresa_datos.sql inserta los empleados “de arriba abajo”). El script de borrado, empresa_borrar.sql, sigue el orden inverso.
8. Introducción al diseño físico
8.1 Del esquema lógico al físico
El diseño lógico (lo que has hecho hasta ahora) es independiente del SGBD concreto: tablas, columnas, claves y restricciones. El diseño físico decide cómo se implementa ese esquema en un SGBD concreto para que funcione con el rendimiento, la seguridad y el coste requeridos.
| Entrada del diseño físico | Ejemplo |
|---|---|
| Esquema lógico normalizado | Las tablas de los casos anteriores |
| Volumen de datos y crecimiento | 2 millones de visualizaciones al día |
| Carga de trabajo: consultas y transacciones frecuentes | “Historial de un perfil”, “préstamos activos de un socio” |
| Requisitos de rendimiento y disponibilidad | Respuesta en menos de 200 ms |
| Características del SGBD | Oracle: tipos de datos, índices, particiones, vistas materializadas |
| Decisión física | Pregunta que responde |
|---|---|
| Tipos de datos | ¿Cómo se almacena cada columna? |
| Índices | ¿Cómo se accede rápido a las filas? |
| Particionamiento | ¿Cómo se divide una tabla enorme? |
| Desnormalización controlada y vistas materializadas | ¿Qué redundancia compensa? |
| Vistas | ¿Qué ve cada usuario o aplicación? |
| Almacenamiento (tablespaces) | ¿Dónde y cómo se guardan los datos? |
8.2 Elección de tipos de datos en Oracle
| Dato | Tipo recomendado | Comentario |
|---|---|---|
| Identificadores numéricos | NUMBER(6) o columna identidad | Ajusta la precisión al volumen esperado |
| Importes | NUMBER(10,2) | Nunca FLOAT/BINARY_DOUBLE para dinero (errores de redondeo) |
| Cantidades enteras | NUMBER(5) | |
| Texto variable | VARCHAR2(n) | Longitud máxima realista; n se mide en bytes por defecto (los caracteres acentuados en UTF-8 ocupan 2) |
| Códigos de longitud fija | CHAR(n) | Por ejemplo, CHAR(1) para S/N |
| NIF, teléfono, código postal | VARCHAR2 | Son códigos, no números: no se suman y pueden tener letras o ceros a la izquierda |
| Fechas | DATE | En Oracle incluye también hora, minutos y segundos |
| Momentos con fracciones de segundo | TIMESTAMP | Por ejemplo, VISUALIZACIONES.momento |
| Textos largos, documentos | CLOB / BLOB | Sinopsis de una película, carátula |
| Lógicos | CHAR(1) + CHECK, o BOOLEAN | BOOLEAN disponible desde Oracle 23ai |
⚠️ Importante: Disponible desde Oracle 23ai: el tipo
BOOLEANen columnas. Alternativa clásica:CHAR(1)conCHECK (col IN ('S', 'N')).
Claves naturales o sustitutas. Una clave natural procede del mundo real (ISBN, NIF, matrícula); una sustituta (surrogate key) es un código interno sin significado, generado por el SGBD (columna identidad o secuencia). En Oracle, como no existe ON UPDATE CASCADE, se prefieren claves sustitutas cuando la natural puede cambiar o es muy larga, manteniendo la natural como UNIQUE. Las verás en los módulos 23 y 26.
8.3 Índices
📘 Concepto: Un índice es una estructura auxiliar (normalmente un árbol B+) que permite localizar filas por el valor de una o varias columnas sin recorrer toda la tabla, igual que el índice alfabético de un libro. Acelera las búsquedas y las reuniones, pero ocupa espacio y ralentiza las inserciones, modificaciones y borrados, porque hay que mantenerlo.
Oracle crea automáticamente un índice para cada PRIMARY KEY y cada UNIQUE, pero no para las claves ajenas. Puedes comprobarlo en el diccionario de datos con el esquema EMPRESA:
SELECT table_name, index_name, column_name
FROM user_ind_columns
WHERE table_name IN ('EMPLEADOS', 'ASIGNACIONES')
ORDER BY table_name, index_name, column_position;
TABLE_NAME INDEX_NAME COLUMN_NAME
--------------- --------------- ------------
ASIGNACIONES PK_ASIGNACIONES EMP_ID
ASIGNACIONES PK_ASIGNACIONES PROYECTO_ID
EMPLEADOS PK_EMPLEADOS EMP_ID
EMPLEADOS UQ_EMP_EMAIL EMAIL
4 filas seleccionadas.
EMPLEADOS.dept_id y EMPLEADOS.jefe_id no tienen índice. Tampoco ASIGNACIONES.proyecto_id como primera columna: el índice de la PK empieza por emp_id, así que sirve para buscar “los proyectos de un empleado” pero no “los empleados de un proyecto”.
Criterios para decidir qué indexar:
| Indexar | No indexar (en general) |
|---|---|
| Claves ajenas: aceleran las reuniones y evitan bloqueos de la tabla hija al borrar o modificar filas padre | Tablas muy pequeñas (se leen enteras más rápido) |
Columnas muy usadas en WHERE, JOIN y ORDER BY | Columnas que casi nunca se consultan |
Columnas selectivas (muchos valores distintos: email, fecha_pedido) | Columnas con pocos valores distintos en sistemas transaccionales (estado, descatalogado) |
| Combinaciones que se consultan juntas (índice compuesto, con la columna más filtrada primero) | Tablas con muchísimas escrituras y pocas lecturas |
Propuesta de índices para EMPRESA (además de los de PK y UK):
| Índice | Motivo |
|---|---|
empleados(dept_id) | FK; “empleados de un departamento” |
empleados(jefe_id) | FK; consultas jerárquicas (módulo 33) |
clientes(comercial_id) | FK; “clientes de un comercial” |
pedidos(cliente_id) | FK; “pedidos de un cliente” |
pedidos(empleado_id) | FK |
pedidos(fecha_pedido) | Búsquedas por rangos de fechas (ventas del mes) |
lineas_pedido(producto_id) | FK; “en qué pedidos aparece un producto” |
productos(categoria_id) | FK |
asignaciones(proyecto_id) | FK; la PK empieza por emp_id |
proyectos(dept_id), categorias(categoria_padre_id) | FK |
Un adelanto de la sintaxis, que estudiarás en el módulo 26:
CREATE INDEX ix_emp_dept ON empleados (dept_id);
Índice creado.
Otros tipos de índice de Oracle que conviene conocer: bitmap (para columnas con pocos valores en almacenes de datos, no en sistemas con muchas actualizaciones concurrentes), basados en funciones (por ejemplo, sobre UPPER(email) para búsquedas sin distinguir mayúsculas) e índices únicos condicionales construidos sobre una expresión CASE, una técnica que permite, por ejemplo, impedir dos ingresos sin alta del mismo paciente sin escribir disparadores.
8.4 Particionamiento
📘 Concepto: El particionamiento divide una tabla (o un índice) muy grande en trozos llamados particiones, que el SGBD gestiona por separado pero que para las consultas siguen siendo una única tabla.
| Tipo | Criterio | Ejemplo |
|---|---|---|
| Por rango (range) | Intervalos de valores, normalmente fechas | VISUALIZACIONES por mes |
| Por lista (list) | Valores concretos | CUENTAS por país |
| Por hash | Función de dispersión, para repartir uniformemente | Tablas sin un criterio natural |
| Compuesto | Combinación de los anteriores | Rango por mes y hash por cuenta |
Ventajas: las consultas que filtran por la columna de partición solo leen las particiones necesarias (partition pruning); se pueden archivar o borrar datos antiguos eliminando una partición entera en lugar de con un DELETE masivo; las operaciones de mantenimiento se hacen por trozos.
💡 Consejo: El particionamiento solo compensa en tablas grandes (del orden de decenas de millones de filas o más). La disponibilidad de la opción de particionamiento depende de la edición y la licencia de Oracle; consúltalo antes de diseñar con ella.
8.5 Estimación de volumen
Antes de decidir índices y particiones hay que estimar cuántos datos habrá. Método: filas esperadas × tamaño medio de fila, proyectado a varios años.
| Tabla | Filas estimadas | Tamaño medio de fila | Volumen |
|---|---|---|---|
| Biblioteca: SOCIOS | 20 000 | ~150 B | ~3 MB |
| Biblioteca: PRESTAMOS (300/día, 300 días/año, 10 años) | 900 000 | ~45 B | ~40 MB |
| Streaming: PERFILES | 2 000 000 | ~60 B | ~120 MB |
| Streaming: VISUALIZACIONES (3 por perfil y día) | 6 000 000/día ≈ 2 190 millones/año | ~50 B | ~110 GB/año |
Conclusiones: la biblioteca cabe holgadamente en memoria y le bastan los índices de las claves ajenas. En la plataforma de streaming, VISUALIZACIONES justifica particionar por mes, revisar con cuidado cada índice (cada uno multiplica el coste de 6 millones de inserciones diarias) y quizá mover el historial antiguo a un almacén de datos. A estas cifras hay que sumar los índices (a menudo, tanto espacio como la propia tabla) y un margen de crecimiento.
8.6 Desnormalización controlada y vistas
Lo visto en el módulo 20 se aplica en el diseño físico, nunca antes de tener el esquema lógico normalizado:
- Vistas: consultas almacenadas que se usan como tablas. No duplican datos. Sirven para simplificar (una vista
v_pedidos_importecon el importe de cada pedido), para seguridad (una vista de empleados sin el salario para el personal de recepción) y para independencia lógica (si cambia una tabla, se adapta la vista y las aplicaciones no se enteran). - Vistas materializadas: guardan físicamente el resultado y el SGBD lo refresca. Útiles para resúmenes costosos (ventas por mes y categoría).
- Columnas desnormalizadas: solo cuando las anteriores no bastan, documentando el mecanismo que las mantiene.
Todo esto se estudia en el módulo 26.
8.7 De diseño a DDL: un adelanto
Así se escribe en Oracle la tabla SOCIOS del caso 1, con todas las restricciones del esquema final (la sintaxis completa la verás en los módulos 23 y 24):
CREATE TABLE socios (
num_socio NUMBER(6) GENERATED ALWAYS AS IDENTITY
CONSTRAINT pk_socios PRIMARY KEY,
dni VARCHAR2(9) CONSTRAINT nn_soc_dni NOT NULL,
nombre VARCHAR2(40) CONSTRAINT nn_soc_nombre NOT NULL,
apellidos VARCHAR2(60) CONSTRAINT nn_soc_apellidos NOT NULL,
email VARCHAR2(80),
telefono VARCHAR2(15),
fecha_alta DATE DEFAULT SYSDATE CONSTRAINT nn_soc_fecha_alta NOT NULL,
CONSTRAINT uq_soc_dni UNIQUE (dni),
CONSTRAINT uq_soc_email UNIQUE (email),
CONSTRAINT ck_soc_email CHECK (email LIKE '%_@_%._%')
);
Tabla creada.
Correspondencia con el diseño lógico: la PK subrayada es pk_socios (con una columna identidad como clave sustituta, disponible desde Oracle 12c); cada atributo sin (N) lleva su NOT NULL con nombre; las UK son uq_soc_dni y uq_soc_email; y se ha añadido un CHECK de formato que no estaba en el diseño lógico (una decisión física de calidad de datos). Si intentas insertar un socio con el correo 'luis-sin-arroba', Oracle lo rechaza con el error ORA-02290 (violación de la restricción CK_SOC_EMAIL).
Para dejar tu esquema limpio después de la prueba:
DROP TABLE socios PURGE;
Tabla borrada.
8.8 Lista de comprobación del diseño físico
- ¿Cada columna tiene el tipo y la longitud adecuados? ¿Los importes son
NUMBER(p,2)? - ¿Las claves primarias son estables? ¿Hacen falta claves sustitutas?
- ¿Todas las restricciones del esquema lógico tienen su
CONSTRAINTcon nombre? - ¿Están indexadas las claves ajenas y las columnas de búsqueda frecuente?
- ¿Hay tablas que por volumen necesiten particionamiento o archivado?
- ¿Qué vistas necesitan las aplicaciones y los distintos perfiles de usuario?
- ¿Hay alguna desnormalización? ¿Está medida, justificada y documentada?
- ¿Qué restricciones adicionales quedan para disparadores o para la aplicación?
9. Ejercicios
Los ejercicios 1 y 2 están resueltos. Los ejercicios 3 a 7 son casos completos propuestos: para cada uno, sigue las siete fases de la sección 1 y, como en los casos resueltos, termina con el esquema final y su grafo relacional.
Ejercicio 1: Índices para la biblioteca
Propón los índices (además de los de PK y UK) para el esquema final del caso 1, sabiendo que las operaciones más frecuentes son: buscar libros por título, consultar los préstamos activos de un socio y ver qué ejemplares de un libro están disponibles.
Ver solución
| Índice | Motivo |
|---|---|
libros(titulo) | Búsqueda por título (si se busca por palabras sueltas, mejor un índice de texto, fuera del alcance del curso) |
libros(editorial_id) | FK |
autorias(autor_id) | FK; “libros de un autor” (la PK empieza por isbn) |
ejemplares(estanteria_id) | FK |
prestamos(num_socio, fecha_devolucion) | Préstamos de un socio, filtrando los no devueltos |
reservas(isbn) | FK; la PK empieza por num_socio |
No hace falta un índice en ejemplares(isbn): la PK (isbn, num_ejemplar) empieza por isbn y sirve también para buscar los ejemplares de un libro. Tampoco en prestamos(isbn, num_ejemplar): lo cubre el índice de la UK (isbn, num_ejemplar, fecha_prestamo).
Con el volumen estimado (menos de un millón de préstamos), no se necesita particionamiento.
Ejercicio 2: Revisar un diseño ajeno
Un compañero ha transformado el caso 4 (liga) y propone esta tabla. Encuentra los problemas:
FICHAS (jugador_id, equipo_id, temporada_id, dorsal, nombre_equipo, nombre_jugador)
Ver solución
- Clave incorrecta: con (jugador_id, equipo_id, temporada_id) como clave, un jugador podría tener ficha en dos equipos la misma temporada, contra el requisito 3. La clave debe ser (jugador_id, temporada_id);
equipo_idpasa a ser un atributo (FK). - No está en 2FN:
equipo_id → nombre_equipoyjugador_id → nombre_jugadorson dependencias parciales. Esas columnas sobran: están en EQUIPOS y JUGADORES. - Falta la UK (equipo_id, temporada_id, dorsal), que es la otra clave candidata y garantiza dorsales únicos.
Tabla corregida: FICHAS (*jugador_id*, *temporada_id*, equipo_id, dorsal) con UK (equipo_id, temporada_id, dorsal).
Ejercicio 3: Propuesto. Agencia de viajes
Una agencia organiza viajes (código, destino, descripción, precio base y número de días) que se ofrecen en varias salidas (fecha de salida y plazas). Cada salida tiene un guía (DNI, nombre, idiomas que habla: varios). Los clientes (DNI, nombre, teléfono, correo) reservan plazas en una salida (fecha de reserva, número de plazas, precio total pagado, estado: CONFIRMADA o ANULADA). Cada viaje incluye estancias en varios hoteles (código, nombre, ciudad, categoría de 1 a 5 estrellas), con el número de noches en cada uno. Atención a los atributos multivaluados y al precio histórico.
Ejercicio 4: Propuesto. Red social
Los usuarios (identificador, nombre de usuario único, correo único, fecha de alta, biografía opcional) siguen a otros usuarios (con la fecha de inicio). Publican publicaciones (texto, fecha, ubicación opcional) que pueden ser respuesta a otra publicación. Una publicación puede llevar varias etiquetas (#hashtags) y recibir me gusta (como mucho uno por usuario, con su fecha). Los usuarios pueden enviarse mensajes privados. Presta atención a las autorrelaciones y a las restricciones que no se pueden expresar (por ejemplo, que un usuario no se siga a sí mismo sí se puede; ¿y que no dé “me gusta” a sus propias publicaciones?).
Ejercicio 5: Propuesto. Gimnasio
Un gimnasio con varios centros tiene socios con una cuota (básica, completa, familiar) cuyo precio cambia cada año. Los monitores imparten actividades (yoga, spinning…) en sesiones con día de la semana, hora, sala y aforo. Los socios se inscriben en sesiones concretas (una fecha concreta de una sesión semanal). Una sala no puede albergar dos sesiones a la misma hora y un monitor no puede impartir dos sesiones a la vez. Comprueba si alguna tabla queda en 3FN pero no en FNBC.
Ejercicio 6: Propuesto. Venta de entradas para conciertos
Una empresa vende entradas para conciertos de artistas (uno o varios por concierto, con su orden de actuación) en recintos que tienen zonas (pista, grada A…) con su aforo. El precio de la entrada depende del concierto y de la zona. Cada venta la hace un cliente, en una fecha, y puede incluir varias entradas, cada una con su zona y un código QR único. Hay que garantizar que no se vendan más entradas que el aforo de la zona (¿se puede declarativamente?).
Ejercicio 7: Propuesto. Academia de idiomas
Una academia ofrece cursos de varios idiomas y niveles (A1 a C2). Cada curso se imparte en grupos con horario, aula y un profesor. Los profesores pueden enseñar varios idiomas (es una relación con la fecha de acreditación). Los alumnos se matriculan en grupos y obtienen una calificación final; además, la academia guarda los pagos mensuales de cada matrícula. Incluye el estudio del caso “cada profesor imparte un único idioma” frente a “puede impartir varios” y su efecto en la forma normal de la tabla de grupos.
Resumen
| Fase | Qué se hace | Errores frecuentes |
|---|---|---|
| Requisitos | Identificar objetos, atributos, asociaciones y reglas | No preguntar por los casos límite (historia, repeticiones, nulos) |
| E/R | Entidades, relaciones, cardinalidades, débiles, jerarquías | Cardinalidades al revés; atributos de otra entidad (“entidades ocultas”) |
| Transformación | Tablas, PK, FK, UK, nulos según la participación mínima | FK en el lado equivocado en 1:1; olvidar la UK que garantiza el 1:1 |
| DF y forma normal | DF de cada tabla, claves candidatas, forma normal | Deducir DF de los datos; olvidar claves alternativas |
| Ajustes | Separar dependencias parciales y transitivas; valorar FNBC | Confundir datos históricos (precio aplicado) con redundancia |
| Esquema final | PK, FK con acción ante el borrado, UK, NOT NULL, CHECK | CASCADE indiscriminado; no documentar las restricciones adicionales |
| Grafo relacional | Flechas de FK; orden de creación y de carga | Ciclos de FK NOT NULL imposibles de cargar |
| Diseño físico | Tipos, índices, particiones, volumen, vistas | Indexarlo todo o nada; desnormalizar sin medir |