Parcial de práctica — Bases de Datos

Temas: partes 1 y 2 del curso (semanas 1 a 8) · Duración: 2 horas

Este es un examen de práctica con el formato del parcial. Sirve para medir cuánto tiempo les toma cada tipo de pregunta: resuélvanlo en la computadora, con el contenedor del curso, sin internet y con el reloj corriendo. El formato general está en exams/README.md.

Indicaciones

  • En SQL importa la idea: un error menor de sintaxis no la invalida.
  • En las preguntas de “qué devuelve”, justifiquen cada respuesta: un resultado sin explicación no basta.
  • El esquema es el de PostgreSQL 18. Las consultas de las partes A y B se responden sobre los datos de abajo, salvo que la pregunta diga otra cosa.
  • En el álgebra relacional usen la notación del curso: σ, Π, ρ, ⋈, ×, ∪, −, ∩, ÷, γ y ←.
Parte Preguntas
A. Escribir consultas 1–4
B. Leer consultas y código 5–6
C. Diseño 7–8
D. Preguntas cortas 9–14

El esquema: una red de consultorios

Una red de consultorios médicos de Lima registra a sus médicos, sus pacientes y las citas. Una cita atendida (done) puede tener el pago fee en NULL si todavía no se registró; una cita cancelada no se cobra. insurer es el seguro del paciente, NULL si no tiene.

CREATE TABLE specialty (
  specialty_id integer PRIMARY KEY,
  name         text NOT NULL UNIQUE
);

CREATE TABLE doctor (
  doctor_id    integer PRIMARY KEY,
  name         text NOT NULL,
  specialty_id integer NOT NULL REFERENCES specialty
);

CREATE TABLE patient (
  patient_id integer PRIMARY KEY,
  dni        char(8) NOT NULL UNIQUE,
  name       text NOT NULL,
  district   text NOT NULL,
  insurer    text
);

CREATE TABLE appointment (
  appointment_id integer PRIMARY KEY,
  patient_id     integer NOT NULL REFERENCES patient,
  doctor_id      integer NOT NULL REFERENCES doctor,
  day            date NOT NULL,
  fee            numeric(7, 2) CHECK (fee >= 0),
  status         text NOT NULL CHECK (status IN ('scheduled', 'done', 'cancelled'))
);

Los datos (∅ es NULL):

 specialty_id |     name
--------------+--------------
            1 | Cardiología
            2 | Pediatría
            3 | Dermatología
            4 | Neurología

 doctor_id |  name   | specialty_id
-----------+---------+--------------
        10 | Rojas   |            1
        11 | Quispe  |            1
        12 | Salazar |            2
        13 | Huamán  |            3
        14 | Vargas  |            2

 patient_id |   dni    | name  |  district  | insurer
------------+----------+-------+------------+---------
          1 | 40112233 | Ana   | Surco      | EsSalud
          2 | 41223344 | Bruno | Miraflores | ∅
          3 | 42334455 | Carla | Surco      | SIS
          4 | 43445566 | Diego | Lince      | EsSalud
          5 | 44556677 | Elena | Lince      | ∅

 appointment_id | patient_id | doctor_id |    day     |  fee   |  status
----------------+------------+-----------+------------+--------+-----------
            100 |          1 |        10 | 2026-03-02 | 120.00 | done
            101 |          1 |        11 | 2026-03-09 | 150.00 | done
            102 |          2 |        10 | 2026-03-02 | 120.00 | done
            103 |          2 |        12 | 2026-03-05 |      ∅ | cancelled
            104 |          3 |        11 | 2026-03-10 | 150.00 | done
            105 |          3 |        10 | 2026-03-11 |      ∅ | done
            106 |          4 |        13 | 2026-03-12 |  90.00 | done
            107 |          1 |        12 | 2026-03-15 |  80.00 | done
            108 |          4 |        12 | 2026-03-16 |      ∅ | cancelled
            109 |          2 |        10 | 2026-03-20 | 120.00 | done

Parte A — Escribir consultas

Para cada consulta, además de escribirla, digan qué filas devuelve con los datos de arriba.

Pregunta 1 — Joins y diferencia

a) Los nombres de los pacientes que tuvieron al menos una cita atendida (done) con un médico de Pediatría. Escríbanla en álgebra relacional y en SQL.

b) Los nombres de los pacientes que nunca tuvieron una cita, en ninguno de sus estados. Escríbanla en álgebra relacional (con una diferencia) y en SQL. Un compañero escribe SELECT name FROM patient EXCEPT SELECT p.name FROM patient p JOIN appointment a ON a.patient_id = p.patient_id. Con estos datos da la respuesta correcta. ¿Por qué igual está mal?

Pregunta 2 — “Para todos”

Los pacientes que tuvieron una cita atendida con cada uno de los médicos de Cardiología.

a) En álgebra relacional, con la división ÷. Digan el esquema de cada lado de la división.

b) En SQL, con NOT EXISTS (la doble negación).

c) Si en su consulta de (b) cambian 'Cardiología' por 'Neurología', que no tiene médicos, ¿qué devuelve? ¿Es un error de la consulta o es lo que dice la lógica?

Pregunta 3 — Agregación

a) Para cada especialidad, su nombre, el número de citas atendidas de sus médicos y lo recaudado por esas citas. Las especialidades sin citas atendidas aparecen con 0 en las dos columnas (no con NULL).

b) Las especialidades cuyo pago promedio por cita atendida es mayor que el pago promedio de todas las citas atendidas. ¿Qué hace avg con la cita 105?

Pregunta 4 — Window functions

a) Para cada médico, su última cita atendida: el nombre del médico, el día y el nombre del paciente. Si un médico tuvo dos citas atendidas en su último día, salen las dos. Usen una window function. ¿Qué función de ranking eligen y por qué no otra?

b) Una compañera lo resuelve sin window functions:

SELECT d.name AS doctor, a.day, p.name AS patient
FROM appointment a
  JOIN doctor d ON d.doctor_id = a.doctor_id
  JOIN patient p ON p.patient_id = a.patient_id
WHERE a.status = 'done'
  AND a.day = (SELECT max(a2.day) FROM appointment a2 WHERE a2.doctor_id = a.doctor_id)
ORDER BY doctor;

¿Qué devuelve? ¿Qué médico falta y por qué? Corríjanla.

Parte B — Leer consultas y código

Pregunta 5 — Qué devuelve

Para cada consulta, escriban el resultado completo (nombres de columna aparte) y expliquen cada valor en una o dos líneas.

a)

SELECT count(*), count(fee), count(DISTINCT fee), sum(fee), round(avg(fee), 2)
FROM appointment
WHERE status = 'done';

b) Las dos consultas, y por qué difieren:

SELECT name FROM patient
WHERE insurer NOT IN (SELECT insurer FROM patient WHERE district = 'Lince');

SELECT p.name FROM patient p
WHERE NOT EXISTS (SELECT 1 FROM patient q
                  WHERE q.district = 'Lince' AND q.insurer = p.insurer);

c) Las tres consultas:

-- (i)
SELECT d.name, count(a.appointment_id) AS n
FROM doctor d LEFT JOIN appointment a ON a.doctor_id = d.doctor_id
WHERE a.status = 'done'
GROUP BY d.name;

-- (ii)
SELECT d.name, count(a.appointment_id) AS n
FROM doctor d LEFT JOIN appointment a ON a.doctor_id = d.doctor_id AND a.status = 'done'
GROUP BY d.name;

-- (iii)
SELECT d.name, count(*) AS n
FROM doctor d LEFT JOIN appointment a ON a.doctor_id = d.doctor_id AND a.status = 'done'
GROUP BY d.name;

d) ¿Qué devuelve cada una? ¿Qué devuelve la de UNION sin el count(*) de afuera?

SELECT count(*) FROM (SELECT patient_id FROM appointment WHERE doctor_id = 10
                      UNION ALL
                      SELECT patient_id FROM appointment WHERE fee >= 150) AS t;

SELECT count(*) FROM (SELECT patient_id FROM appointment WHERE doctor_id = 10
                      UNION
                      SELECT patient_id FROM appointment WHERE fee >= 150) AS t;

e) ¿Cuántas filas devuelve cada una, y con qué valores?

SELECT count(*), sum(a.fee), max(a.day)
FROM appointment a JOIN doctor d ON d.doctor_id = a.doctor_id
WHERE d.specialty_id = 4;

SELECT d.specialty_id, count(*), sum(a.fee), max(a.day)
FROM appointment a JOIN doctor d ON d.doctor_id = a.doctor_id
WHERE d.specialty_id = 4
GROUP BY d.specialty_id;

f)

SELECT insurer, count(*) AS n
FROM patient
GROUP BY insurer;

Pregunta 6 — SQL desde Node.js

La web de los consultorios usa estas dos funciones, con un pg.Pool que se crea una vez al iniciar la aplicación:

// Pacientes de un distrito, para el buscador de la web.
export async function patientsIn(pool, district) {
  const { rows } = await pool.query(
    "SELECT name, insurer FROM patient WHERE district = '" + district + "' ORDER BY name",
  );
  return rows;
}

// Reprograma una cita: cancela la cita appointmentId y crea la cita newId
// con el mismo paciente y el mismo médico, para el día newDay.
export async function reschedule(pool, appointmentId, newId, newDay) {
  const client = await pool.connect();
  try {
    await pool.query('BEGIN');
    await client.query(
      "UPDATE appointment SET status = 'cancelled' WHERE appointment_id = $1",
      [appointmentId],
    );
    await client.query(
      `INSERT INTO appointment (appointment_id, patient_id, doctor_id, day, fee, status)
       SELECT $2, patient_id, doctor_id, $3, NULL, 'scheduled'
       FROM appointment WHERE appointment_id = $1`,
      [appointmentId, newId, newDay],
    );
    await pool.query('COMMIT');
    client.release();
  } catch (err) {
    await pool.query('ROLLBACK');
    throw err;
  }
}

a) Escriban un valor de district que haga que patientsIn devuelva a todos los pacientes, y otro que borre todas las citas. ¿Por qué el segundo funciona con pg en esta función? Corrijan la función.

b) Se llama reschedule(pool, 106, 100, '2026-03-30') por error: la cita 100 ya existe. ¿Qué sentencia falla y con qué error? ¿En qué estado queda la cita 106 después de la llamada? ¿Qué le pasa al pool? Hay dos errores distintos en la función: expliquen cada uno y escriban la función corregida.

Parte C — Diseño

Pregunta 7 — Modelo E-R

Un instituto de idiomas tiene varias sedes (código, nombre y distrito) y ofrece cursos (código, nombre como “Inglés Básico 1”, idioma y número de horas). Un curso puede tener como prerrequisito a otros cursos, y puede ser prerrequisito de otros.

Cada ciclo (por ejemplo, 2026-1) el instituto abre secciones de sus cursos. Una sección se identifica por su curso, el ciclo y un número (1, 2, 3, …) que solo distingue a las secciones del mismo curso en el mismo ciclo. Cada sección tiene un horario, se dicta en una sola sede y la dicta un solo profesor. De cada profesor se guarda su DNI, su nombre y sus correos (puede tener varios); un profesor puede dictar varias secciones o ninguna.

Los alumnos (código, nombre y fecha de nacimiento; la edad no se guarda, se calcula) se matriculan en secciones. De cada matrícula se guarda la fecha y la nota final, que no existe mientras el ciclo no termina.

a) Dibujen el diagrama E-R en la notación del libro: conjuntos de entidades con sus atributos (subrayen la clave primaria; marquen los atributos multivaluados y derivados), conjuntos de relaciones con sus cardinalidades y su participación, y la entidad débil con su relación identificadora y su discriminador. Nombren los roles donde haga falta.

b) Tradúzcanlo a tablas con CREATE TABLE, con todas las claves primarias y foráneas y los NOT NULL que se deducen del diagrama.

Pregunta 8 — Dependencias funcionales y normalización

Sea R = (A, B, C, D, E, G) con el conjunto de dependencias funcionales

F = { A → BC, CD → E, B → D, E → A }

a) Calculen A⁺ y B⁺, mostrando los pasos. ¿Está A → E en F⁺?

b) Encuentren todas las claves candidatas de R. Justifiquen por qué G está en todas y por qué cada una es mínima.

c) ¿Es F una cubierta canónica? Justifiquen probando al menos un atributo que podría ser extraño.

d) ¿Está R en BCNF? ¿Está en 3NF? Justifiquen con cada dependencia de F.

e) Descompongan R en BCNF con el algoritmo del curso, diciendo en cada paso qué dependencia viola BCNF. ¿Su descomposición preserva las dependencias? Si R ya estaba en 3NF, ¿qué ganan y qué pierden al pasarla a BCNF?

f) ¿Es sin pérdida la descomposición de R en (A, B, C, D) y (D, E, G)? Si no lo es, den una instancia de R de dos tuplas que cumpla F y muestren las tuplas que aparecen de más en el join.

Parte D — Preguntas cortas

Respondan cada una en tres o cuatro líneas.

Pregunta 9

¿Qué diferencia hay entre una superclave y una clave candidata? ¿Cuáles son las claves candidatas de patient? ¿Es {patient_id, name} una superclave? ¿Y una clave candidata?

Pregunta 10

¿Cuántas tuplas tiene Πpatient_id(σdoctor_id = 10(appointment))? ¿Cuántas filas devuelve SELECT patient_id FROM appointment WHERE doctor_id = 10? ¿Por qué SQL trabaja con multiconjuntos y no con conjuntos?

Pregunta 11

¿Qué devuelve esta consulta? Escriban la tabla de trabajo de cada iteración.

WITH RECURSIVE n(k) AS (
  SELECT 1
  UNION ALL
  SELECT k * 2 FROM n WHERE k < 20
)
SELECT count(*), max(k) FROM n;

Pregunta 12

¿Qué devuelve cada columna? ¿Cuál de las cuatro da un resultado que probablemente no es el que se quería, y cómo se corrige?

SELECT doc -> 'vitals' ->> 'bp'               AS a,
       doc ->> 'allergies'                    AS b,
       doc @> '{"allergies": ["penicilina"]}' AS c,
       doc -> 'vitals' ->> 'hr' > '100'       AS d
FROM (VALUES ('{"vitals": {"bp": "120/80", "hr": 72},
                "allergies": ["penicilina", "látex"]}'::jsonb)) AS t(doc);

Pregunta 13

Cada médico habla uno o más idiomas y atiende uno o más días de la semana, sin relación entre ambas cosas. Se guarda todo en doctor_info(doctor_id, language, office_day), con los tres atributos como clave. ¿Qué dependencias funcionales no triviales tiene? ¿Está en BCNF? ¿Qué redundancia tiene, y qué dependencia la explica? ¿Cómo se descompone?

Pregunta 14

Dos reglas: (1) fee nunca es negativo; (2) cuando una cita pasa a cancelled, su fee se borra (queda en NULL). ¿Qué usarían para cada una: una restricción CHECK, un trigger BEFORE o un trigger AFTER? Justifiquen. ¿Hay una alternativa declarativa para la segunda, y en qué se comporta distinto?