Módulo 22: Álgebra relacional

Objetivos del módulo

  • Distinguir los lenguajes de consulta formales: álgebra relacional (procedimental) y cálculo relacional (declarativo)
  • Dominar los operadores primitivos: selección, proyección, unión, diferencia, producto cartesiano y renombrado
  • Comprobar la compatibilidad de unión entre relaciones
  • Manejar los operadores derivados: intersección, combinaciones (theta, equicombinación y natural), división, semicombinación y antisemicombinación
  • Usar las combinaciones externas y el operador de agrupación
  • Construir expresiones complejas y secuencias de operaciones con asignación
  • Conocer las propiedades y equivalencias algebraicas y su uso en la optimización de consultas
  • Expresar consultas en cálculo relacional de tuplas y de dominios
  • Traducir cada operador del álgebra a SQL
  • Resolver consultas reales sobre el esquema EMPRESA y predecir su resultado exacto

1. Lenguajes de consulta formales

1.1. Álgebra y cálculo

En 1972 Codd definió dos lenguajes formales para manipular relaciones:

Lenguaje Tipo Cómo se expresa una consulta Analogía
Álgebra relacional Procedimental Como una secuencia de operaciones que, aplicadas a relaciones, producen la relación resultado Una receta de cocina: “primero filtra, luego combina, luego quédate con estas columnas”
Cálculo relacional (de tuplas o de dominios) Declarativo (no procedimental) Como una condición lógica que deben cumplir las tuplas del resultado La descripción del plato: “quiero las tuplas que cumplan esto”, sin decir cómo obtenerlas

SQL toma elementos de ambos: su estructura SELECT ... FROM ... WHERE se parece al cálculo de tuplas, mientras que operadores como UNION, INTERSECT, MINUS o JOIN vienen directamente del álgebra. Otros lenguajes históricos son QUEL (Ingres, basado en el cálculo de tuplas) y QBE (Query By Example, IBM, basado en el cálculo de dominios; su idea sobrevive en los diseñadores visuales de consultas de Access o LibreOffice Base).

📘 Concepto: El álgebra relacional es un conjunto de operadores que toman una o dos relaciones como operandos y devuelven una relación como resultado.

1.2. ¿Por qué estudiar el álgebra si existe SQL?

  • Es la base teórica de SQL: entender el álgebra es entender qué hace realmente cada consulta.
  • Es el lenguaje interno de los optimizadores: Oracle traduce tu SELECT a un árbol de operadores algebraicos, lo reorganiza con las equivalencias de la sección 5 y elige el plan más barato.
  • Obliga a pensar en conjuntos, no en bucles fila a fila.
  • Es materia obligada en exámenes de universidad y de ciclos formativos.

1.3. Propiedad de cierre y completitud

📘 Concepto: Propiedad de cierre (closure): el resultado de cualquier operación del álgebra es otra relación. Por eso las operaciones se pueden anidar y componer libremente, igual que en aritmética se escribe (3 + 4) × 2.

Un lenguaje es relacionalmente completo si puede expresar cualquier consulta que exprese el álgebra relacional (o, equivalentemente, el cálculo relacional seguro). SQL es relacionalmente completo y además va más allá (agregados, ordenación, recursividad).

1.4. Notación del curso

Operador Símbolo Notación Tipo
Selección σ σ[condición](R) Primitivo, unario
Proyección π π[atributos](R) Primitivo, unario
Renombrado ρ ρ[S](R), ρ[S(B1, ..., Bn)](R) Primitivo, unario
Unión ∪ R ∪ S Primitivo, binario
Diferencia − R − S Primitivo, binario
Producto cartesiano × R × S Primitivo, binario
Intersección ∩ R ∩ S Derivado
Combinación theta ⋈ R ⋈[condición] S Derivado
Combinación natural ⋈ R ⋈ S Derivado
División ÷ R ÷ S Derivado
Semicombinación ⋉ R ⋉[condición] S Derivado
Antisemicombinación ▷ R ▷[condición] S Derivado
Combinaciones externas ⟕ ⟖ ⟗ R ⟕[condición] S Extendido
Agrupación γ γ[atributos de grupo; funciones](R) Extendido
Asignación ← T ← expresión Auxiliar

En las condiciones se usan los comparadores =, ≠, <, ≤, >, ≥ y los conectores ∧ (y), ∨ (o) y ¬ (no). Para los nulos se admiten IS NULL e IS NOT NULL. Cuando dos relaciones tienen atributos con el mismo nombre, se cualifican con el nombre de la relación: EMPLEADOS.dept_id.

💡 Consejo: En otros libros verás la condición como subíndice del símbolo (σ con «salario > 2000» escrito debajo) o con los operadores en palabras (SELECT, PROJECT, JOIN). Son exactamente lo mismo; en este curso los corchetes facilitan escribirlo en texto plano.

1.5. Las relaciones de ejemplo

Todas las consultas trabajan sobre el esquema EMPRESA (módulo 16) con los datos de ../recursos/empresa_datos.sql. Estas son las relaciones que más se usan, para que puedas comprobar a mano cada resultado:

EMPLEADOS (se omiten email, teléfono y fecha de alta)

emp_id nombre apellidos oficio salario comision jefe_id dept_id
100 Ana García López DIRECTOR GENERAL 6500 NULL NULL 10
101 Luis Martínez Ruiz DIRECTOR 4800 NULL 100 20
102 Marta Sánchez Gil DIRECTOR 5000 NULL 100 30
103 Pedro Gómez Díaz DIRECTOR 4200 NULL 100 40
104 Laura Fernández Moreno DIRECTOR 4300 NULL 100 50
105 Javier López Navarro DIRECTOR 4000 NULL 100 60
110 Carmen Ruiz Ortega VENDEDOR 2200 500 101 20
111 David Torres Romero VENDEDOR 2100 800 101 20
112 Elena Navarro Castro VENDEDOR 2300 300 101 20
113 Sergio Jiménez Vidal VENDEDOR 1900 0 101 20
114 Lucía Romero Serrano ADMINISTRATIVO 1700 NULL 101 20
120 Pablo Moreno Herrera ANALISTA 3500 NULL 102 30
121 Raquel Molina Prieto PROGRAMADOR 2800 NULL 120 30
122 Iván Castro Méndez PROGRAMADOR 2600 NULL 120 30
123 Sara Ortiz Delgado PROGRAMADOR 2400 NULL 120 30
124 Hugo Rubio Marín TÉCNICO SISTEMAS 2500 NULL 102 30
130 Isabel Domínguez Vega CONTABLE 2600 NULL 103 40
131 Jorge Vázquez Ramos ADMINISTRATIVO 1650 NULL 103 40
140 Nuria Gil Soto DISEÑADOR 2300 NULL 104 50
141 Álvaro Peña Cano ANALISTA 2700 NULL 104 50
150 Rosa Iglesias Cruz OPERARIO 1600 NULL 105 60
151 Manuel Herrero Lozano OPERARIO 1600 NULL 105 60
152 Cristina Medina Blanco ADMINISTRATIVO 1750 NULL 105 60
160 Tomás Reyes Fuentes BECARIO 900 NULL 102 NULL

DEPARTAMENTOS

dept_id nombre localidad presupuesto
10 DIRECCIÓN Madrid 500000
20 VENTAS Madrid 350000
30 INFORMÁTICA Valencia 420000
40 CONTABILIDAD Sevilla 150000
50 MARKETING Barcelona 200000
60 LOGÍSTICA Zaragoza 180000
70 INVESTIGACIÓN Bilbao NULL

PROYECTOS (se omiten las fechas)

proyecto_id nombre dept_id presupuesto
1 Migración a la nube 30 120000
2 Nueva tienda online 30 90000
3 Campaña Navidad 2025 50 40000
4 Optimización de almacén 60 35000
5 Plan estratégico 2027 10 NULL

ASIGNACIONES (se omite la fecha)

emp_id proyecto_id horas funcion
120 1 450 Jefe de proyecto
121 1 600 Desarrollador
124 1 380 Administrador de sistemas
120 2 300 Jefe de proyecto
122 2 520 Desarrollador
123 2 410 Desarrollador
140 2 120 Diseñador
104 3 80 Responsable
140 3 200 Diseñador
141 3 160 Analista
110 3 40 Apoyo comercial
105 4 60 Responsable
150 4 150 Operario
152 4 90 Administrativo

2. Operadores primitivos

2.1. Selección (σ)

📘 Concepto: La selección σ[condición](R) devuelve las tuplas de R que cumplen la condición (para las que la condición es verdadera). Es un filtro horizontal: elige filas.

  • Grado del resultado: el mismo que R (no cambia los atributos).
  • Cardinalidad: menor o igual que la de R.
  • La condición se evalúa tupla a tupla y solo puede referirse a atributos de esa tupla y a constantes.

💻 Ejemplo: Empleados del departamento 40.

σ[dept_id = 40](EMPLEADOS)
emp_id nombre apellidos oficio salario comision jefe_id dept_id
103 Pedro Gómez Díaz DIRECTOR 4200 NULL 100 40
130 Isabel Domínguez Vega CONTABLE 2600 NULL 103 40
131 Jorge Vázquez Ramos ADMINISTRATIVO 1650 NULL 103 40

Condiciones compuestas: σ[oficio = 'VENDEDOR' ∧ comision > 400](EMPLEADOS) devuelve a Carmen (500) y David (800).

Propiedades:

  • Conmutativa: σ[c1](σ[c2](R)) = σ[c2](σ[c1](R)).
  • Cascada: σ[c1 ∧ c2](R) = σ[c1](σ[c2](R)).

⚠️ Importante: Con nulos se aplica la lógica trivaluada (módulo 16): la selección solo devuelve las tuplas para las que la condición es V. σ[comision < 500](EMPLEADOS) devuelve 2 tuplas (Elena y Sergio), no 22: las 20 comisiones nulas dan desconocido y se descartan.

2.2. Proyección (π)

📘 Concepto: La proyección π[A1, ..., Ak](R) devuelve una relación con solo los atributos indicados de R, eliminando las tuplas duplicadas que puedan aparecer. Es un filtro vertical: elige columnas.

  • Grado: k (el número de atributos de la lista).
  • Cardinalidad: menor o igual que la de R. Es igual si la lista contiene una clave de R.

💻 Ejemplo: Localidades en las que hay departamentos.

π[localidad](DEPARTAMENTOS)
localidad
Madrid
Valencia
Sevilla
Barcelona
Zaragoza
Bilbao

Hay 7 departamentos pero 6 localidades: Madrid aparecía dos veces (DIRECCIÓN y VENTAS) y la proyección, al devolver un conjunto, la deja una sola vez.

⚠️ Importante: Esta es la diferencia más importante entre el álgebra y SQL. SELECT localidad FROM departamentos; devuelve 7 filas con Madrid repetido; para obtener las 6 del álgebra hay que escribir SELECT DISTINCT localidad FROM departamentos;.

Combinando selección y proyección: “nombre y salario de los empleados del departamento 30”:

π[nombre, salario](σ[dept_id = 30](EMPLEADOS))
nombre salario
Marta 5000
Pablo 3500
Raquel 2800
Iván 2600
Sara 2400
Hugo 2500

💡 Consejo: El orden importa: σ[dept_id = 30](π[nombre, salario](EMPLEADOS)) es incorrecta, porque después de proyectar ya no existe el atributo dept_id. Selecciona primero y proyecta después, salvo que la condición use solo atributos proyectados.

2.3. Compatibilidad de unión

Los operadores de conjuntos (∪, −, ∩) solo se pueden aplicar a dos relaciones compatibles para la unión (union-compatible):

📘 Concepto: R(A1, …, An) y S(B1, …, Bn) son compatibles para la unión si tienen el mismo grado n y, para cada i, los dominios de Ai y Bi son compatibles (el mismo dominio o dominios comparables). Por convenio, el resultado toma los nombres de atributo del primer operando.

¿Compatibles? Expresiones Motivo
Sí π[emp_id](ASIGNACIONES) y π[jefe_id](EMPLEADOS) Grado 1 y ambos son identificadores de empleado
No π[emp_id](EMPLEADOS) y π[emp_id, proyecto_id](ASIGNACIONES) Grado distinto (1 y 2)
No (semánticamente) π[dept_id](DEPARTAMENTOS) y π[proyecto_id](PROYECTOS) Mismo grado y tipo numérico, pero dominios sin relación: la unión no tiene sentido

2.4. Unión (∪)

📘 Concepto: R ∪ S devuelve las tuplas que están en R, en S o en ambas (sin duplicados).

💻 Ejemplo: Empleados asignados al proyecto 1 o al proyecto 2.

π[emp_id](σ[proyecto_id = 1](ASIGNACIONES)) ∪ π[emp_id](σ[proyecto_id = 2](ASIGNACIONES))

El primer operando es {120, 121, 124}; el segundo, {120, 122, 123, 140}. La unión tiene 6 tuplas: 120, 121, 122, 123, 124 y 140 (el 120 aparece una sola vez).

Propiedades: conmutativa y asociativa. Cardinalidad: máx( R , S ) ≤ R ∪ S ≤ R + S .

2.5. Diferencia (−)

📘 Concepto: R − S devuelve las tuplas que están en R pero no en S.

💻 Ejemplo: Departamentos sin empleados.

π[dept_id](DEPARTAMENTOS) − π[dept_id](σ[dept_id IS NOT NULL](EMPLEADOS))

El primer operando es {10, 20, 30, 40, 50, 60, 70}; el segundo, {10, 20, 30, 40, 50, 60}. Resultado: {70}.

⚠️ Importante: La diferencia no es conmutativa: R − S ≠ S − R. Y es el operador clave para expresar negaciones (“que no…”, “sin…”, “nunca…”), que no se pueden expresar solo con σ, π, ∪ y ×.

2.6. Producto cartesiano (×)

📘 Concepto: R × S devuelve todas las combinaciones de una tupla de R con una tupla de S. Si R tiene grado n y cardinalidad r, y S grado m y cardinalidad s, el producto tiene grado n + m y cardinalidad r × s.

💻 Ejemplo: σ[localidad = 'Madrid'](DEPARTAMENTOS) × π[proyecto_id, nombre](PROYECTOS) combina los 2 departamentos de Madrid con los 5 proyectos: 10 tuplas de grado 4 + 2 = 6. Como ambas relaciones tienen un atributo nombre, se cualifican:

dept_id DEPARTAMENTOS.nombre localidad presupuesto proyecto_id PROYECTOS.nombre
10 DIRECCIÓN Madrid 500000 1 Migración a la nube
10 DIRECCIÓN Madrid 500000 2 Nueva tienda online
… … … … … …
20 VENTAS Madrid 350000 5 Plan estratégico 2027

(10 tuplas.) El producto por sí solo rara vez tiene sentido: mezcla tuplas que no tienen nada que ver. Su utilidad es servir de base a la combinación: un producto seguido de una selección que empareja las tuplas relacionadas.

2.7. Renombrado (ρ)

📘 Concepto: ρ[S](R) da el nombre S a la relación R; ρ[S(B1, ..., Bn)](R) además renombra sus atributos. No cambia las tuplas.

Es imprescindible para:

  • Combinar una relación consigo misma (autocombinación): ρ[J](EMPLEADOS) permite tener “dos copias” de EMPLEADOS, una para el empleado y otra para el jefe.
  • Hacer compatibles dos relaciones o controlar qué atributos coinciden en una combinación natural.
  • Dar nombre a resultados intermedios.

2.8. Conjunto completo de operadores

El conjunto {σ, π, ∪, −, ×} (más ρ para resolver conflictos de nombres) es completo: cualquier otro operador del álgebra relacional clásica se puede expresar con ellos. Por eso los demás se llaman derivados: no añaden potencia expresiva, sino comodidad.


3. Operadores derivados

3.1. Intersección (∩)

📘 Concepto: R ∩ S devuelve las tuplas que están a la vez en R y en S. Requiere compatibilidad de unión. Se deriva de la diferencia: R ∩ S = R − (R − S).

💻 Ejemplo: Empleados asignados a la vez a los proyectos 1 y 2: {120, 121, 124} ∩ {120, 122, 123, 140} = {120} (Pablo).

⚠️ Importante: No confundas “empleados asignados al proyecto 1 y al 2” (intersección de dos conjuntos) con σ[proyecto_id = 1 ∧ proyecto_id = 2](ASIGNACIONES), que siempre es vacía: ninguna tupla tiene a la vez dos valores en el mismo atributo.

3.2. Combinación theta y equicombinación

📘 Concepto: La combinación theta (theta join) de R y S con una condición θ es el producto cartesiano seguido de una selección: R ⋈[θ] S = σθ. Si la condición solo usa igualdades, se llama equicombinación (equijoin).

💻 Ejemplo: Empleados con el nombre y la localidad de su departamento (equicombinación por la clave ajena):

EMPLEADOS ⋈[EMPLEADOS.dept_id = DEPARTAMENTOS.dept_id] DEPARTAMENTOS

El resultado tiene 23 tuplas de grado 11 + 4 = 15 (los atributos dept_id aparecen dos veces, con el mismo valor). Tomás (160) no aparece: su dept_id es nulo y NULL = valor nunca es verdadero. El departamento 70 tampoco aparece: ningún empleado lo referencia.

💻 Ejemplo: Combinación theta con desigualdad: empleados que ganan más que Pablo (120).

EMPLEADOS ⋈[EMPLEADOS.salario > P.salario] σ[emp_id = 120](ρ[P](EMPLEADOS))

Devuelve los 6 empleados con salario mayor que 3500: Ana, Luis, Marta, Pedro, Laura y Javier.

3.3. Combinación natural

📘 Concepto: La combinación natural R ⋈ S es una equicombinación sobre todos los atributos con el mismo nombre en R y S, en la que cada atributo común aparece una sola vez en el resultado. Si no hay atributos comunes, equivale al producto cartesiano.

💻 Ejemplo: ASIGNACIONES ⋈ PROYECTOS. El único atributo común es proyecto_id, así que el resultado contiene las 14 asignaciones con los datos de su proyecto (el proyecto 5, sin asignaciones, no aparece), con proyecto_id una sola vez: grado 5 + 6 − 1 = 10.

⚠️ Importante: La combinación natural compara por nombre, no por significado, y es una fuente clásica de errores. En EMPRESA, EMPLEADOS ⋈ DEPARTAMENTOS compara dept_id y también nombre (los dos tienen un atributo nombre): exige que el empleado se llame igual que su departamento, y el resultado es vacío (0 tuplas). Lo mismo ocurre con EMPLEADOS ⋈ PROYECTOS (comparten nombre y dept_id) o DEPARTAMENTOS ⋈ PROYECTOS (comparten dept_id, nombre y presupuesto).

Soluciones: usar una equicombinación con la condición explícita, o renombrar antes los atributos que no deben coincidir:

EMPLEADOS ⋈ ρ[D(dept_id, nombre_dep, localidad, presupuesto)](DEPARTAMENTOS)

Ahora el único atributo común es dept_id y el resultado tiene 23 tuplas, de grado 11 + 4 − 1 = 14.

💡 Consejo: En SQL pasa exactamente lo mismo con NATURAL JOIN. Por eso en el trabajo profesional casi siempre se escribe JOIN ... ON con la condición explícita (módulo 30).

3.4. Semicombinación (⋉) y antisemicombinación (▷)

📘 Concepto: La semicombinación (semijoin) R ⋉[θ] S devuelve las tuplas de R que tienen alguna pareja en S según la condición, sin añadir atributos de S: R ⋉[θ] S = πatributos de R. La antisemicombinación (antijoin) R ▷[θ] S devuelve las tuplas de R que no tienen ninguna pareja en S: R ▷[θ] S = R − (R ⋉[θ] S).

💻 Ejemplo: Departamentos que tienen algún proyecto:

DEPARTAMENTOS ⋉[DEPARTAMENTOS.dept_id = PROYECTOS.dept_id] PROYECTOS

Resultado: 4 tuplas completas de DEPARTAMENTOS (10, 30, 50 y 60). Observa que el 30 tiene dos proyectos pero aparece una vez. Y la antisemicombinación, DEPARTAMENTOS ▷[DEPARTAMENTOS.dept_id = PROYECTOS.dept_id] PROYECTOS, devuelve los 3 restantes: 20, 40 y 70.

En SQL corresponden a EXISTS / IN y a NOT EXISTS / NOT IN (módulo 31). En otros textos la antisemicombinación se escribe también ⊳ o con el símbolo ⋉ tachado.

3.5. División (÷)

📘 Concepto: Sean R(A, B) y S(B), donde A y B son conjuntos de atributos. La división R ÷ S devuelve los valores de A que están relacionados en R con todos los valores de S. Formalmente, una tupla t está en R ÷ S si para toda tupla s de S, la tupla (t, s) está en R.

La división responde a las consultas con “todos”: “empleados que trabajan en todos los proyectos de…”, “clientes que han comprado todos los productos de…”. Se deriva de los operadores primitivos así:

R ÷ S = π[A](R) − π[A]( (π[A](R) × S) − R )

Intuición: π[A](R) × S son todas las parejas que deberían existir si cada candidato estuviera relacionado con todo S; restándole R quedan las parejas que faltan; los candidatos con alguna pareja que falta se descartan.

Ejemplo paso a paso: empleados que trabajan en todos los proyectos del departamento 30

Paso 0. Preparar dividendo y divisor.

R ← π[emp_id, proyecto_id](ASIGNACIONES)            (14 tuplas)
S ← π[proyecto_id](σ[dept_id = 30](PROYECTOS))      (2 tuplas: 1 y 2)

Paso 1. Candidatos. T1 ← π[emp_id](R): los 12 empleados con alguna asignación.

T1 = {104, 105, 110, 120, 121, 122, 123, 124, 140, 141, 150, 152}

Paso 2. Parejas que deberían existir. T2 ← T1 × S: cada candidato con cada proyecto del departamento 30, 12 × 2 = 24 tuplas: (104,1), (104,2), (105,1), (105,2), …, (152,1), (152,2).

Paso 3. Parejas que faltan. T3 ← T2 − R. De las 24 parejas, las que sí están en R son siete: (120,1), (121,1), (124,1), (120,2), (122,2), (123,2) y (140,2). Quedan 17 tuplas:

emp_id proyecto_id   emp_id proyecto_id
104 1   123 1
104 2   124 2
105 1   140 1
105 2   141 1
110 1   141 2
110 2   150 1
121 2   150 2
122 1   152 1
      152 2

Paso 4. Candidatos a los que les falta algo. T4 ← π[emp_id](T3) = {104, 105, 110, 121, 122, 123, 124, 140, 141, 150, 152}: 11 empleados.

Paso 5. Resultado. T1 − T4 = {120}. Pablo Moreno es el único que trabaja en los dos proyectos del departamento 30.

⚠️ Importante: Si el divisor S está vacío (por ejemplo, “todos los proyectos del departamento 70”, que no tiene ninguno), la división devuelve todos los candidatos πA: “para todo elemento de un conjunto vacío” es siempre verdadero. Ten en cuenta este caso límite al interpretar los resultados.

3.6. Combinaciones externas (⟕ ⟖ ⟗)

La combinación normal (interna) pierde las tuplas que no tienen pareja: en EMPLEADOS ⋈[...] DEPARTAMENTOS desaparecen Tomás (sin departamento) y el departamento 70 (sin empleados). Las combinaciones externas las conservan rellenando con nulos.

Operador Nombre Conserva
R ⟕[θ] S Combinación externa izquierda (left outer join) Todas las tuplas de R; las que no tienen pareja, con nulos en los atributos de S
R ⟖[θ] S Combinación externa derecha (right outer join) Todas las tuplas de S, con nulos en los de R si no tienen pareja
R ⟗[θ] S Combinación externa completa (full outer join) Todas las tuplas de R y de S

💻 Ejemplo: Con la condición EMPLEADOS.dept_id = DEPARTAMENTOS.dept_id:

Expresión Tuplas Por qué
EMPLEADOS ⋈[θ] DEPARTAMENTOS 23 Los empleados con departamento
DEPARTAMENTOS ⟕[θ] EMPLEADOS 24 Las 23 anteriores + el departamento 70 con nulos en los atributos de empleado
EMPLEADOS ⟕[θ] DEPARTAMENTOS 24 Las 23 + Tomás con nulos en los atributos de departamento
EMPLEADOS ⟗[θ] DEPARTAMENTOS 25 Las 23 + Tomás + el departamento 70

Fragmento de π[DEPARTAMENTOS.dept_id, DEPARTAMENTOS.nombre, emp_id, EMPLEADOS.nombre](EMPLEADOS ⟗[θ] DEPARTAMENTOS):

DEPARTAMENTOS.dept_id DEPARTAMENTOS.nombre emp_id EMPLEADOS.nombre
10 DIRECCIÓN 100 Ana
20 VENTAS 101 Luis
… … … …
70 INVESTIGACIÓN NULL NULL
NULL NULL 160 Tomás

(25 tuplas.) Las combinaciones externas no forman parte del álgebra original de Codd, que no tenía nulos; se añadieron después y hoy son imprescindibles.

3.7. Operadores extendidos: proyección generalizada y agrupación

Proyección generalizada. Permite expresiones aritméticas y renombrar con →: π[nombre, salario * 14 → salario_anual](EMPLEADOS).

Agrupación (γ). γ[G1, ..., Gk; F1(A1) → N1, ...](R) agrupa las tuplas de R por los valores de los atributos de agrupación G y calcula, para cada grupo, las funciones de agregado COUNT, SUM, AVG, MAX y MIN. El resultado tiene una tupla por grupo.

💻 Ejemplo: Número de empleados, salario medio y máximo de cada departamento.

γ[dept_id; COUNT(*) → num_emp, AVG(salario) → media, MAX(salario) → maximo](EMPLEADOS)
dept_id num_emp media maximo
10 1 6500 6500
20 6 2500 4800
30 6 3133,33 5000
40 3 2816,67 4200
50 3 3100 4300
60 4 2237,5 4000
NULL 1 900 900

(7 tuplas.) Como en SQL, las tuplas con dept_id nulo forman su propio grupo (Tomás). Sin atributos de agrupación, γ devuelve una sola tupla: γ[; COUNT(*) → total](EMPLEADOS) = 24.

Otros operadores del álgebra extendida que encontrarás en la bibliografía: eliminación de duplicados δ y ordenación τ, necesarios cuando el álgebra trabaja con multiconjuntos (como SQL) en lugar de con conjuntos.


4. Expresiones y secuencias de operaciones

Una consulta compleja se puede escribir como una sola expresión anidada o como una secuencia de asignaciones T ← expresión, que da nombre a resultados intermedios. Ambas formas son equivalentes; la secuencia es más legible y más fácil de corregir.

💻 Ejemplo: Nombre y apellidos de los empleados del departamento de INFORMÁTICA asignados al proyecto “Nueva tienda online”.

En una sola expresión:

π[EMPLEADOS.nombre, apellidos](
    σ[dept_id = 30](EMPLEADOS)
    ⋈[EMPLEADOS.emp_id = ASIGNACIONES.emp_id]
    (ASIGNACIONES ⋈[ASIGNACIONES.proyecto_id = PROYECTOS.proyecto_id] σ[nombre = 'Nueva tienda online'](PROYECTOS))
)

En secuencia (más clara, y además no depende de saber que INFORMÁTICA es el 30):

T1 ← π[dept_id](σ[nombre = 'INFORMÁTICA'](DEPARTAMENTOS))                       {30}
T2 ← π[proyecto_id](σ[nombre = 'Nueva tienda online'](PROYECTOS))                {2}
T3 ← π[emp_id](ASIGNACIONES ⋈ T2)                                                {120, 122, 123, 140}
T4 ← EMPLEADOS ⋈ T1                                                              (los 6 empleados del 30)
RESULTADO ← π[nombre, apellidos](T4 ⋈ T3)
nombre apellidos
Pablo Moreno Herrera
Iván Castro Méndez
Sara Ortiz Delgado

(3 tuplas.) Nuria (140) trabaja en el proyecto pero pertenece a MARKETING. Observa que las combinaciones naturales de la secuencia son seguras porque T1, T2 y T3 tienen un único atributo, que es justo el que debe coincidir.

💡 Consejo: Método para resolver cualquier consulta: 1) identifica qué relaciones contienen los datos pedidos y los datos de las condiciones; 2) selecciona cuanto antes en cada una; 3) combina por las claves ajenas; 4) proyecta al final los atributos pedidos. Si la consulta contiene “no”, “nunca” o “sin”, piensa en una diferencia; si contiene “todos”, en una división.


5. Propiedades, equivalencias y optimización

5.1. Reglas de equivalencia

Dos expresiones son equivalentes (≡) si devuelven la misma relación para cualquier extensión de la base de datos. Estas son las equivalencias más usadas:

N.º Equivalencia Nombre
1 σ[c1 ∧ c2](R) ≡ σ[c1](σ[c2](R)) Cascada (descomposición) de selecciones
2 σ[c1](σ[c2](R)) ≡ σ[c2](σ[c1](R)) Conmutatividad de la selección
3 π[L1](π[L2](R)) ≡ π[L1](R), si L1 ⊆ L2 Cascada de proyecciones
4 π[L](σ[c](R)) ≡ σ[c](π[L](R)), si c solo usa atributos de L Conmutación de σ y π
5 R ⋈ S ≡ S ⋈ R, R × S ≡ S × R Conmutatividad de ⋈ y × (salvo el orden de atributos)
6 (R ⋈ S) ⋈ T ≡ R ⋈ (S ⋈ T) Asociatividad de ⋈ y ×
7 σ[c](R ⋈ S) ≡ σ[c](R) ⋈ S, si c solo usa atributos de R Empujar selecciones a través de ⋈ y ×
8 σ[c1 ∧ c2](R ⋈ S) ≡ σ[c1](R) ⋈ σ[c2](S) (c1 de R, c2 de S) Empujar selecciones a ambos lados
9 π[L](R ⋈[θ] S) ≡ π[L](π[L1](R) ⋈[θ] π[L2](S)), con L1 y L2 los atributos de L y de θ en cada relación Empujar proyecciones
10 σ[θ](R × S) ≡ R ⋈[θ] S Producto + selección = combinación
11 R ∪ S ≡ S ∪ R, R ∩ S ≡ S ∩ R (la − no) Conmutatividad de ∪ y ∩
12 σ[c](R ∪ S) ≡ σ[c](R) ∪ σ[c](S), igual con ∩ y − Distribución de σ sobre operaciones de conjuntos
13 π[L](R ∪ S) ≡ π[L](R) ∪ π[L](S) Distribución de π sobre ∪ (no sobre ∩ ni −)

⚠️ Importante: La proyección no se distribuye sobre la diferencia. Contraejemplo: R = {(1,’a’)}, S = {(1,’b’)}. π[1](R − S) = {1}, pero π[1](R) − π[1](S) = {1} − {1} = ∅.

5.2. Uso en la optimización

El optimizador del SGBD recibe la consulta, la traduce a un árbol de consulta (hojas = relaciones, nodos internos = operadores) y lo transforma con las equivalencias anteriores siguiendo unas heurísticas:

  1. Descomponer las selecciones conjuntivas en cascada (regla 1).
  2. Empujar las selecciones lo más abajo posible del árbol (reglas 2, 7, 8, 12): cuanto antes se filtre, menos tuplas viajan hacia arriba.
  3. Sustituir los productos seguidos de selección por combinaciones (regla 10).
  4. Reordenar las combinaciones para ejecutar primero las más restrictivas (reglas 5 y 6).
  5. Empujar las proyecciones hacia abajo para descartar atributos innecesarios (reglas 3, 4, 9).

💻 Ejemplo: “Nombre y apellidos de los empleados de departamentos situados en Valencia que trabajan en proyectos con presupuesto superior a 100 000 €”.

Expresión canónica (la traducción directa, como haría un SELECT con varias tablas en el FROM):

π[E.nombre, E.apellidos](
  σ[D.localidad = 'Valencia' ∧ P.presupuesto > 100000 ∧ E.dept_id = D.dept_id
    ∧ E.emp_id = A.emp_id ∧ A.proyecto_id = P.proyecto_id](
      ρ[E](EMPLEADOS) × ρ[D](DEPARTAMENTOS) × ρ[A](ASIGNACIONES) × ρ[P](PROYECTOS)))

Árbol canónico:

flowchart TB
    c1["π[E.nombre, E.apellidos]"] --> c2["σ[localidad = 'Valencia' ∧ presupuesto > 100000 ∧ 3 condiciones de combinación]"]
    c2 --> c3["×"]
    c3 --> c4["×"]
    c3 --> c5["P: PROYECTOS (5)"]
    c4 --> c6["×"]
    c4 --> c7["A: ASIGNACIONES (14)"]
    c6 --> c8["E: EMPLEADOS (24)"]
    c6 --> c9["D: DEPARTAMENTOS (7)"]

El producto de las cuatro relaciones genera 24 × 7 × 14 × 5 = 11 760 tuplas intermedias, de las que la selección se queda con 3.

Árbol optimizado (selecciones empujadas a las hojas y productos convertidos en combinaciones):

flowchart TB
    o1["π[E.nombre, E.apellidos]"] --> o2["⋈[E.emp_id = A.emp_id] (3 tuplas)"]
    o2 --> o3["⋈[E.dept_id = D.dept_id] (6 tuplas)"]
    o2 --> o4["⋈[A.proyecto_id = P.proyecto_id] (3 tuplas)"]
    o3 --> o5["E: EMPLEADOS (24)"]
    o3 --> o6["σ[localidad = 'Valencia'] (1 tupla)"]
    o6 --> o7["D: DEPARTAMENTOS (7)"]
    o4 --> o8["A: ASIGNACIONES (14)"]
    o4 --> o9["σ[presupuesto > 100000] (1 tupla)"]
    o9 --> o10["P: PROYECTOS (5)"]

Ahora el resultado intermedio más grande tiene 6 tuplas. Ambos árboles devuelven lo mismo:

nombre apellidos
Pablo Moreno Herrera
Raquel Molina Prieto
Hugo Rubio Marín

💡 Consejo: En SQL no tienes que hacer esta optimización a mano: el optimizador de Oracle la hace por ti. Pero entenderla te permite leer un plan de ejecución (EXPLAIN PLAN) y saber por qué una consulta es lenta.


6. Cálculo relacional

6.1. Cálculo relacional de tuplas (CRT)

📘 Concepto: En el cálculo relacional de tuplas una consulta tiene la forma **{ t P(t) }: “el conjunto de tuplas t que cumplen el predicado P”. Las variables recorren **tuplas de relaciones.

Elementos de las fórmulas:

  • Átomos: R(t) (t es una tupla de R), t.A op s.B, t.A op constante, con op ∈ {=, ≠, <, ≤, >, ≥}.
  • Conectores: ∧, ∨, ¬, → (implica).
  • Cuantificadores: ∃ (existe) y ∀ (para todo). Una variable cuantificada está ligada; la que aparece a la izquierda de la barra es libre.
Consulta Cálculo de tuplas
Empleados con salario mayor que 3000 { t \| EMPLEADOS(t) ∧ t.salario > 3000 }
Nombre y salario de esos empleados { t.nombre, t.salario \| EMPLEADOS(t) ∧ t.salario > 3000 }
Empleados de departamentos de Valencia con el nombre del departamento { e.nombre, d.nombre \| EMPLEADOS(e) ∧ DEPARTAMENTOS(d) ∧ e.dept_id = d.dept_id ∧ d.localidad = 'Valencia' }
Departamentos con algún proyecto (∃) { d \| DEPARTAMENTOS(d) ∧ ∃p (PROYECTOS(p) ∧ p.dept_id = d.dept_id) }
Empleados sin ninguna asignación (¬∃) { e \| EMPLEADOS(e) ∧ ¬∃a (ASIGNACIONES(a) ∧ a.emp_id = e.emp_id) }

La división se expresa de forma muy natural con ∀: “empleados que trabajan en todos los proyectos del departamento 30”:

{ e.emp_id | EMPLEADOS(e) ∧ ∀p ( (PROYECTOS(p) ∧ p.dept_id = 30)
                                  → ∃a (ASIGNACIONES(a) ∧ a.emp_id = e.emp_id ∧ a.proyecto_id = p.proyecto_id) ) }

Como SQL no tiene ∀, se usa la equivalencia ∀x P(x) ≡ ¬∃x ¬P(x): “no existe ningún proyecto del departamento 30 en el que no exista una asignación del empleado”. Es el famoso doble NOT EXISTS de SQL (sección 7).

⚠️ Importante: Una expresión como { t | ¬EMPLEADOS(t) } (“todas las tuplas que no son empleados”) describe un conjunto infinito. Se llaman expresiones inseguras y no se admiten. Una expresión es segura si todos los valores de su resultado proceden de las relaciones de la base de datos (de su dominio activo).

6.2. Cálculo relacional de dominios (CRD)

📘 Concepto: En el cálculo relacional de dominios las variables recorren valores de los dominios (no tuplas). Una consulta tiene la forma **{ ⟨x1, …, xn⟩ P(x1, …, xn) }**, y el átomo R(x1, ..., xm) significa que la tupla formada por esos valores pertenece a R (por posición).

Usando DEPARTAMENTOS(dept_id, nombre, localidad, presupuesto) y PROYECTOS(proyecto_id, nombre, dept_id, fecha_inicio, fecha_fin, presupuesto):

Consulta Cálculo de dominios
Nombre de los departamentos de Madrid { ⟨n⟩ \| ∃d ∃p DEPARTAMENTOS(d, n, 'Madrid', p) }
Nombre de cada proyecto y localidad de su departamento { ⟨np, l⟩ \| ∃i ∃d ∃fi ∃ff ∃pp ∃nd ∃pd (PROYECTOS(i, np, d, fi, ff, pp) ∧ DEPARTAMENTOS(d, nd, l, pd)) }
Departamentos sin proyectos { ⟨d, n⟩ \| ∃l ∃p (DEPARTAMENTOS(d, n, l, p) ∧ ¬∃i ∃np ∃fi ∃ff ∃pp PROYECTOS(i, np, d, fi, ff, pp)) }

El primer resultado es {DIRECCIÓN, VENTAS}; el segundo empareja cada proyecto con la localidad de su departamento (Migración a la nube - Valencia, …); el tercero devuelve (20, VENTAS), (40, CONTABILIDAD) y (70, INVESTIGACIÓN). Fíjate en que la igualdad se expresa repitiendo la misma variable (d en las dos relaciones), que es la idea que usaba QBE.

6.3. Teorema de equivalencia

📘 Concepto: Teorema de Codd: el álgebra relacional, el cálculo relacional de tuplas seguro y el cálculo relacional de dominios seguro tienen la misma potencia expresiva. Cualquier consulta escrita en uno de ellos se puede escribir en los otros dos.


7. Equivalencia con SQL

Álgebra SQL (Oracle) Observaciones
σ[c](R) SELECT * FROM r WHERE c La condición sigue la lógica trivaluada
π[A, B](R) SELECT DISTINCT a, b FROM r Sin DISTINCT, SQL conserva duplicados
ρ[S](R) FROM r s (alias de tabla), AS para columnas Oracle no admite AS en los alias de tabla
R ∪ S UNION UNION ALL conserva duplicados
R − S MINUS Estándar: EXCEPT (Oracle lo admite desde 21c)
R ∩ S INTERSECT  
R × S FROM r CROSS JOIN s o FROM r, s  
R ⋈[θ] S FROM r JOIN s ON θ O condición en WHERE (sintaxis tradicional)
R ⋈ S FROM r NATURAL JOIN s Mismo riesgo con atributos homónimos
R ⋉[θ] S WHERE EXISTS (SELECT ... FROM s WHERE θ) o IN  
R ▷[θ] S WHERE NOT EXISTS (...) Cuidado con NOT IN y los nulos
R ÷ S Doble NOT EXISTS, o GROUP BY ... HAVING COUNT(...) = (SELECT COUNT(*) ...) SQL no tiene operador de división
R ⟕[θ] S, ⟖, ⟗ LEFT, RIGHT, FULL OUTER JOIN ... ON θ Oracle también tiene la notación antigua (+)
γ[G; F](R) SELECT g, f FROM r GROUP BY g σ sobre el resultado de γ = HAVING
T ← expresión WITH t AS (...) (CTE) o una vista  

💻 Ejemplo: La división del apartado 3.5 en SQL, con doble NOT EXISTS (“empleados para los que no existe ningún proyecto del departamento 30 en el que no estén asignados”):

SELECT e.emp_id, e.nombre
FROM   empleados e
WHERE  NOT EXISTS (
           SELECT 1
           FROM   proyectos p
           WHERE  p.dept_id = 30
           AND    NOT EXISTS (
                      SELECT 1
                      FROM   asignaciones a
                      WHERE  a.emp_id = e.emp_id
                      AND    a.proyecto_id = p.proyecto_id
                  )
       );

Y con agrupación (cuenta los proyectos del 30 en los que está cada empleado y compara con el total):

SELECT a.emp_id
FROM   asignaciones a
       JOIN proyectos p ON p.proyecto_id = a.proyecto_id
WHERE  p.dept_id = 30
GROUP  BY a.emp_id
HAVING COUNT(*) = (SELECT COUNT(*) FROM proyectos WHERE dept_id = 30);

Las dos devuelven el empleado 120. Ojo: la versión con doble NOT EXISTS devolvería todos los empleados si el departamento no tuviera proyectos (división por conjunto vacío), mientras que la de GROUP BY no devolvería ninguno.


8. Colección de consultas resueltas sobre EMPRESA

Intenta resolver cada consulta antes de desplegar la solución. Los resultados son los que se obtienen con los datos de ../recursos/empresa_datos.sql. Recuerda: el álgebra trabaja con conjuntos, así que el orden de las tuplas del resultado es irrelevante y nunca hay duplicados.

Consulta 1: Empleados con salario superior a 3000 €

Nombre, apellidos y salario.

Ver solución
π[nombre, apellidos, salario](σ[salario > 3000](EMPLEADOS))
nombre apellidos salario
Ana García López 6500
Luis Martínez Ruiz 4800
Marta Sánchez Gil 5000
Pedro Gómez Díaz 4200
Laura Fernández Moreno 4300
Javier López Navarro 4000
Pablo Moreno Herrera 3500

7 tuplas. SQL: SELECT nombre, apellidos, salario FROM empleados WHERE salario > 3000; (sin DISTINCT el resultado es el mismo porque la combinación de atributos no se repite).

Consulta 2: Oficios distintos que existen en la empresa

Ver solución
π[oficio](EMPLEADOS)
oficio
ADMINISTRATIVO
ANALISTA
BECARIO
CONTABLE
DIRECTOR
DIRECTOR GENERAL
DISEÑADOR
OPERARIO
PROGRAMADOR
TÉCNICO SISTEMAS
VENDEDOR

11 tuplas (de 24 empleados). SQL: SELECT DISTINCT oficio FROM empleados;. Sin DISTINCT, SQL devolvería 24 filas.

Consulta 3: Vendedores con comisión superior a 400 €

Identificador, nombre, salario y comisión.

Ver solución
π[emp_id, nombre, salario, comision](σ[oficio = 'VENDEDOR' ∧ comision > 400](EMPLEADOS))
emp_id nombre salario comision
110 Carmen 2200 500
111 David 2100 800

2 tuplas. SQL: SELECT emp_id, nombre, salario, comision FROM empleados WHERE oficio = 'VENDEDOR' AND comision > 400;

Consulta 4: Productos no descatalogados con precio entre 100 y 300 €

Ver solución
π[producto_id, nombre, precio](σ[precio ≥ 100 ∧ precio ≤ 300 ∧ descatalogado = 'N'](PRODUCTOS))
producto_id nombre precio
9 Monitor 24 FHD 139.00
11 Smartphone Gama Media 299.00
15 Silla Ergonómica 249.00
18 Impresora Láser 189.00
20 Tablet 10 279.00

5 tuplas. SQL: ... WHERE precio BETWEEN 100 AND 300 AND descatalogado = 'N';

Consulta 5: Empleados de los departamentos situados en Valencia

Nombre y apellidos del empleado, nombre y localidad del departamento.

Ver solución
π[EMPLEADOS.nombre, apellidos, DEPARTAMENTOS.nombre, localidad](
    EMPLEADOS ⋈[EMPLEADOS.dept_id = DEPARTAMENTOS.dept_id] σ[localidad = 'Valencia'](DEPARTAMENTOS))
EMPLEADOS.nombre apellidos DEPARTAMENTOS.nombre localidad
Marta Sánchez Gil INFORMÁTICA Valencia
Pablo Moreno Herrera INFORMÁTICA Valencia
Raquel Molina Prieto INFORMÁTICA Valencia
Iván Castro Méndez INFORMÁTICA Valencia
Sara Ortiz Delgado INFORMÁTICA Valencia
Hugo Rubio Marín INFORMÁTICA Valencia

6 tuplas. No se puede usar la combinación natural (EMPLEADOS y DEPARTAMENTOS comparten nombre).

SELECT e.nombre, e.apellidos, d.nombre AS departamento, d.localidad
FROM   empleados e
       JOIN departamentos d ON e.dept_id = d.dept_id
WHERE  d.localidad = 'Valencia';

Consulta 6: Clientes de las provincias de Madrid o Sevilla con el nombre de su comercial

Ver solución
π[CLIENTES.nombre, EMPLEADOS.nombre](
    σ[provincia = 'Madrid' ∨ provincia = 'Sevilla'](CLIENTES)
    ⋈[CLIENTES.comercial_id = EMPLEADOS.emp_id] EMPLEADOS)
CLIENTES.nombre EMPLEADOS.nombre
María José Herrera David
Academia Nova David
Clínica Dental Sonrisa Elena
Antonio Ruiz Pérez Elena

4 tuplas. Los cuatro clientes de esas provincias tienen comercial; si alguno no lo tuviera, la combinación interna lo perdería (haría falta ⟕).

Consulta 7: Identificadores de los empleados que son jefes de alguien

Ver solución
π[jefe_id](σ[jefe_id IS NOT NULL](EMPLEADOS))

Resultado (7 tuplas): 100, 101, 102, 103, 104, 105 y 120.

La selección evita que el nulo de Ana aparezca en el resultado. SQL: SELECT DISTINCT jefe_id FROM empleados WHERE jefe_id IS NOT NULL;

Consulta 8: Empleados que no son jefes de nadie

Ver solución
π[emp_id](EMPLEADOS) − ρ[J(emp_id)](π[jefe_id](σ[jefe_id IS NOT NULL](EMPLEADOS)))

El renombrado hace que el segundo operando tenga el mismo nombre de atributo (en rigor, basta con que sean compatibles). Resultado (17 tuplas): 110, 111, 112, 113, 114, 121, 122, 123, 124, 130, 131, 140, 141, 150, 151, 152 y 160.

SELECT emp_id FROM empleados
MINUS
SELECT jefe_id FROM empleados WHERE jefe_id IS NOT NULL;

⚠️ Importante: Si en SQL lo escribes como WHERE emp_id NOT IN (SELECT jefe_id FROM empleados) sin filtrar los nulos, el resultado es vacío, porque la subconsulta contiene el NULL de Ana.

Consulta 9: Departamentos sin empleados

Identificador, nombre y localidad.

Ver solución
T1 ← π[dept_id](DEPARTAMENTOS) − π[dept_id](σ[dept_id IS NOT NULL](EMPLEADOS))
RESULTADO ← π[dept_id, nombre, localidad](DEPARTAMENTOS ⋈ T1)
dept_id nombre localidad
70 INVESTIGACIÓN Bilbao

1 tupla. La diferencia solo puede hacerse sobre relaciones compatibles, por eso se calcula sobre dept_id y luego se recuperan los demás atributos con una combinación natural (segura: T1 solo tiene dept_id). Equivale a la antisemicombinación DEPARTAMENTOS ▷[DEPARTAMENTOS.dept_id = EMPLEADOS.dept_id] EMPLEADOS.

Consulta 10: Clientes que no han hecho ningún pedido

Ver solución
T1 ← π[cliente_id](CLIENTES) − π[cliente_id](PEDIDOS)
RESULTADO ← π[cliente_id, nombre](CLIENTES ⋈ T1)
cliente_id nombre
1013 Carlos Ibáñez Rey
1014 Farmacia Central

2 tuplas.

SELECT c.cliente_id, c.nombre
FROM   clientes c
WHERE  NOT EXISTS (SELECT 1 FROM pedidos p WHERE p.cliente_id = c.cliente_id);

Consulta 11: Productos que nunca se han vendido

Ver solución
T1 ← π[producto_id](PRODUCTOS) − π[producto_id](LINEAS_PEDIDO)
RESULTADO ← π[producto_id, nombre](PRODUCTOS ⋈ T1)
producto_id nombre
14 Altavoz Inteligente

1 tupla: el producto descatalogado. Todos los demás aparecen en al menos una línea de pedido.

Consulta 12: Empleados asignados a la vez a los proyectos 1 y 2

Ver solución
T1 ← π[emp_id](σ[proyecto_id = 1](ASIGNACIONES)) ∩ π[emp_id](σ[proyecto_id = 2](ASIGNACIONES))
RESULTADO ← π[emp_id, nombre](EMPLEADOS ⋈ T1)
emp_id nombre
120 Pablo

1 tupla. SQL: dos SELECT unidos con INTERSECT.

Consulta 13: Empleados asignados al proyecto 1 o al proyecto 2

Ver solución
π[emp_id](σ[proyecto_id = 1](ASIGNACIONES)) ∪ π[emp_id](σ[proyecto_id = 2](ASIGNACIONES))

Resultado (6 tuplas): 120, 121, 122, 123, 124 y 140. Aquí también valdría una sola selección con ∨: π[emp_id](σ[proyecto_id = 1 ∨ proyecto_id = 2](ASIGNACIONES)), porque las dos condiciones se refieren a la misma relación.

Consulta 14: Empleados asignados al proyecto 1 pero no al 2

Ver solución
π[emp_id](σ[proyecto_id = 1](ASIGNACIONES)) − π[emp_id](σ[proyecto_id = 2](ASIGNACIONES))

Resultado (2 tuplas): 121 (Raquel) y 124 (Hugo). Error típico: π[emp_id](σ[proyecto_id = 1 ∧ proyecto_id ≠ 2](ASIGNACIONES)) devuelve 120, 121 y 124, porque la condición se evalúa tupla a tupla y la tupla (120, 1) la cumple.

SELECT emp_id FROM asignaciones WHERE proyecto_id = 1
MINUS
SELECT emp_id FROM asignaciones WHERE proyecto_id = 2;

Consulta 15: Nombre de cada empleado del departamento 30 y el nombre de su jefe

Ver solución
π[E.nombre, J.nombre](σ[E.dept_id = 30](ρ[E](EMPLEADOS)) ⋈[E.jefe_id = J.emp_id] ρ[J](EMPLEADOS))
E.nombre J.nombre
Marta Ana
Pablo Marta
Raquel Pablo
Iván Pablo
Sara Pablo
Hugo Marta

6 tuplas. El renombrado crea dos “copias” de EMPLEADOS con papeles distintos (empleado y jefe).

SELECT e.nombre AS empleado, j.nombre AS jefe
FROM   empleados e
       JOIN empleados j ON e.jefe_id = j.emp_id
WHERE  e.dept_id = 30;

Consulta 16: Empleados que ganan más que su jefe

Ver solución
π[E.emp_id, E.nombre](ρ[E](EMPLEADOS) ⋈[E.jefe_id = J.emp_id ∧ E.salario > J.salario] ρ[J](EMPLEADOS))

Resultado: vacío (0 tuplas). En EMPRESA todos los jefes ganan más que sus subordinados. Un resultado vacío es una respuesta perfectamente válida: no indica que la consulta esté mal.

Consulta 17: Empleados cuyo jefe trabaja en un departamento distinto

Ver solución
π[E.emp_id, E.nombre, E.dept_id, J.nombre, J.dept_id](
    ρ[E](EMPLEADOS) ⋈[E.jefe_id = J.emp_id ∧ E.dept_id ≠ J.dept_id] ρ[J](EMPLEADOS))
E.emp_id E.nombre E.dept_id J.nombre J.dept_id
101 Luis 20 Ana 10
102 Marta 30 Ana 10
103 Pedro 40 Ana 10
104 Laura 50 Ana 10
105 Javier 60 Ana 10

5 tuplas. Tomás (160) tiene jefe (Marta, del 30) pero su dept_id es nulo: NULL ≠ 30 es desconocido y la tupla se descarta. Si quisieras incluirlo, la condición sería E.dept_id ≠ J.dept_id ∨ E.dept_id IS NULL.

Consulta 18: Empleados que trabajan en algún proyecto de un departamento distinto al suyo

Ver solución
T1 ← EMPLEADOS ⋈ ASIGNACIONES
RESULTADO ← π[emp_id, T1.nombre, T1.proyecto_id](
    T1 ⋈[T1.proyecto_id = PROYECTOS.proyecto_id ∧ T1.dept_id ≠ PROYECTOS.dept_id] PROYECTOS)
emp_id nombre proyecto_id
110 Carmen 3
140 Nuria 2

2 tuplas: Carmen (VENTAS) en la campaña de MARKETING, y Nuria (MARKETING) en la tienda online de INFORMÁTICA. La primera combinación natural es segura (EMPLEADOS y ASIGNACIONES solo comparten emp_id), pero la segunda no podría ser natural: T1 y PROYECTOS comparten proyecto_id, nombre y dept_id, y la natural exigiría justo lo contrario de lo que se pide (mismo departamento).

Consulta 19: Empleados que ganan más que Pablo Moreno (120)

Ver solución
π[EMPLEADOS.nombre, EMPLEADOS.salario](
    EMPLEADOS ⋈[EMPLEADOS.salario > P.salario] σ[emp_id = 120](ρ[P](EMPLEADOS)))
nombre salario
Ana 6500
Luis 4800
Marta 5000
Pedro 4200
Laura 4300
Javier 4000

6 tuplas. En SQL se resolvería con una subconsulta escalar: WHERE salario > (SELECT salario FROM empleados WHERE emp_id = 120).

Consulta 20: Empleado o empleados con el salario máximo, sin usar agregados

Ver solución

Idea: el salario máximo es el de los empleados que no ganan menos que ningún otro. Primero se calculan los que ganan menos que alguien y luego se restan.

T1 ← π[E1.emp_id](ρ[E1](EMPLEADOS) ⋈[E1.salario < E2.salario] ρ[E2](EMPLEADOS))
RESULTADO ← π[emp_id, nombre, salario](EMPLEADOS ⋈ (π[emp_id](EMPLEADOS) − T1))
emp_id nombre salario
100 Ana 6500

1 tupla. T1 contiene a los 23 empleados que ganan menos que alguien. Es un patrón clásico de examen; con el álgebra extendida sería γ[; MAX(salario)](EMPLEADOS) y una combinación.

Consulta 21: Departamentos que tienen algún proyecto

Ver solución
DEPARTAMENTOS ⋉[DEPARTAMENTOS.dept_id = PROYECTOS.dept_id] PROYECTOS
dept_id nombre localidad presupuesto
10 DIRECCIÓN Madrid 500000
30 INFORMÁTICA Valencia 420000
50 MARKETING Barcelona 200000
60 LOGÍSTICA Zaragoza 180000

4 tuplas.

SELECT d.*
FROM   departamentos d
WHERE  EXISTS (SELECT 1 FROM proyectos p WHERE p.dept_id = d.dept_id);

Consulta 22: Empleados no asignados a ningún proyecto

Ver solución
π[emp_id, nombre, oficio](EMPLEADOS ▷[EMPLEADOS.emp_id = ASIGNACIONES.emp_id] ASIGNACIONES)
emp_id nombre oficio
100 Ana DIRECTOR GENERAL
101 Luis DIRECTOR
102 Marta DIRECTOR
103 Pedro DIRECTOR
111 David VENDEDOR
112 Elena VENDEDOR
113 Sergio VENDEDOR
114 Lucía ADMINISTRATIVO
130 Isabel CONTABLE
131 Jorge ADMINISTRATIVO
151 Manuel OPERARIO
160 Tomás BECARIO

12 tuplas (24 empleados − 12 con asignaciones). SQL: WHERE NOT EXISTS (SELECT 1 FROM asignaciones a WHERE a.emp_id = e.emp_id).

Consulta 23: Pedidos de clientes de la ciudad de Madrid gestionados por empleados del departamento 20

Número de pedido, nombre del cliente y nombre del empleado.

Ver solución
C ← σ[ciudad = 'Madrid'](CLIENTES)
E ← σ[dept_id = 20](EMPLEADOS)
RESULTADO ← π[pedido_id, C.nombre, E.nombre](
    (PEDIDOS ⋈[PEDIDOS.cliente_id = C.cliente_id] C) ⋈[PEDIDOS.empleado_id = E.emp_id] E)
pedido_id C.nombre E.nombre
5003 María José Herrera David
5004 Academia Nova David
5012 Academia Nova David
5015 María José Herrera David

4 tuplas. Las selecciones se aplican antes de combinar (heurística de optimización).

Consulta 24: Productos de las subcategorías de “Periféricos”

Nombre del producto y nombre de su categoría.

Ver solución
PADRE ← π[categoria_id](σ[nombre = 'Periféricos'](CATEGORIAS))                         {4}
HIJAS ← π[C.categoria_id, C.nombre](ρ[C](CATEGORIAS) ⋈[C.categoria_padre_id = PADRE.categoria_id] PADRE)
RESULTADO ← π[PRODUCTOS.nombre, HIJAS.nombre](PRODUCTOS ⋈[PRODUCTOS.categoria_id = HIJAS.categoria_id] HIJAS)
PRODUCTOS.nombre HIJAS.nombre
Ratón inalámbrico Ratones y teclados
Teclado mecánico Ratones y teclados
Pack teclado y ratón Ratones y teclados
Monitor 24 FHD Monitores
Monitor 27 4K Monitores

5 tuplas. Los productos asignados directamente a “Periféricos” (Webcam HD e Impresora Láser) no aparecen: no están en una subcategoría. El álgebra clásica no puede recorrer una jerarquía de profundidad arbitraria (necesitaría recursividad); SQL lo hace con CONNECT BY o con CTE recursivas (módulos 32 y 33).

Consulta 25: Empleados que trabajan en todos los proyectos del departamento 30

Ver solución
R ← π[emp_id, proyecto_id](ASIGNACIONES)
S ← π[proyecto_id](σ[dept_id = 30](PROYECTOS))
RESULTADO ← π[emp_id, nombre](EMPLEADOS ⋈ (R ÷ S))
emp_id nombre
120 Pablo

1 tupla. El desarrollo paso a paso está en el apartado 3.5 y las dos versiones SQL en la sección 7.

Consulta 26: Clientes que han comprado todos los productos de la categoría “Monitores”

Ver solución
R ← π[cliente_id, producto_id](PEDIDOS ⋈ LINEAS_PEDIDO)
S ← π[producto_id](PRODUCTOS ⋈ π[categoria_id](σ[nombre = 'Monitores'](CATEGORIAS)))
RESULTADO ← π[cliente_id, nombre](CLIENTES ⋈ (R ÷ S))

S = {9, 10}. Las tuplas de R con productos de S son: (1001, 9), (1001, 10), (1002, 9), (1002, 10), (1004, 10), (1007, 9), (1010, 9) y (1012, 10). Solo dos clientes tienen ambos:

cliente_id nombre
1001 Talleres Martín S.L.
1002 Construcciones Levante S.A.

2 tuplas. PEDIDOS ⋈ LINEAS_PEDIDO es segura (solo comparten pedido_id), igual que PRODUCTOS ⋈ π[categoria_id](...); en cambio PRODUCTOS ⋈ CATEGORIAS sin proyectar compararía también nombre. Observa que el 1001 compró el monitor 9 en el pedido 5001 y el 10 en el 5021: la división no exige que sea en el mismo pedido.

Consulta 27: Número de empleados y salario medio por departamento, solo de los departamentos con más de 3 empleados

Ver solución
T1 ← γ[dept_id; COUNT(*) → num_emp, AVG(salario) → media](σ[dept_id IS NOT NULL](EMPLEADOS))
RESULTADO ← σ[num_emp > 3](T1)
dept_id num_emp media
20 6 2500
30 6 3133,33
60 4 2237,5

3 tuplas. La selección después de γ corresponde a HAVING:

SELECT dept_id, COUNT(*) AS num_emp, ROUND(AVG(salario), 2) AS media
FROM   empleados
WHERE  dept_id IS NOT NULL
GROUP  BY dept_id
HAVING COUNT(*) > 3;

Consulta 28: Horas totales y número de empleados de cada proyecto, incluidos los proyectos sin asignaciones

Ver solución
T1 ← PROYECTOS ⟕[PROYECTOS.proyecto_id = ASIGNACIONES.proyecto_id] ASIGNACIONES
RESULTADO ← γ[PROYECTOS.proyecto_id, nombre; COUNT(emp_id) → num_emp, SUM(horas) → total_horas](T1)
proyecto_id nombre num_emp total_horas
1 Migración a la nube 3 1430
2 Nueva tienda online 4 1350
3 Campaña Navidad 2025 4 480
4 Optimización de almacén 3 300
5 Plan estratégico 2027 0 NULL

5 tuplas. Con una combinación interna, el proyecto 5 desaparecería. COUNT(emp_id) cuenta valores no nulos (0 para el proyecto 5), mientras que COUNT(*) contaría la tupla rellenada con nulos y daría 1: un error muy frecuente.

Consulta 29: Importe de cada pedido del cliente 1001

El importe de una línea es cantidad × precio_unitario × (1 − descuento / 100).

Ver solución
T1 ← π[pedido_id](σ[cliente_id = 1001](PEDIDOS))
T2 ← π[pedido_id, cantidad * precio_unitario * (1 − descuento / 100) → importe_linea](LINEAS_PEDIDO ⋈ T1)
RESULTADO ← γ[pedido_id; SUM(importe_linea) → importe](T2)
pedido_id importe
5001 1802.90
5006 439.20
5021 2462.05

3 tuplas. Por ejemplo, 5021 = 1 × 1899 × 0,95 + 2 × 329 = 1804,05 + 658 = 2462,05.

⚠️ Importante: Cuidado con la proyección T2: si dos líneas del mismo pedido tuvieran el mismo importe, la proyección (que elimina duplicados) dejaría solo una y la suma saldría mal. Lo correcto es conservar num_linea en T2: π[pedido_id, num_linea, ... → importe_linea]. Es un buen ejemplo de por qué SQL trabaja con multiconjuntos.


9. Ejercicios

Ejercicio 1: Grado y cardinalidad

Sin calcular las tuplas, indica el grado y la cardinalidad de: a) DEPARTAMENTOS × PROYECTOS b) σ[dept_id = 30](EMPLEADOS) c) π[dept_id](PROYECTOS) d) EMPLEADOS ⟗[EMPLEADOS.dept_id = DEPARTAMENTOS.dept_id] DEPARTAMENTOS e) π[emp_id](ASIGNACIONES) f) EMPLEADOS ⋈ DEPARTAMENTOS

Ver solución
Expresión Grado Cardinalidad
a) 4 + 6 = 10 7 × 5 = 35
b) 11 6
c) 1 4 (30, 50, 60 y 10; el 30 se repite)
d) 11 + 4 = 15 25
e) 1 12
f) 11 + 4 − 2 = 13 0 (comparan dept_id y nombre)

Ejercicio 2: Operadores derivados con primitivos

Escribe usando solo σ, π, ∪, −, × y ρ: a) R ∩ S b) R ⋈[R.a = S.b] S c) R ⋉[R.a = S.b] S, con R(a, c) y S(b, d) d) R ÷ S, con R(x, y) y S(y)

Ver solución
a) R ∩ S          = R − (R − S)
b) R ⋈[R.a = S.b] S = σ[R.a = S.b](R × S)
c) R ⋉[R.a = S.b] S = π[a, c](σ[R.a = S.b](R × S))
d) R ÷ S          = π[x](R) − π[x]((π[x](R) × S) − R)

En d), la diferencia interna es válida porque π[x](R) × S tiene los atributos (x, y), los mismos que R.

Ejercicio 3: División a mano

Universidad. Dadas MATRICULAS(alumno, asignatura) = {(Ana, BD), (Ana, PROG), (Ana, SIS), (Luis, BD), (Luis, SIS), (Eva, PROG), (Eva, BD)} y OBLIGATORIAS(asignatura) = {(BD), (SIS)}, calcula paso a paso MATRICULAS ÷ OBLIGATORIAS.

Ver solución
T1 ← π[alumno](MATRICULAS)             = {Ana, Luis, Eva}
T2 ← T1 × OBLIGATORIAS                 = {(Ana,BD), (Ana,SIS), (Luis,BD), (Luis,SIS), (Eva,BD), (Eva,SIS)}
T3 ← T2 − MATRICULAS                   = {(Eva,SIS)}
T4 ← π[alumno](T3)                     = {Eva}
RESULTADO ← T1 − T4                    = {Ana, Luis}

Ana y Luis están matriculados en todas las obligatorias. Que Ana tenga además PROG no importa: la división pide “al menos todas las del divisor”.

Ejercicio 4: Optimizar una expresión

Dibuja (o escribe) la versión optimizada de esta expresión, que obtiene el nombre de los clientes de Valencia (ciudad) que han comprado algún monitor (categoría 6):

π[CLIENTES.nombre](σ[ciudad = 'Valencia' ∧ categoria_id = 6 ∧ CLIENTES.cliente_id = PEDIDOS.cliente_id
    ∧ PEDIDOS.pedido_id = LINEAS_PEDIDO.pedido_id ∧ LINEAS_PEDIDO.producto_id = PRODUCTOS.producto_id](
        CLIENTES × PEDIDOS × LINEAS_PEDIDO × PRODUCTOS))

¿Cuántas tuplas tiene el producto cartesiano? ¿Cuál es el resultado?

Ver solución

El producto tiene 14 × 24 × 51 × 20 = 342 720 tuplas. Versión optimizada (selecciones abajo, productos convertidos en combinaciones, proyecciones tempranas):

C ← π[cliente_id, nombre](σ[ciudad = 'Valencia'](CLIENTES))                      (2 tuplas: 1001, 1013)
M ← π[producto_id](σ[categoria_id = 6](PRODUCTOS))                               (2 tuplas: 9, 10)
L ← π[pedido_id](LINEAS_PEDIDO ⋈ M)                                              (pedidos con monitores)
RESULTADO ← π[nombre](C ⋈ π[cliente_id, pedido_id](PEDIDOS) ⋈ L)
flowchart TB
    r1["π[nombre]"] --> r2["⋈ cliente_id"]
    r2 --> r3["π[cliente_id, nombre]"]
    r3 --> r4["σ[ciudad = 'Valencia']"]
    r4 --> r5["CLIENTES"]
    r2 --> r6["⋈ pedido_id"]
    r6 --> r7["π[cliente_id, pedido_id]"]
    r7 --> r8["PEDIDOS"]
    r6 --> r9["π[pedido_id]"]
    r9 --> r10["⋈ producto_id"]
    r10 --> r11["LINEAS_PEDIDO"]
    r10 --> r12["π[producto_id]"]
    r12 --> r13["σ[categoria_id = 6]"]
    r13 --> r14["PRODUCTOS"]

Resultado: Talleres Martín S.L. (1 tupla), que compró monitores en los pedidos 5001 y 5021. El otro cliente de Valencia (1013) no tiene pedidos. Las combinaciones naturales son seguras porque antes se ha proyectado sobre los atributos de combinación.

Ejercicio 5: Traducir entre lenguajes

Expresa en cálculo de tuplas y en SQL: π[nombre](CLIENTES ▷[CLIENTES.cliente_id = PEDIDOS.cliente_id] PEDIDOS). ¿Qué devuelve?

Ver solución

Cálculo de tuplas:

{ c.nombre | CLIENTES(c) ∧ ¬∃p (PEDIDOS(p) ∧ p.cliente_id = c.cliente_id) }

SQL:

SELECT c.nombre
FROM   clientes c
WHERE  NOT EXISTS (
           SELECT 1
           FROM   pedidos p
           WHERE  p.cliente_id = c.cliente_id
       );

Devuelve los clientes sin pedidos: Carlos Ibáñez Rey y Farmacia Central (2 tuplas).

Ejercicio 6: Encuentra el error

Cada expresión pretende resolver la consulta indicada pero es incorrecta. Explica por qué y corrígela.

a) “Nombre de cada proyecto y nombre de su departamento”: π[nombre](PROYECTOS ⋈ DEPARTAMENTOS). b) “Empleados sin comisión”: π[emp_id](EMPLEADOS) − π[emp_id](σ[comision ≥ 0](EMPLEADOS)). ¿Es incorrecta? c) “Identificadores de empleados y de clientes”: π[emp_id, nombre](EMPLEADOS) ∪ π[cliente_id](CLIENTES). d) “Empleados que trabajan en el proyecto 1 y en el 3”: π[emp_id](σ[proyecto_id = 1 ∧ proyecto_id = 3](ASIGNACIONES)).

Ver solución

a) La combinación natural compara dept_id, nombre y presupuesto (atributos homónimos) y el resultado es vacío; además π[nombre] es ambiguo. Corrección: π[PROYECTOS.nombre, DEPARTAMENTOS.nombre](PROYECTOS ⋈[PROYECTOS.dept_id = DEPARTAMENTOS.dept_id] DEPARTAMENTOS).

b) Es correcta, aunque rebuscada: el sustraendo contiene los 4 empleados con comisión no nula (incluido Sergio, con 0), y el resultado son los 20 con comisión nula. Más directa: π[emp_id](σ[comision IS NULL](EMPLEADOS)). Ojo: si “sin comisión” incluyera también a los que tienen 0, serían 21.

c) No son compatibles: grado 2 frente a grado 1. Corrección: π[emp_id](EMPLEADOS) ∪ π[cliente_id](CLIENTES) (grado 1, ambos numéricos). Aun así, mezclar identificadores de dominios distintos es semánticamente dudoso.

d) Ninguna tupla tiene a la vez proyecto_id = 1 y proyecto_id = 3: el resultado siempre es vacío. Corrección: intersección π[emp_id](σ[proyecto_id = 1](ASIGNACIONES)) ∩ π[emp_id](σ[proyecto_id = 3](ASIGNACIONES)), que en EMPRESA es vacía (ningún empleado está en ambos), pero por una razón legítima.

Ejercicios propuestos

Resuelve en álgebra relacional (y, si quieres, en SQL). Entre paréntesis tienes el número de tuplas que debe devolver tu solución con los datos del curso, para que puedas comprobarla.

✏️ Ejercicio: 1. Nombre y precio de los productos sin stock. (2 tuplas)

✏️ Ejercicio: 2. Clientes de la provincia de Valencia. (2 tuplas)

✏️ Ejercicio: 3. Empleados dados de alta a partir del 1 de enero de 2020 con salario inferior a 2000 €. (5 tuplas)

✏️ Ejercicio: 4. Nombre de los departamentos que tienen algún proyecto sin fecha de fin. (3 tuplas)

✏️ Ejercicio: 5. Nombre de los clientes que tienen algún pedido gestionado por Carmen Ruiz (110). (3 tuplas)

✏️ Ejercicio: 6. Productos que nunca se han vendido a clientes cuya ciudad es Madrid. (13 tuplas)

✏️ Ejercicio: 7. Empleados que no han gestionado ningún pedido. (20 tuplas)

✏️ Ejercicio: 8. Parejas de empleados distintos que trabajan en el mismo departamento y tienen el mismo oficio, sin repetir cada pareja en los dos órdenes. (10 tuplas)

✏️ Ejercicio: 9. Clientes que han comprado algún portátil (categoría 2) y también algún monitor (categoría 6). (4 tuplas)

✏️ Ejercicio: 10. Clientes que han comprado todos los productos de la categoría “Telefonía”. (1 tupla)

✏️ Ejercicio: 11. Número de pedidos de cada estado. (4 tuplas)

✏️ Ejercicio: 12. Proyectos con más de 1000 horas asignadas en total. (2 tuplas)

✏️ Ejercicio: 13. Empleados que ganan más que todos los empleados del departamento 20, sin usar agregados. (2 tuplas)

✏️ Ejercicio: 14. Todos los clientes con sus pedidos, incluidos los clientes que no tienen ninguno. (26 tuplas)

✏️ Ejercicio: 15. Categorías que no tienen ningún producto asignado directamente. (1 tupla)


Resumen

Operador Notación Resultado SQL
Selección σ[c](R) Tuplas de R que cumplen c (V) WHERE
Proyección π[L](R) Atributos L de R, sin duplicados SELECT DISTINCT
Renombrado ρ[S](R) R con otro nombre Alias
Unión R ∪ S Tuplas de R o de S UNION
Diferencia R − S Tuplas de R que no están en S MINUS / EXCEPT
Producto R × S Todas las combinaciones CROSS JOIN
Intersección R ∩ S = R − (R − S) Tuplas comunes INTERSECT
Combinación theta R ⋈[θ] S = σ[θ](R × S) Parejas que cumplen θ JOIN ... ON
Combinación natural R ⋈ S Igualdad en todos los atributos homónimos NATURAL JOIN
Semicombinación R ⋉[θ] S Tuplas de R con pareja en S EXISTS / IN
Antisemicombinación R ▷[θ] S Tuplas de R sin pareja en S NOT EXISTS
División R ÷ S Valores relacionados con todos los de S Doble NOT EXISTS
Externas ⟕ ⟖ ⟗ Combinación que conserva las tuplas sin pareja LEFT/RIGHT/FULL OUTER JOIN
Agrupación γ[G; F](R) Una tupla por grupo con agregados GROUP BY (+ HAVING)
Asignación T ← e Nombre para un resultado intermedio WITH
Compatibilidad de unión — Mismo grado y dominios compatibles —
Optimización — Empujar σ y π hacia las hojas; σ + × → ⋈ Optimizador del SGBD
Cálculo de tuplas { t \| P(t) } Declarativo; variables de tupla Base de SELECT
Cálculo de dominios { ⟨x⟩ \| P(x) } Declarativo; variables de dominio Base de QBE