UD07 · Consultas avanzadas: composiciones, resúmenes y subconsultas

Resumen del tema

En la UD06 aprendiste a consultar una tabla. Pero una base de datos bien diseñada reparte la información entre muchas tablas: el nombre del alumno está en ALUMNO, su nota en MATRICULA y el nombre del módulo en MODULO. Casi ninguna pregunta real del centro se responde con una sola tabla. Esta unidad enseña a volver a juntar lo que el diseño separó (composiciones o joins), a resumir muchas filas en pocas (funciones de agregado y GROUP BY) y a encadenar consultas unas dentro de otras (subconsultas).

El recorrido es acumulativo. Primero se resume una sola tabla, porque agrupar es más fácil de entender sin composiciones de por medio. Después se combinan tablas, primero de forma interna (solo las filas que emparejan) y luego externa (conservando también las que no emparejan: el grupo sin tutor, el ciclo sin grupos). A continuación se usan subconsultas para preguntas que necesitan dos pasos («quién está por encima de la media de su módulo»). Por último se guardan las consultas útiles como vistas, se combinan resultados con UNION, INTERSECT y MINUS, y se introducen las funciones analíticas y la optimización con el plan de ejecución.

Esta es la unidad que convierte el SQL en una herramienta profesional: al terminarla serás capaz de contestar prácticamente cualquier pregunta que secretaría, jefatura de estudios o el equipo docente le puedan hacer a EduGest. En la UD08 reutilizarás casi todo lo de aquí, porque UPDATE y DELETE admiten subconsultas, y en la UD09 encapsularás estas consultas en procedimientos y vistas más elaboradas.

Todos los ejemplos se ejecutan sobre el esquema de referencia EDUGEST con los datos del curso 2025-26 cargados con los scripts del proyecto: 5 departamentos, 12 profesores, 4 ciclos, 24 módulos, 6 grupos, 32 alumnos, 143 matrículas, 24 asignaciones docentes y 46 faltas de asistencia.

Resultados de aprendizaje y criterios de evaluación
RA3 Consulta la información almacenada en una base de datos empleando asistentes, herramientas gráficas y el lenguaje de manipulación de datos. 7 CE
  • RA3.a Se han identificado las herramientas y sentencias para realizar consultas.
  • RA3.c Se han realizado consultas sobre el contenido de varias tablas mediante composiciones internas.
  • RA3.d Se han realizado consultas sobre el contenido de varias tablas mediante composiciones externas.
  • RA3.e Se han realizado consultas resumen.
  • RA3.f Se han realizado consultas con subconsultas.
  • RA3.g Se han realizado consultas que implican múltiples selecciones.
  • RA3.h Se han aplicado criterios de optimización de consultas.
RA2 Crea bases de datos definiendo su estructura y las características de sus elementos según el modelo relacional. 1 CE
  • RA2.f Se han creado vistas.

Objetivos de aprendizaje

Al terminar esta unidad serás capaz de:

  • Resumir el contenido de una tabla con funciones de agregado, razonando cómo afectan los valores nulos.
  • Formar grupos con GROUP BY y filtrarlos con HAVING, distinguiendo su papel del de WHERE.
  • Combinar varias tablas con composiciones internas siguiendo el camino de las claves ajenas.
  • Decidir cuándo una pregunta exige una composición externa y escribirla sin anularla con un filtro mal colocado.
  • Resolver con autocomposiciones las preguntas en que una tabla se relaciona consigo misma.
  • Escribir subconsultas de una fila, de varias filas, correlacionadas y con EXISTS, y sustituirlas por una cláusula WITH cuando mejore la legibilidad.
  • Crear vistas que encapsulen composiciones y valorar cuándo son actualizables.
  • Combinar resultados de varias consultas con UNION, UNION ALL, INTERSECT y MINUS.
  • Utilizar funciones analíticas para comparar cada fila con su grupo sin perder el detalle.
  • Obtener e interpretar el plan de ejecución de una consulta y aplicar criterios de optimización.

Temporalización

La unidad ocupa 16 horas de aula (10 de teoría y 6 de práctica). Las composiciones y las subconsultas son el núcleo del RA3: conviene no avanzar hasta que cada consulta se pueda escribir sin consultar los apuntes.

UD07 · 16 horas de aula Teoría 10 h · Práctica 6 hDel 01/03/2027 al 23/03/2027
SesiónHorasTipoContenidoDónde
12TeoríaFunciones de agregado, GROUP BY y HAVING§1 y §2 · laboratorio de agrupamiento
21PrácticaConsultas resumenPráctica 7.1
32TeoríaComposiciones internas: INNER JOIN y varias tablas§3 · laboratorio de composiciones
41PrácticaComposiciones paso a pasoPráctica 7.2
52TeoríaComposiciones externas y autocomposición§4 y §5
61PrácticaInformes para jefaturaPráctica 7.3
72TeoríaSubconsultas, EXISTS y WITH§6
81PrácticaSubconsultasPráctica 7.4
91TeoríaVistas con composiciones y operadores de conjuntos§7 y §8
101PrácticaMúltiples selecciones y vistasPráctica 7.5
111TeoríaFunciones analíticas y optimización de consultas§9 y §10
121PrácticaOptimización: medir antes y despuésPráctica 7.6
Trabajo autónomo (fuera del horario de aula): Práctica 7.7 (reto: el cuadro de mando) · Proyecto EduGest · UD07 (informes de secretaría y jefatura) · Ampliación de funciones analíticas

Estudia esta unidad con la base de datos abierta y con un método fijo para cada consulta:

  1. Escribe en castellano la pregunta que quieres responder.
  2. Decide qué tablas la contienen y por qué claves se enlazan.
  3. Predice cuántas filas debe devolver y de qué tipo.
  4. Ejecuta y compara con tu predicción. Si no coincide, el error está en tu modelo mental, no en Oracle: averigua dónde.

Si una consulta devuelve muchas más filas de las esperadas, casi siempre falta una condición de composición. Si devuelve menos, casi siempre sobra un filtro o falta una composición externa.


Sesión 1 2 h Teoría · Funciones de agregado, GROUP BY y HAVING

1. Funciones de agregado

1.1 Qué problema resuelven

Las funciones de fila de la UD06 (UPPER, ROUND, TO_CHAR…) devuelven un valor por cada fila: entran 143 filas y salen 143. Pero muchas preguntas no piden filas, piden un número que resuma las filas: «¿cuántas matrículas hay?», «¿cuál es la nota media?», «¿cuál es la nota más alta?».

Para eso están las funciones de agregado (también llamadas funciones de grupo o de columna): reciben un conjunto de filas y devuelven un único valor.

flowchart LR
    A[143 filas de MATRICULA] -->|AVG nota_final| B[1 fila: 6,36]
Oracle 26ai
FunciónDevuelveAdmite DISTINCTTipos admitidos
COUNT(*)Número de filas del grupoNo—
COUNT(expr)Número de filas en que expr no es nulaSícualquiera
SUM(expr)SumaSínuméricos
AVG(expr)Media aritméticaSínuméricos
MIN(expr) / MAX(expr)Valor menor / mayorNo (no aporta)numéricos, texto, fechas
STDDEV(expr) / VARIANCE(expr)Desviación típica / varianzaSínuméricos
MEDIAN(expr)MedianaNonuméricos, fechas
LISTAGG(expr, sep)Concatena los valores del grupo en una cadenaSítexto

COUNT, SUM, AVG, MIN y MAX son SQL estándar y existen en todos los SGBD. STDDEV, VARIANCE, MEDIAN y LISTAGG son extensiones de Oracle (algunas están también en PostgreSQL con otro nombre). MEDIAN y LISTAGG se tratan como ampliación: lo evaluable del RA3.e son las cinco primeras.

1.2 Ejemplo sencillo: contar

SELECT COUNT(*)                    AS matriculas,
       COUNT(nota_final)           AS calificadas,
       COUNT(*) - COUNT(nota_final) AS sin_calificar,
       COUNT(DISTINCT id_alumno)   AS alumnos_distintos,
       COUNT(DISTINCT id_modulo)   AS modulos_distintos
FROM   matricula;
MATRICULASCALIFICADASSIN_CALIFICARALUMNOS_DISTINTOSMODULOS_DISTINTOS
14313762924

1 fila

Elemento a elemento:

ElementoSignificado
COUNT(*)Cuenta filas, sin mirar su contenido: 143 matrículas
COUNT(nota_final)Cuenta las filas en que nota_final tiene valor: 137
COUNT(*) - COUNT(nota_final)Las 6 matrículas sin calificar
COUNT(DISTINCT id_alumno)Valores distintos de id_alumno: 29 alumnos tienen matrícula (de los 32)

Todas las funciones de agregado excepto COUNT(*) ignoran los nulos. No los cuentan, no los suman y no los tienen en cuenta en la media. Esta es la causa número uno de resultados «raros» en las consultas resumen, y la razón por la que COUNT(*) y COUNT(columna) dan números distintos.

1.3 Ejemplo aplicado: estadísticos de las notas

SELECT COUNT(nota_final)            AS calificadas,
       ROUND(AVG(nota_final), 2)    AS media,
       MIN(nota_final)              AS minima,
       MAX(nota_final)              AS maxima,
       SUM(nota_final)              AS suma,
       ROUND(SUM(nota_final) / COUNT(*), 2) AS media_incorrecta
FROM   matricula;
CALIFICADASMEDIAMINIMAMAXIMASUMAMEDIA_INCORRECTA
1376.361.5108716.09

1 fila

La última columna es deliberadamente errónea y conviene entenderla bien:

  • AVG(nota_final) calcula 871 / 137 = 6,36: divide entre las matrículas calificadas.
  • SUM(nota_final) / COUNT(*) calcula 871 / 143 = 6,09: divide entre todas las matrículas, incluidas las seis sin nota, como si fueran ceros.

Las dos cifras responden a preguntas distintas y ninguna es más correcta que la otra en abstracto: depende de lo que te pidan. «Nota media de los alumnos calificados» es 6,36. «Nota media considerando los no presentados como un cero» es 6,09, y entonces conviene escribirlo explícitamente con AVG(NVL(nota_final, 0)) para que el lector vea la decisión. Lo que nunca debe pasar es tomar una por la otra sin darse cuenta.

1.4 MIN y MAX no son solo para números

MIN y MAX funcionan con texto (orden alfabético según NLS_SORT) y con fechas, donde MIN es la más antigua:

SELECT MIN(fecha_nacimiento) AS mayor_edad,
       MAX(fecha_nacimiento) AS menor_edad,
       MIN(apellidos)        AS primero_alfabetico
FROM   alumno;
MAYOR_EDADMENOR_EDADPRIMERO_ALFABETICO
07/08/200025/12/2006Agulló Cerdá

1 fila

1.5 Contar condiciones: SUM con CASE

Una técnica muy habitual en informes es contar cuántas filas cumplen una condición dentro del mismo resumen, en vez de lanzar una consulta por cada condición:

SELECT COUNT(*)                                            AS matriculas,
       SUM(CASE WHEN nota_final >= 5 THEN 1 ELSE 0 END)     AS aprobadas,
       SUM(CASE WHEN nota_final <  5 THEN 1 ELSE 0 END)     AS suspensas,
       COUNT(*) - COUNT(nota_final)                         AS sin_calificar,
       ROUND(100 * SUM(CASE WHEN nota_final >= 5 THEN 1 ELSE 0 END)
             / COUNT(nota_final), 1)                        AS pct_aprobados
FROM   matricula;
MATRICULASAPROBADASSUSPENSASSIN_CALIFICARPCT_APROBADOS
14311027680.3

1 fila

Fíjate en el denominador del porcentaje: COUNT(nota_final), no COUNT(*). El porcentaje de aprobados se calcula sobre los calificados.

En lugar de SUM(CASE WHEN … THEN 1 ELSE 0 END) también funciona COUNT(CASE WHEN … THEN 1 END): como COUNT ignora los nulos y el CASE sin ELSE devuelve NULL cuando no se cumple, el efecto es el mismo. Usa la forma que te resulte más legible, pero sé coherente dentro de un mismo informe.

1.6 Errores habituales con los agregados

-- (a) Mezclar una columna normal con un agregado sin GROUP BY
SELECT localidad, COUNT(*) FROM alumno;
-- ORA-00937: not a single-group group function

-- (b) Usar un agregado en el WHERE
SELECT localidad FROM alumno WHERE COUNT(*) > 3 GROUP BY localidad;
-- ORA-00934: group function is not allowed here

-- (c) Anidar agregados sin GROUP BY intermedio
SELECT MAX(AVG(nota_final)) FROM matricula;
-- ORA-00978: nested group function without GROUP BY

El error (a) es el más frecuente: si pides una columna normal junto a un agregado, Oracle no sabe qué localidad poner al lado de un recuento de 32 alumnos. La solución es el GROUP BY del apartado siguiente.


2. Agrupamiento: GROUP BY y HAVING

2.1 Cómo se forman los grupos

GROUP BY divide las filas en grupos que comparten el mismo valor en las columnas indicadas, y después aplica las funciones de agregado a cada grupo por separado. El resultado tiene una fila por grupo.

SELECT cod_grupo, COUNT(*) AS alumnos
FROM   alumno
GROUP  BY cod_grupo
ORDER  BY cod_grupo;
COD_GRUPOALUMNOS
1ASIR5
1DAM7
1DAW6
2DAM6
2DAW5
(null)3

6 filas

Observaciones importantes:

  • Hay seis filas porque hay cinco valores distintos de cod_grupo más el grupo de los nulos.
  • GROUP BY reúne todos los nulos en un único grupo (al contrario que el operador =, que nunca considera iguales dos nulos). Esos son los 3 alumnos sin grupo asignado.
  • La suma de los recuentos es 32: ninguna fila se pierde y ninguna se cuenta dos veces.
  • Los NULL aparecen al final porque ORDER BY … ASC los coloca al final en Oracle.

GROUP BY no garantiza el orden del resultado, aunque a veces lo parezca. Si el orden importa, escribe ORDER BY. Es un error clásico confiar en que los grupos salgan ordenados.

2.2 Qué puede aparecer en el SELECT

Esta es la regla que más errores provoca del tema:

En una consulta con GROUP BY, el SELECT solo puede contener:

  1. las columnas o expresiones que aparecen en el GROUP BY,
  2. funciones de agregado,
  3. constantes y expresiones calculadas a partir de 1 y 2.

Cualquier otra columna provoca ORA-00979: not a GROUP BY expression.

-- Incorrecta: nombre no está en el GROUP BY y no es un agregado
SELECT localidad, nombre, COUNT(*)
FROM   alumno
GROUP  BY localidad;
-- ORA-00979: not a GROUP BY expression

La consulta no tiene sentido: el grupo «Alicante» contiene 13 alumnos con 13 nombres distintos; ¿cuál debería mostrar? Tienes tres salidas posibles, según lo que realmente quieras:

Lo que quieresSolución
Una fila por localidad, sin el nombreQuita nombre del SELECT
Una fila por localidad y nombreAñade nombre al GROUP BY
Una fila por localidad con un nombre representativoMIN(nombre) o MAX(nombre)
Una fila por localidad con todos los nombres en una celdaLISTAGG(nombre, ', ') WITHIN GROUP (ORDER BY nombre)
SELECT localidad,
       COUNT(*) AS alumnos,
       MIN(apellidos) AS primero,
       LISTAGG(nombre, ', ') WITHIN GROUP (ORDER BY nombre) AS nombres
FROM   alumno
WHERE  localidad IN ('Elche', 'El Campello')
GROUP  BY localidad
ORDER  BY localidad;
LOCALIDADALUMNOSPRIMERONOMBRES
El Campello3Alemany PérezDaniel, Elena, Jorge
Elche3Iborra FerriRubén, Sofía, Víctor

2 filas

1. FROM y JOIN

Se construye el conjunto de filas de partida uniendo las tablas indicadas.

2. WHERE

Filtra filas antes de agrupar. No puede usar funciones de agregado.

3. GROUP BY

Forma los grupos con las filas que quedan.

4. HAVING

Filtra grupos después de agrupar. Aquí sí se pueden usar COUNT, AVG…

5. SELECT

Calcula las expresiones y los alias del resultado.

6. ORDER BY

Ordena. Es la única fase que ve los alias del SELECT.

7. FETCH FIRST

Limita el número de filas devueltas.

2.3 Orden de evaluación: WHERE frente a HAVING

Ya conoces el orden lógico de las cláusulas; ahora se completa con GROUP BY y HAVING:

FROM  →  WHERE  →  GROUP BY  →  HAVING  →  SELECT  →  ORDER BY
  1        2          3            4         5           6
flowchart LR
    F["1 · FROM<br/>143 filas"] --> W["2 · WHERE<br/>filtra FILAS"]
    W --> G["3 · GROUP BY<br/>forma grupos"]
    G --> H["4 · HAVING<br/>filtra GRUPOS"]
    H --> S["5 · SELECT<br/>calcula columnas"]
    S --> O["6 · ORDER BY<br/>ordena"]

De este orden se derivan todas las reglas prácticas:

PreguntaRespuestaPorque…
¿Puedo usar un agregado en el WHERE?No (ORA-00934)El WHERE se evalúa antes de formar los grupos
¿Puedo usar un agregado en el HAVING?SíEl HAVING se evalúa después de agrupar
¿Puedo usar un alias del SELECT en el HAVING?No en OracleEl SELECT se evalúa después del HAVING
¿Puedo usar un alias del SELECT en el ORDER BY?SíEl ORDER BY es lo último
¿Filtro por nota_final < 5 en WHERE o en HAVING?En el WHEREEs una condición sobre filas, no sobre grupos

Las dos cláusulas se combinan con frecuencia, y cada una hace un trabajo distinto:

-- Módulos con 3 suspensos o más: quién está suspenso se decide fila a fila
-- (WHERE); cuántos suspensos tiene el grupo se decide después (HAVING).
SELECT id_modulo,
       COUNT(*)                  AS suspensos,
       ROUND(AVG(nota_final), 2) AS media_de_los_suspensos
FROM   matricula
WHERE  nota_final < 5
GROUP  BY id_modulo
HAVING COUNT(*) >= 3
ORDER  BY suspensos DESC, id_modulo;
ID_MODULOSUSPENSOSMEDIA_DE_LOS_SUSPENSOS
343.81
1833

2 filas

¿Qué pasaría si moviéramos la condición nota_final < 5 al HAVING?

No es posible escribirla tal cual: HAVING nota_final < 5 da ORA-00979, porque nota_final no está en el GROUP BY. Si la escribes como HAVING MIN(nota_final) < 5, la consulta es válida pero responde a otra pregunta: «módulos en los que hay al menos un suspenso», y el COUNT(*) contaría todas las matrículas del módulo, no solo las suspensas.

Regla práctica: si la condición se puede decidir mirando una sola fila, va en el WHERE. Si necesita ver el grupo entero, va en el HAVING. Además, filtrar en el WHERE es más eficiente, porque se descartan filas antes de agrupar.

2.4 Agrupar por varias columnas y por expresiones

El GROUP BY puede contener varias columnas: entonces se forma un grupo por cada combinación de valores presente en los datos (nunca se inventan combinaciones que no existan).

SELECT curso_academico, convocatoria,
       COUNT(*)                  AS matriculas,
       COUNT(nota_final)         AS calificadas,
       ROUND(AVG(nota_final), 2) AS media
FROM   matricula
GROUP  BY curso_academico, convocatoria
ORDER  BY curso_academico, convocatoria;
CURSO_ACADEMICOCONVOCATORIAMATRICULASCALIFICADASMEDIA
2025-2611401346.38
2025-262335.25

2 filas

También se puede agrupar por una expresión, siempre que la misma expresión aparezca en el GROUP BY:

SELECT CASE WHEN nota_final IS NULL THEN 'Sin calificar'
            WHEN nota_final < 5     THEN 'Insuficiente'
            WHEN nota_final < 7     THEN 'Suficiente / Bien'
            WHEN nota_final < 9     THEN 'Notable'
            ELSE                         'Sobresaliente'
       END      AS tramo,
       COUNT(*) AS matriculas
FROM   matricula
GROUP  BY CASE WHEN nota_final IS NULL THEN 'Sin calificar'
               WHEN nota_final < 5     THEN 'Insuficiente'
               WHEN nota_final < 7     THEN 'Suficiente / Bien'
               WHEN nota_final < 9     THEN 'Notable'
               ELSE                         'Sobresaliente'
          END
ORDER  BY matriculas DESC;
TRAMOMATRICULAS
Suficiente / Bien52
Notable49
Insuficiente27
Sobresaliente9
Sin calificar6

5 filas

Repetir el CASE completo es incómodo y propenso a errores. En Oracle no se puede escribir GROUP BY tramo usando el alias, pero sí se puede dar nombre al cálculo con una subconsulta en el FROM o con una cláusula WITH (apartado 6.6):

WITH clasificadas AS (
    SELECT CASE WHEN nota_final IS NULL THEN 'Sin calificar'
                WHEN nota_final < 5     THEN 'Insuficiente'
                ELSE                         'Aprobada'
           END AS tramo
    FROM   matricula)
SELECT tramo, COUNT(*) AS matriculas
FROM   clasificadas
GROUP  BY tramo
ORDER  BY matriculas DESC;

2.5 Subtotales: ROLLUP, CUBE y GROUPING SETS

Un informe suele necesitar, además del detalle por grupo, una fila de totales. GROUP BY ROLLUP(...) la añade automáticamente:

Oracle 26ai
SELECT id_modulo,
       COUNT(*)                  AS matriculas,
       COUNT(nota_final)         AS calificadas,
       ROUND(AVG(nota_final), 2) AS media,
       GROUPING(id_modulo)       AS es_total
FROM   matricula
WHERE  id_modulo IN (16, 17, 18, 19)
GROUP  BY ROLLUP(id_modulo)
ORDER  BY GROUPING(id_modulo), id_modulo;
ID_MODULOMATRICULASCALIFICADASMEDIAES_TOTAL
16546.380
17555.80
18554.90
19556.750
(null)20195.931

5 filas

CláusulaQué añade
ROLLUP(a, b)Subtotales por a y un total general: jerarquía de izquierda a derecha
CUBE(a, b)Todas las combinaciones de subtotales: por a, por b y total
GROUPING SETS((a), (b))Exactamente los subtotales que indiques
GROUPING(col)Devuelve 1 si la fila es un subtotal respecto de col, y 0 si no

En las filas de subtotal, la columna agrupada vale NULL. Si la columna ya tenía valores nulos propios (como cod_grupo en ALUMNO), no podrás distinguir «los alumnos sin grupo» de «el total» mirando la columna: necesitas GROUPING(cod_grupo). Para presentarlo se combina con CASE: CASE WHEN GROUPING(cod_grupo) = 1 THEN 'TOTAL' ELSE NVL(cod_grupo, '(sin grupo)') END.

ROLLUP, CUBE y GROUPING SETS son SQL estándar y existen en Oracle, SQL Server y PostgreSQL; en MySQL solo está WITH ROLLUP. Aquí son ampliación: resuelve primero los informes sin subtotales.

2.6 Laboratorio de agrupamiento

En el laboratorio siguiente trabajas con las 143 matrículas reales de EduGest (hay 6 notas sin poner, dos de ellas en el módulo 0490). Elige la columna de agrupamiento, la función de agregado y, si quieres, una condición HAVING, y observa tres cosas a la vez: qué filas forman cada grupo, qué devuelve el agregado y qué SQL estás construyendo en realidad.

Presta atención especialmente a dos experimentos:

  1. Agrupa por id_modulo y compara COUNT(*) con COUNT(nota_final) en el módulo 0490: hay dos matrículas sin nota.
  2. Marca la casilla «añadir id_alumno al SELECT»: verás el error ORA-00979, que es el que más veces aparece en los primeros ejercicios del tema.
Interactivo Laboratorio de GROUP BY y HAVING Tabla MATRICULA de EduGest (143 filas) · simulación didáctica, no es un SGBD

2 · Funciones de agregado en el SELECT

3 · WHERE (filtra filas antes de agrupar)

4 · HAVING (filtra grupos después de agrupar)

Autoevaluación 3 preguntas
1 La tabla MATRICULA tiene 143 filas y 6 tienen nota_final a NULL. ¿Qué devuelve SELECT COUNT(*), COUNT(nota_final) FROM matricula?
2 ¿Por qué falla SELECT localidad, nombre, COUNT(*) FROM alumno GROUP BY localidad?
3 Quieres los módulos cuya nota media supera 6, calculada solo con las matrículas de primera convocatoria. ¿Dónde va cada condición?

Sesión 3 2 h Teoría · Composiciones internas: INNER JOIN

3. Composiciones internas: INNER JOIN

3.1 Qué problema resuelven

La normalización (UD04) reparte la información para no repetirla: en MATRICULA no se guarda el nombre del alumno, sino su id_alumno. Es el diseño correcto, pero deja un problema práctico: el acta de evaluación necesita el nombre, no el identificador.

Una composición (join) es la operación que vuelve a unir esa información: combina filas de dos o más tablas emparejándolas por una condición, normalmente la igualdad entre una clave ajena y la clave primaria a la que apunta.

flowchart LR
    M["MATRICULA<br/>id_alumno = 3<br/>nota = 8"] -->|id_alumno| A["ALUMNO<br/>id_alumno = 3<br/>Noelia Verdú Espí"]
    M -->|id_modulo| MO["MODULO<br/>id_modulo = 2<br/>Bases de datos"]
Curiosidad El coste de un producto cartesiano

Un FROM a, b sin condición de unión combina cada fila de a con cada fila de b. Con dos tablas de 1.000 filas salen 1.000.000 de filas, y con tres de 1.000 filas, mil millones. Por eso olvidar la condición del JOIN es uno de los errores que más tardan en ejecutarse.

3.2 El punto de partida: el producto cartesiano

Si escribes dos tablas en el FROM sin condición, Oracle combina cada fila de la primera con cada fila de la segunda. Eso es el producto cartesiano, y en SQL moderno se escribe CROSS JOIN:

SELECT (SELECT COUNT(*) FROM grupo) AS grupos,
       (SELECT COUNT(*) FROM ciclo) AS ciclos,
       (SELECT COUNT(*) FROM grupo g CROSS JOIN ciclo c) AS producto
FROM   dual;
GRUPOSCICLOSPRODUCTO
6424

1 fila

El producto cartesiano de GRUPO y CICLO tiene 24 filas: 6 × 4. Predecir este número es justo lo que hay que aprender a hacer. Las primeras filas, ordenadas, dejan ver el problema:

SELECT g.cod_grupo, g.cod_ciclo AS ciclo_del_grupo, c.cod_ciclo AS ciclo_de_la_tabla
FROM   grupo g CROSS JOIN ciclo c
ORDER  BY g.cod_grupo, c.cod_ciclo
FETCH FIRST 8 ROWS ONLY;
COD_GRUPOCICLO_DEL_GRUPOCICLO_DE_LA_TABLA
1ASIRASIRASIR
1ASIRASIRDAM
1ASIRASIRDAW
1ASIRASIRSMR
1DAMDAMASIR
1DAMDAMDAM
1DAMDAMDAW
1DAMDAMSMR

8 filas

De las cuatro filas de cada grupo, solo una tiene sentido: la que empareja el grupo con su ciclo. Una composición interna es exactamente el producto cartesiano más la condición que selecciona esas filas con sentido.

El producto cartesiano crece multiplicando. Con las 143 matrículas y las 46 faltas de EduGest son 6 578 filas; con dos tablas de un millón de filas serían un billón. Si una consulta tarda muchísimo o devuelve un número absurdo de filas, lo primero que hay que sospechar es una condición de composición olvidada.

3.3 Sintaxis JOIN … ON

SQL estándar
SELECT columnas
FROM   tabla1 alias1
       [INNER] JOIN tabla2 alias2 ON condición_de_emparejamiento
       [INNER] JOIN tabla3 alias3 ON condición_de_emparejamiento
WHERE  condiciones_de_filtrado
ElementoSignificado
JOIN tabla2Tabla que se añade a la composición
ON condiciónCómo se emparejan las filas; es obligatoria en JOIN
INNEROpcional: JOIN sin más es siempre una composición interna
aliasNombre corto de la tabla; imprescindible para cualificar las columnas

Ejemplo sencillo, cada módulo con el nombre completo de su ciclo:

SELECT mo.codigo, mo.nombre AS modulo, c.nombre AS ciclo, mo.curso, mo.horas
FROM   modulo mo
       JOIN ciclo c ON c.cod_ciclo = mo.cod_ciclo
WHERE  mo.cod_ciclo = 'ASIR'
ORDER  BY mo.codigo;
CODIGOMODULOCICLOCURSOHORAS
0369Implantación de sistemas operativosAdministración de Sistemas Informáticos en Red1224
0370Planificación y administración de redesAdministración de Sistemas Informáticos en Red1192
0371Fundamentos de hardwareAdministración de Sistemas Informáticos en Red196
0372Gestión de bases de datosAdministración de Sistemas Informáticos en Red1160
0373Lenguajes de marcas y sistemas de gestión de informaciónAdministración de Sistemas Informáticos en Red196

5 filas

Resultado esperado: 5 filas, las mismas que módulos de ASIR. Una composición por clave ajena hacia una clave primaria no puede multiplicar filas: cada módulo tiene exactamente un ciclo. Esta es la comprobación más útil del tema.

Usa siempre alias de tabla y cualifica todas las columnas. No es una cuestión estética:

  • MODULO y CICLO tienen las dos una columna nombre. Sin cualificar, SELECT nombre daría ORA-00918: column ambiguously defined.
  • Al leer la consulta meses después, c.nombre dice de dónde sale el dato; nombre no.
  • Si alguien añade una columna a una tabla, una consulta sin cualificar puede volverse ambigua de repente y dejar de compilar.

3.4 Composición de tres y cuatro tablas

Las composiciones se encadenan. El método es siempre el mismo: trazar el camino de claves ajenas desde la tabla que tiene el dato que buscas hasta la que tiene el dato que quieres mostrar.

flowchart LR
    F[FALTA_ASISTENCIA] -->|id_matricula| M[MATRICULA]
    M -->|id_alumno| A[ALUMNO]
    M -->|id_modulo| MO[MODULO]
    MO -->|cod_ciclo| C[CICLO]
    A -->|cod_grupo| G[GRUPO]

Tres tablas: alumnado de 2DAM con su grupo y el nombre del ciclo.

SELECT a.apellidos || ', ' || a.nombre AS alumno,
       g.cod_grupo, g.turno, c.nombre AS ciclo
FROM   alumno a
       JOIN grupo g ON g.cod_grupo = a.cod_grupo
       JOIN ciclo c ON c.cod_ciclo = g.cod_ciclo
WHERE  g.cod_grupo = '2DAM'
ORDER  BY a.apellidos;
ALUMNOCOD_GRUPOTURNOCICLO
Alemany Vidal, Martina2DAMMDesarrollo de Aplicaciones Multiplataforma
Belda Quiles, Lucía2DAMMDesarrollo de Aplicaciones Multiplataforma
Brotons Iborra, Hugo2DAMMDesarrollo de Aplicaciones Multiplataforma
Cerdá Tomás, Nerea2DAMMDesarrollo de Aplicaciones Multiplataforma
Domènech Quiles, María2DAMMDesarrollo de Aplicaciones Multiplataforma
Ripoll Agulló, Iván2DAMMDesarrollo de Aplicaciones Multiplataforma

6 filas

Cuatro tablas: la asignación docente de 2DAM, que cruza IMPARTE con MODULO, GRUPO y PROFESOR.

SELECT g.cod_grupo, mo.codigo, mo.nombre AS modulo,
       p.apellidos AS profesor, i.horas_semanales
FROM   imparte i
       JOIN modulo mo   ON mo.id_modulo   = i.id_modulo
       JOIN grupo g     ON g.cod_grupo    = i.cod_grupo
       JOIN profesor p  ON p.id_profesor  = i.id_profesor
WHERE  i.cod_grupo = '2DAM'
ORDER  BY mo.codigo;
COD_GRUPOCODIGOMODULOPROFESORHORAS_SEMANALES
2DAM0486Acceso a datosFerrándiz Mora4
2DAM0488Desarrollo de interfacesNavarro Ruiz4
2DAM0489Programación multimedia y dispositivos móvilesCano Vidal3
2DAM0490Programación de servicios y procesosFerrándiz Mora2
2DAM0491Sistemas de gestión empresarialPastor Gil3

5 filas

Construye las composiciones de una en una. Añade una tabla, ejecuta SELECT COUNT(*), comprueba que el número es el que esperas y pasa a la siguiente. Así, cuando el número se desmadre, sabrás exactamente qué JOIN lo ha causado. La práctica 7.2 está montada sobre esta técnica.

3.5 Composiciones y agrupamientos juntos

Lo habitual en un informe es componer y después resumir. El orden lógico no cambia: primero FROM (con sus composiciones), luego WHERE, luego GROUP BY.

SELECT c.cod_ciclo, c.nombre AS ciclo,
       COUNT(m.id_matricula)     AS matriculas,
       COUNT(m.nota_final)       AS calificadas,
       ROUND(AVG(m.nota_final), 2) AS media
FROM   ciclo c
       JOIN modulo mo    ON mo.cod_ciclo = c.cod_ciclo
       JOIN matricula m  ON m.id_modulo  = mo.id_modulo
GROUP  BY c.cod_ciclo, c.nombre
ORDER  BY c.cod_ciclo;
COD_CICLOCICLOMATRICULASCALIFICADASMEDIA
ASIRAdministración de Sistemas Informáticos en Red25247.07
DAMDesarrollo de Aplicaciones Multiplataforma68656.29
DAWDesarrollo de Aplicaciones Web50486.09

3 filas

Falta uno de los cuatro ciclos: SMR no aparece porque no tiene módulos y, por tanto, ninguna fila sobrevive a la composición interna. Ese es precisamente el problema que resuelve el apartado 4.

En el GROUP BY se han puesto c.cod_ciclo y c.nombre, aunque el nombre dependa funcionalmente del código. Oracle exige en el GROUP BY todas las columnas no agregadas del SELECT. Cuando agrupes por una entidad, incluye su clave primaria en el GROUP BY: garantiza que no se fundan dos entidades con el mismo nombre.

3.6 Sintaxis antigua y otras formas de composición

Sintaxis con comas (heredada)

Antes del estándar SQL-92, la condición de composición se escribía en el WHERE:

-- Sintaxis antigua: NO la uses en código nuevo
SELECT mo.codigo, c.nombre
FROM   modulo mo, ciclo c
WHERE  c.cod_ciclo = mo.cod_ciclo
AND    mo.curso = 2;

Devuelve exactamente lo mismo que la versión con JOIN … ON, pero tiene dos problemas graves que explican por qué la norma profesional es no usarla:

  1. Mezcla las condiciones de composición con las de filtrado: en una consulta de seis tablas y diez condiciones es imposible ver de un vistazo si falta un emparejamiento.
  2. Si olvidas una condición, no hay ningún error: obtienes silenciosamente un producto cartesiano parcial con resultados erróneos.

Debes saber leerla porque aparece en muchísimo código heredado y en documentación antigua de Oracle, pero escribe siempre JOIN … ON.

USING

Cuando las columnas que se emparejan tienen el mismo nombre en las dos tablas, USING abrevia:

SELECT codigo, cod_ciclo, nombre_ciclo
FROM   (SELECT mo.codigo, mo.cod_ciclo, c.nombre AS nombre_ciclo
        FROM   modulo mo JOIN ciclo c USING (cod_ciclo))
FETCH FIRST 3 ROWS ONLY;

USING tiene una peculiaridad que sorprende: la columna común deja de pertenecer a una tabla concreta y no se puede cualificar. SELECT mo.cod_ciclo … USING (cod_ciclo) da ORA-25154: column part of USING clause cannot have qualifier. Hay que escribir cod_ciclo sin alias.

NATURAL JOIN

NATURAL JOIN empareja automáticamente por todas las columnas que tengan el mismo nombre en las dos tablas. Suena cómodo y es peligroso:

SELECT COUNT(*) FROM modulo NATURAL JOIN ciclo;
COUNT(*)
0

1 fila

Cero filas. MODULO y CICLO comparten dos nombres de columna: cod_ciclo y nombre. El NATURAL JOIN exige que coincidan los dos, y ningún módulo se llama igual que su ciclo.

No uses NATURAL JOIN en código real. La composición depende de los nombres de las columnas, así que añadir una columna llamada nombre, fecha_alta u observaciones a una tabla puede cambiar en silencio el resultado de consultas que llevaban años funcionando. Escribe siempre la condición explícitamente en el ON.

Condiciones en el ON o en el WHERE

En una composición interna da lo mismo poner una condición de filtrado en el ON o en el WHERE: el resultado es idéntico, y el optimizador de Oracle lo reorganiza igual.

-- Equivalentes (composición interna)
FROM modulo mo JOIN ciclo c ON c.cod_ciclo = mo.cod_ciclo AND mo.curso = 2
FROM modulo mo JOIN ciclo c ON c.cod_ciclo = mo.cod_ciclo WHERE mo.curso = 2

En una composición externa no son equivalentes, y la diferencia es una de las trampas más repetidas del módulo: se analiza en el apartado 4.4. Convención recomendada: en el ON, solo lo que empareja; en el WHERE, lo que filtra.

3.7 Laboratorio de composiciones

El laboratorio siguiente compone dos tablas reales de EduGest: los 6 grupos (uno de ellos, 2ASIR, sin tutor asignado) y 7 profesores (dos de los cuales no son tutores de ningún grupo). Cambia el tipo de composición y observa tres cosas: el SQL que se genera, qué filas se conservan y cuántas filas salen.

Antes de tocar nada, predice los números: ¿cuántas filas debería devolver cada tipo de composición? Después compruébalo. Usa también las dos casillas inferiores, que reproducen los dos errores clásicos del tema: el filtro en el WHERE que anula una composición externa y la diferencia entre poner una condición en el ON o en el WHERE.

Interactivo Laboratorio de composiciones GRUPO (6 filas) y PROFESOR (7 filas) de EduGest · simulación didáctica, no es un SGBD

GRUPO (id_tutor es clave ajena opcional)

PROFESOR (muestra de 7)

0 · Predicción

Escribe tu predicción y pulsa «Comprobar» para ver el resultado.

1 · Tipo de composición

2 · Las dos trampas

Autoevaluación 3 preguntas
1 FALTA_ASISTENCIA tiene 46 filas y MATRICULA 143. ¿Cuántas filas devuelve SELECT * FROM falta_asistencia f JOIN matricula m ON m.id_matricula = f.id_matricula?
2 ¿Por qué SELECT nombre FROM modulo mo JOIN ciclo c ON c.cod_ciclo = mo.cod_ciclo da ORA-00918?
3 Una consulta que compone ALUMNO con MATRICULA devuelve 143 filas en lugar de 32. ¿Qué ocurre?

Sesión 5 2 h Teoría · Composiciones externas y autocomposición

4. Composiciones externas: LEFT, RIGHT y FULL JOIN

4.1 Qué problema resuelven

Una composición interna solo devuelve las filas que emparejan. Eso hace desaparecer silenciosamente información que muchas veces es la más importante del informe:

Pregunta de EduGestLo que la composición interna oculta
¿Cuántos grupos tiene cada ciclo?SMR, que no tiene ninguno
¿Quién es el tutor de cada grupo?2ASIR, que todavía no tiene tutor
¿Cuántos alumnos tiene cada grupo?2ASIR, sin alumnado matriculado
¿Qué profesorado hay en cada departamento?Matemáticas, sin profesorado asignado
¿Qué alumnos tienen matrícula?Los 3 alumnos sin grupo ni matrícula

En todos estos casos, la fila «sin pareja» es justamente la que hay que detectar y resolver. Una composición externa conserva las filas de una tabla aunque no encuentren pareja en la otra, rellenando con NULL las columnas que faltan.

TipoConservaCuándo usarlo
A LEFT [OUTER] JOIN BTodas las filas de A«Todos los grupos, con su tutor si lo tienen»
A RIGHT [OUTER] JOIN BTodas las filas de BLo mismo al revés; poco usado en la práctica
A FULL [OUTER] JOIN BTodas las de A y todas las de BConciliar dos listados: detectar huérfanos en ambos lados

La palabra OUTER es opcional: LEFT JOIN y LEFT OUTER JOIN son lo mismo.

4.2 LEFT JOIN: el caso habitual

SELECT g.cod_grupo, g.cod_ciclo, g.turno, g.id_tutor,
       p.apellidos || ', ' || p.nombre AS tutor
FROM   grupo g
       LEFT JOIN profesor p ON p.id_profesor = g.id_tutor
ORDER  BY g.cod_grupo;
COD_GRUPOCOD_CICLOTURNOID_TUTORTUTOR
1ASIRASIRM105Brotons Sala, Elena
1DAMDAMM103Ferrándiz Mora, Lucía
1DAWDAWT106Cano Vidal, Raúl
2ASIRASIRM(null)(null)
2DAMDAMM104Navarro Ruiz, Andrés
2DAWDAWT107Gómez Pérez, Nuria

6 filas

Resultado esperado: las 6 filas de GRUPO, porque GRUPO está a la izquierda. La fila de 2ASIR se conserva con todas las columnas de PROFESOR a NULL. Para presentarlo se combina con las funciones de la UD06:

SELECT g.cod_grupo,
       NVL(p.apellidos || ', ' || p.nombre, '(sin tutor asignado)') AS tutor
FROM   grupo g
       LEFT JOIN profesor p ON p.id_profesor = g.id_tutor
ORDER  BY g.cod_grupo;

Otro caso clásico, con la composición en el sentido 1:N. Todos los ciclos, con sus grupos:

SELECT c.cod_ciclo, c.nombre AS ciclo, g.cod_grupo, g.turno
FROM   ciclo c
       LEFT JOIN grupo g ON g.cod_ciclo = c.cod_ciclo
ORDER  BY c.cod_ciclo, g.cod_grupo;
COD_CICLOCICLOCOD_GRUPOTURNO
ASIRAdministración de Sistemas Informáticos en Red1ASIRM
ASIRAdministración de Sistemas Informáticos en Red2ASIRM
DAMDesarrollo de Aplicaciones Multiplataforma1DAMM
DAMDesarrollo de Aplicaciones Multiplataforma2DAMM
DAWDesarrollo de Aplicaciones Web1DAWT
DAWDesarrollo de Aplicaciones Web2DAWT
SMRSistemas Microinformáticos y Redes(null)(null)

7 filas

Siete filas: las seis parejas reales más SMR, que aparece una vez con las columnas de grupo nulas.

4.3 COUNT en una composición externa: COUNT(*) frente a COUNT(columna)

Aquí los dos conceptos del tema se cruzan y producen el error más sutil de la unidad:

SELECT g.cod_grupo,
       COUNT(*)           AS filas_del_grupo,
       COUNT(a.id_alumno) AS alumnos
FROM   grupo g
       LEFT JOIN alumno a ON a.cod_grupo = g.cod_grupo
GROUP  BY g.cod_grupo
ORDER  BY g.cod_grupo;
COD_GRUPOFILAS_DEL_GRUPOALUMNOS
1ASIR55
1DAM77
1DAW66
2ASIR10
2DAM66
2DAW55

6 filas

En una composición externa, la fila sin pareja existe (por eso COUNT(*) vale 1 para 2ASIR) pero no tiene datos de la tabla opcional (por eso COUNT(a.id_alumno) vale 0). En un LEFT JOIN, cuenta siempre una columna de la tabla opcional, nunca COUNT(*), si lo que quieres es «cuántos hijos tiene cada padre». Lo mismo vale para SUM, que devolverá NULL en vez de 0: envuélvelo en NVL(SUM(...), 0).

4.4 La trampa del WHERE: cómo anular una composición externa

-- (a) INCORRECTA: el WHERE descarta la fila nula que acaba de crear el LEFT JOIN
SELECT g.cod_grupo, p.apellidos AS tutor
FROM   grupo g LEFT JOIN profesor p ON p.id_profesor = g.id_tutor
WHERE  p.id_departamento = 1;
-- 5 filas: 2ASIR desaparece

-- (b) CORRECTA: la condición sobre la tabla opcional va en el ON
SELECT g.cod_grupo, p.apellidos AS tutor
FROM   grupo g LEFT JOIN profesor p
       ON p.id_profesor = g.id_tutor AND p.id_departamento = 1;
-- 6 filas: 2ASIR aparece con tutor nulo

La explicación está en el orden de evaluación: el LEFT JOIN crea la fila de 2ASIR con p.id_departamento a NULL; después, el WHERE evalúa NULL = 1, que es UNKNOWN, y descarta la fila. Un WHERE sobre una columna de la tabla opcional convierte un LEFT JOIN en un INNER JOIN.

Regla para recordar: en una composición externa, las condiciones sobre la tabla opcional van en el ON; las condiciones sobre la tabla conservada pueden ir en el WHERE.

Hay una excepción deliberada: WHERE columna_opcional IS NULL, que es justo lo contrario, como se ve en el apartado siguiente.

4.5 Anticomposición: buscar lo que NO existe

Si lo que quieres son precisamente las filas sin pareja, el patrón es composición externa + IS NULL sobre una columna de la tabla opcional. Se llama anti-join:

-- Alumnado sin ninguna matrícula
SELECT a.id_alumno, a.nombre, a.apellidos, a.cod_grupo
FROM   alumno a
       LEFT JOIN matricula m ON m.id_alumno = a.id_alumno
WHERE  m.id_matricula IS NULL
ORDER  BY a.id_alumno;
ID_ALUMNONOMBREAPELLIDOSCOD_GRUPO
30ZoeIborra Valero(null)
31LucasAgulló Cerdá(null)
32IreneBelda Iborra(null)

3 filas

Son los tres alumnos que ni tienen grupo ni están matriculados: un dato de calidad de datos que secretaría debe revisar antes de cerrar la matrícula.

Elige para el IS NULL una columna de la tabla opcional que no admita nulos en su tabla (normalmente su clave primaria). Si eligieras una columna que sí admite nulos, no podrías distinguir «no hay pareja» de «hay pareja con ese valor nulo».

4.6 FULL OUTER JOIN

Conserva las filas de las dos tablas. Es la herramienta para conciliar dos listados:

-- Conciliación grupo ↔ tutor: grupos sin tutor y profesorado que no tutoriza
SELECT g.cod_grupo, p.id_profesor, p.apellidos
FROM   grupo g
       FULL OUTER JOIN profesor p ON p.id_profesor = g.id_tutor
WHERE  g.cod_grupo IS NULL OR p.id_profesor IS NULL
ORDER  BY g.cod_grupo, p.id_profesor;
COD_GRUPOID_PROFESORAPELLIDOS
2ASIR(null)(null)
(null)101Soler Ivars
(null)102Pastor Gil
(null)108Lillo Martí
(null)109Ortiz Llorca
(null)110Ramos Climent
(null)111Vicent Ribes
(null)112Esteve Juan

8 filas

Un grupo sin tutor y siete profesores que no son tutores de ningún grupo. Sin el WHERE, la composición devolvería 13 filas: 5 parejas + 1 + 7.

4.7 Nota histórica: el operador (+) de Oracle

Antes de SQL-92, Oracle expresaba las composiciones externas con el operador (+) junto a la columna de la tabla que puede quedar sin datos:

Oracle 26ai
-- Sintaxis propietaria antigua: equivalente a GRUPO LEFT JOIN PROFESOR
SELECT g.cod_grupo, p.apellidos
FROM   grupo g, profesor p
WHERE  p.id_profesor(+) = g.id_tutor;

Oracle la sigue admitiendo por compatibilidad, pero la propia documentación recomienda la sintaxis estándar: (+) no es portable, no permite FULL OUTER JOIN, no se puede combinar libremente con OR ni con IN, y en consultas de varias tablas es muy difícil de leer. Debes reconocerla al leer código antiguo y no escribirla.

¿Qué devuelve esta consulta y por qué?
SELECT c.cod_ciclo, COUNT(g.cod_grupo) AS grupos
FROM   ciclo c LEFT JOIN grupo g ON g.cod_ciclo = c.cod_ciclo
WHERE  g.turno = 'M'
GROUP  BY c.cod_ciclo;

Devuelve dos filas (ASIR con 2 y DAM con 2), no cuatro. El WHERE g.turno = 'M' elimina las filas en que g.turno es nulo, es decir, la fila de SMR creada por el LEFT JOIN, y también DAW, cuyos dos grupos son de tarde. La composición externa ha quedado anulada para SMR.

Para obtener los cuatro ciclos con el número de grupos de mañana de cada uno, la condición debe ir en el ON:

SELECT c.cod_ciclo, COUNT(g.cod_grupo) AS grupos
FROM   ciclo c LEFT JOIN grupo g
       ON g.cod_ciclo = c.cod_ciclo AND g.turno = 'M'
GROUP  BY c.cod_ciclo
ORDER  BY c.cod_ciclo;

Ahora salen 4 filas: ASIR 2, DAM 2, DAW 0, SMR 0.


5. Autocomposición y otras composiciones

5.1 Autocomposición (self join)

Una tabla también se puede componer consigo misma. Ocurre siempre que una relación se establece entre filas de la misma tabla: el jefe de un empleado, el prerrequisito de un módulo, el profesor que dirige un departamento del que forma parte.

La clave técnica es que hacen falta dos alias distintos para que Oracle (y el lector) sepa de qué «copia» de la tabla se habla.

En EduGest, cada profesor pertenece a un departamento y cada departamento tiene un jefe que es, a su vez, un profesor. Para poner al lado de cada docente el nombre de su jefatura de departamento, PROFESOR aparece dos veces:

SELECT p.apellidos || ', ' || p.nombre AS profesor,
       d.nombre                        AS departamento,
       NVL(j.apellidos || ', ' || j.nombre, '(vacante)') AS jefe_departamento,
       CASE WHEN j.id_profesor = p.id_profesor THEN 'Sí' ELSE 'No' END AS es_el_jefe
FROM   profesor p
       JOIN departamento d  ON d.id_departamento = p.id_departamento
       LEFT JOIN profesor j ON j.id_profesor     = d.id_jefe
WHERE  d.id_departamento IN (2, 3, 5)
ORDER  BY d.nombre, p.apellidos;
PROFESORDEPARTAMENTOJEFE_DEPARTAMENTOES_EL_JEFE
Esteve Juan, DavidAdministración y GestiónEsteve Juan, DavidSí
Ortiz Llorca, CarmenFormación y Orientación LaboralOrtiz Llorca, CarmenSí
Ramos Climent, SergioFormación y Orientación LaboralOrtiz Llorca, CarmenNo
Vicent Ribes, LauraInglésVicent Ribes, LauraSí

4 filas

Observa los dos alias de PROFESOR: p es «el docente» y j es «su jefe de departamento». El LEFT JOIN es necesario porque un departamento puede tener la jefatura vacante.

En una autocomposición, los alias no son opcionales: sin ellos, WHERE id_jefe = id_profesor sería ambiguo y Oracle daría ORA-00918. Elige alias que digan el papel que juega cada copia (p/j, empleado/jefe, modulo/prerrequisito), no t1/t2.

5.2 Autocomposición para comparar filas entre sí

El otro uso de la autocomposición es comparar filas de la misma tabla: duplicados, parejas, coincidencias.

-- Parejas de alumnos del mismo grupo que viven en la misma localidad
SELECT a.cod_grupo, a.localidad,
       a.apellidos AS alumno_1, b.apellidos AS alumno_2
FROM   alumno a
       JOIN alumno b ON b.cod_grupo = a.cod_grupo
                    AND b.localidad = a.localidad
                    AND b.id_alumno > a.id_alumno
WHERE  a.cod_grupo = '1DAM'
ORDER  BY a.localidad, a.apellidos, b.apellidos;
COD_GRUPOLOCALIDADALUMNO_1ALUMNO_2
1DAMAlicanteBrotons SorianoCerdá Pérez
1DAMAlicanteFerri BaezaBrotons Soriano
1DAMAlicanteFerri BaezaBrotons Soriano
1DAMAlicanteFerri BaezaCerdá Pérez
1DAMAlicanteFerri BaezaCerdá Pérez
1DAMAlicanteFerri BaezaFerri Baeza
1DAMSant Joan d’AlacantVerdú EspíQuiles Marco

7 filas

Dos detalles imprescindibles:

  • La condición b.id_alumno > a.id_alumno evita que un alumno se empareje consigo mismo y que cada pareja salga dos veces (A-B y B-A). Si la quitas, las 7 filas se convierten en 22.
  • En 1DAM hay dos alumnos distintos que se apellidan «Ferri Baeza» (Adrián y Paula), lo que explica las filas aparentemente repetidas: son parejas diferentes con apellidos iguales. Es un buen recordatorio de que el apellido no identifica a una persona: por eso la clave primaria es id_alumno.

5.3 CROSS JOIN legítimo: construir una rejilla completa

El producto cartesiano del apartado 3.2 parecía solo una fuente de errores, pero tiene un uso profesional importante: generar todas las combinaciones posibles para que en un informe aparezcan también las celdas con valor cero.

Un ejemplo real: el parte trimestral de faltas pide, para cada grupo, las faltas justificadas y las no justificadas. Si se calcula con composiciones normales, los grupos sin ninguna falta simplemente no aparecen, y las combinaciones sin faltas tampoco:

-- Sin rejilla: solo aparecen las combinaciones que existen -> 10 filas
SELECT a.cod_grupo, f.justificada, COUNT(*) AS faltas, SUM(f.horas) AS horas
FROM   falta_asistencia f
       JOIN matricula m ON m.id_matricula = f.id_matricula
       JOIN alumno a    ON a.id_alumno    = m.id_alumno
GROUP  BY a.cod_grupo, f.justificada
ORDER  BY a.cod_grupo, f.justificada;

Con CROSS JOIN se construye primero la rejilla «6 grupos × 2 tipos de falta» y después se cuelgan los datos con composiciones externas:

SELECT g.cod_grupo, t.justificada,
       COUNT(f.id_falta)    AS faltas,
       NVL(SUM(f.horas), 0) AS horas
FROM   grupo g
       CROSS JOIN (SELECT 'N' AS justificada FROM dual
                   UNION ALL
                   SELECT 'S'                FROM dual) t
       LEFT JOIN alumno a           ON a.cod_grupo    = g.cod_grupo
       LEFT JOIN matricula m        ON m.id_alumno    = a.id_alumno
       LEFT JOIN falta_asistencia f ON f.id_matricula = m.id_matricula
                                   AND f.justificada  = t.justificada
GROUP  BY g.cod_grupo, t.justificada
ORDER  BY g.cod_grupo, t.justificada;
COD_GRUPOJUSTIFICADAFALTASHORAS
1ASIRN46
1ASIRS55
1DAMN713
1DAMS25
1DAWN59
1DAWS25
2ASIRN00
2ASIRS00
2DAMN614
2DAMS611
2DAWN58
2DAWS45

12 filas

Ahora hay 12 filas (6 × 2) en vez de 10, y 2ASIR aparece con ceros explícitos, que es lo que jefatura necesita ver en un parte.

Fíjate en la combinación de técnicas: un CROSS JOIN deliberado para la rejilla, LEFT JOIN para no perder las celdas vacías, la condición f.justificada = t.justificada en el ON (si estuviera en el WHERE perderíamos de nuevo 2ASIR), COUNT de una columna de la tabla opcional y NVL sobre el SUM. Las cuatro reglas de los apartados anteriores actuando juntas.

5.4 Componer con una subconsulta o con una vista

Una composición no exige que los dos operandos sean tablas: puede ser una vista (apartado 7) o una subconsulta en el FROM, también llamada tabla en línea o vista en línea. Esto permite resumir primero y componer después:

-- Resumen por módulo (subconsulta) compuesto con el catálogo de módulos
SELECT mo.codigo, mo.cod_ciclo, mo.nombre AS modulo,
       r.calificadas, ROUND(r.media, 2) AS media
FROM   (SELECT id_modulo,
               COUNT(nota_final) AS calificadas,
               AVG(nota_final)   AS media
        FROM   matricula
        GROUP  BY id_modulo) r
       JOIN modulo mo ON mo.id_modulo = r.id_modulo
WHERE  r.media < 6
ORDER  BY r.media, mo.codigo;
CODIGOCOD_CICLOMODULOCALIFICADASMEDIA
0614DAWDespliegue de aplicaciones web54.9
0488DAMDesarrollo de interfaces65.42
0485DAMProgramación85.56
0483DAWSistemas informáticos65.71
0613DAWDesarrollo web en entorno servidor55.8
0484DAWBases de datos65.92
0484DAMBases de datos75.93

7 filas

Los siete módulos con nota media inferior a 6. Observa que el código 0484 aparece dos veces, una por ciclo: en EduGest el mismo módulo oficial existe en DAM y en DAW con id_modulo distinto, y por eso la columna cod_ciclo es imprescindible para interpretar el informe.


Sesión 7 2 h Teoría · Subconsultas, EXISTS y WITH

6. Subconsultas

6.1 Qué problema resuelven

Hay preguntas que no se pueden responder de una vez porque necesitan un dato que a su vez hay que calcular: «los módulos con más horas que la media» exige calcular primero la media. Una subconsulta es una consulta SELECT escrita dentro de otra, entre paréntesis, cuyo resultado usa la consulta exterior.

Según dónde se escriba y cuántas filas devuelva, se comporta de forma distinta:

PosiciónNombreDebe devolverEjemplo de uso
En el WHERE / HAVINGSubconsulta de una fila1 fila, 1 columnahoras > (SELECT AVG(horas) …)
En el WHERE / HAVINGSubconsulta de varias filasN filas, 1 columnaid_alumno IN (SELECT …)
En el FROMTabla en líneaN filas, N columnasResumir antes de componer
En el SELECTSubconsulta escalar1 fila, 1 columnaUn dato calculado por fila
Con EXISTSSubconsulta de existenciaLo que sea«¿hay alguna fila que…?»

6.2 Subconsultas de una fila

Se usan con los operadores de comparación normales (=, <>, >, <, >=, <=). La subconsulta debe devolver exactamente una fila y una columna; normalmente lo garantiza una función de agregado sin GROUP BY.

-- Media de horas de los módulos de DAM: 132
SELECT codigo, nombre, horas
FROM   modulo
WHERE  cod_ciclo = 'DAM'
AND    horas > (SELECT AVG(horas) FROM modulo WHERE cod_ciclo = 'DAM')
ORDER  BY horas DESC, codigo;
CODIGONOMBREHORAS
0485Programación256
0483Sistemas informáticos160
0484Bases de datos160

3 filas

Si una subconsulta de una fila devuelve más de una, Oracle lanza ORA-01427: single-row subquery returns more than one row. Es el error típico de WHERE horas > (SELECT horas FROM modulo WHERE cod_ciclo = 'DAM'): hay 10 módulos de DAM, así que hay 10 valores. La solución es o resumir (AVG, MAX…) o usar un operador de varias filas (ANY, ALL, IN).

Las subconsultas también funcionan en el HAVING, cuando la condición sobre el grupo se compara con un valor global:

-- Módulos cuya nota media supera la media general del centro (6,36)
SELECT id_modulo, COUNT(nota_final) AS calificadas, ROUND(AVG(nota_final), 2) AS media
FROM   matricula
GROUP  BY id_modulo
HAVING AVG(nota_final) > (SELECT AVG(nota_final) FROM matricula)
ORDER  BY media DESC
FETCH FIRST 5 ROWS ONLY;
ID_MODULOCALIFICADASMEDIA
2148
2057.4
577.21
2456.85
2256.8

5 filas

6.3 Subconsultas de varias filas: IN, ANY y ALL

OperadorSignificadoEquivalente
IN (subconsulta)Igual a alguno de los valores= ANY
NOT IN (subconsulta)Distinto de todos los valores<> ALL
> ANY (subconsulta)Mayor que al menos uno: mayor que el mínimo> MIN(…)
> ALL (subconsulta)Mayor que todos: mayor que el máximo> MAX(…)
< ANY / < ALLMenor que el máximo / menor que el mínimo
-- Alumnado matriculado en Bases de datos (código 0484, en cualquier ciclo)
SELECT a.id_alumno, a.apellidos || ', ' || a.nombre AS alumno, a.cod_grupo
FROM   alumno a
WHERE  a.id_alumno IN (SELECT m.id_alumno
                       FROM   matricula m
                              JOIN modulo mo ON mo.id_modulo = m.id_modulo
                       WHERE  mo.codigo = '0484')
ORDER  BY a.cod_grupo, a.apellidos;
ID_ALUMNOALUMNOCOD_GRUPO
5Brotons Soriano, Andrea1DAM
6Cerdá Pérez, Tomás1DAM
1Ferri Baeza, Adrián1DAM
4Ferri Baeza, Paula1DAM
2Iborra Ferri, Rubén1DAM
7Quiles Marco, Valeria1DAM
3Verdú Espí, Noelia1DAM
18Alemany Pérez, Daniel1DAW
16Espí Marco, Julia1DAW
17Iborra Alemany, Jorge1DAW
15Soriano Domènech, Manuel1DAW
19Torregrosa Planelles, Alba1DAW
14Valero Cerdá, Carla1DAW

13 filas

-- Módulos de DAW con más horas que el módulo más corto de ASIR (96 h)
SELECT codigo, nombre, horas
FROM   modulo
WHERE  cod_ciclo = 'DAW'
AND    horas > ANY (SELECT horas FROM modulo WHERE cod_ciclo = 'ASIR')
ORDER  BY horas, codigo;
CODIGONOMBREHORAS
0615Diseño de interfaces web120
0373Lenguajes de marcas y sistemas de gestión de información128
0612Desarrollo web en entorno cliente140
0483Sistemas informáticos160
0484Bases de datos160
0613Desarrollo web en entorno servidor160
0485Programación256

7 filas

ANY y ALL son difíciles de leer. Traduce siempre a la forma con agregado antes de escribirlos: > ANY es > (SELECT MIN(...)) y > ALL es > (SELECT MAX(...)). Si dudas, escribe la versión con MIN/MAX: es equivalente y mucho más clara. La única diferencia práctica aparece con subconsultas vacías.

6.4 La trampa de NOT IN con nulos

-- Profesorado que NO es tutor de ningún grupo
SELECT COUNT(*) AS filas
FROM   profesor
WHERE  id_profesor NOT IN (SELECT id_tutor FROM grupo);
FILAS
0

1 fila

Cero filas, cuando sabemos que hay siete profesores sin tutoría. No es un error de Oracle: es la lógica de tres valores de la UD06. La subconsulta devuelve seis valores, y uno de ellos es NULL (2ASIR no tiene tutor). Entonces:

id_profesor NOT IN (105, 103, 106, NULL, 104, 107)
   ≡  id_profesor <> 105 AND … AND id_profesor <> NULL
   ≡  TRUE AND … AND UNKNOWN
   ≡  UNKNOWN   →  el WHERE no deja pasar ninguna fila

Hay dos formas correctas de escribirlo:

-- (a) Excluir los nulos de la subconsulta
SELECT id_profesor, apellidos
FROM   profesor
WHERE  id_profesor NOT IN (SELECT id_tutor FROM grupo WHERE id_tutor IS NOT NULL)
ORDER  BY id_profesor;

-- (b) NOT EXISTS: inmune a los nulos (recomendada)
SELECT p.id_profesor, p.apellidos || ', ' || p.nombre AS profesor
FROM   profesor p
WHERE  NOT EXISTS (SELECT 1 FROM grupo g WHERE g.id_tutor = p.id_profesor)
ORDER  BY p.id_profesor;
ID_PROFESORPROFESOR
101Soler Ivars, Marta
102Pastor Gil, Javier
108Lillo Martí, Pablo
109Ortiz Llorca, Carmen
110Ramos Climent, Sergio
111Vicent Ribes, Laura
112Esteve Juan, David

7 filas

NOT IN sobre una subconsulta que pueda devolver NULL es un error de diseño esperando a ocurrir. El día en que alguien deje una clave ajena a nulo, la consulta dejará de devolver filas sin dar ningún error. Usa NOT EXISTS o añade IS NOT NULL en la subconsulta. IN (afirmativo) no sufre este problema: con nulos simplemente no empareja.

6.5 Subconsultas correlacionadas y EXISTS

Una subconsulta es correlacionada cuando hace referencia a una columna de la consulta exterior. Conceptualmente se evalúa una vez por cada fila de la consulta exterior, con el valor de esa fila.

-- Matrículas de 2DAW cuya nota supera la media de SU módulo
SELECT a.apellidos || ', ' || a.nombre AS alumno, mo.codigo, m.nota_final,
       (SELECT ROUND(AVG(m2.nota_final), 2)
        FROM   matricula m2
        WHERE  m2.id_modulo = m.id_modulo) AS media_del_modulo
FROM   matricula m
       JOIN alumno a  ON a.id_alumno  = m.id_alumno
       JOIN modulo mo ON mo.id_modulo = m.id_modulo
WHERE  a.cod_grupo = '2DAW'
AND    m.nota_final > (SELECT AVG(m2.nota_final)
                       FROM   matricula m2
                       WHERE  m2.id_modulo = m.id_modulo)
ORDER  BY mo.codigo, m.nota_final DESC;
ALUMNOCODIGONOTA_FINALMEDIA_DEL_MODULO
Carbonell Soriano, Elena06128.756.38
Sala Brotons, Mateo06126.56.38
Carbonell Soriano, Elena06137.255.8
Amorós Guillem, Sara061365.8
Verdú Castelló, Nicolás061484.9
Sala Brotons, Mateo06147.54.9
Sala Brotons, Mateo06159.256.75
Amorós Guillem, Sara06157.256.75
Planelles Marco, Sofía061576.75

9 filas

La correlación está en m2.id_modulo = m.id_modulo: m es la fila exterior y m2 recorre todas las matrículas de ese módulo. Sin la correlación, la media sería la general del centro y el resultado otro.

EXISTS es un operador que solo pregunta si la subconsulta devuelve al menos una fila; no mira los valores. Por eso dentro se escribe SELECT 1 (o SELECT *): da lo mismo.

-- Alumnado con al menos una falta no justificada
SELECT a.id_alumno, a.apellidos || ', ' || a.nombre AS alumno, a.cod_grupo
FROM   alumno a
WHERE  EXISTS (SELECT 1
               FROM   falta_asistencia f
                      JOIN matricula m ON m.id_matricula = f.id_matricula
               WHERE  m.id_alumno    = a.id_alumno
               AND    f.justificada  = 'N')
ORDER  BY a.cod_grupo, a.apellidos
FETCH FIRST 6 ROWS ONLY;
ID_ALUMNOALUMNOCOD_GRUPO
25Iborra Torregrosa, Diego1ASIR
27Tomás Vidal, Álex1ASIR
6Cerdá Pérez, Tomás1DAM
1Ferri Baeza, Adrián1DAM
7Quiles Marco, Valeria1DAM
3Verdú Espí, Noelia1DAM

6 filas

TécnicaCuándo preferirla
JOINNecesitas mostrar columnas de la otra tabla
IN (subconsulta)Solo filtras, la subconsulta es pequeña y no devuelve nulos
EXISTSSolo filtras, la subconsulta es grande o correlacionada; o hay nulos
NOT EXISTS«No existe ninguno que…»: siempre preferible a NOT IN
LEFT JOIN … IS NULLIgual que NOT EXISTS; elige la que tu equipo lea mejor

Una composición puede duplicar filas si la otra tabla tiene varias coincidencias; EXISTS nunca lo hace, porque solo pregunta si hay alguna. Si una consulta con JOIN te devuelve filas repetidas y lo único que querías era filtrar, cámbiala por EXISTS en lugar de añadir un DISTINCT.

6.6 Dar nombre a los pasos: la cláusula WITH (CTE)

Cuando una consulta necesita dos o tres pasos intermedios, anidar subconsultas la vuelve ilegible. La cláusula WITH, también llamada expresión de tabla común (Common Table Expression, CTE), permite dar nombre a cada paso y escribirlos en el orden en que se piensan.

SQL estándar
WITH nombre1 AS (SELECT …),
     nombre2 AS (SELECT … FROM nombre1 …)
SELECT … FROM nombre2 …

La misma consulta del apartado 5.4, reescrita:

WITH resumen_modulo AS (
    SELECT id_modulo,
           COUNT(nota_final) AS calificadas,
           AVG(nota_final)   AS media
    FROM   matricula
    GROUP  BY id_modulo
)
SELECT mo.codigo, mo.cod_ciclo, mo.nombre AS modulo,
       r.calificadas, ROUND(r.media, 2) AS media
FROM   resumen_modulo r
       JOIN modulo mo ON mo.id_modulo = r.id_modulo
WHERE  r.media < 6
ORDER  BY r.media, mo.codigo;

Devuelve exactamente las 7 filas del apartado 5.4. Lo que cambia es la legibilidad:

Subconsulta anidadaCláusula WITH
Se lee de dentro hacia fueraSe lee de arriba abajo, en el orden del razonamiento
El paso intermedio no tiene nombreresumen_modulo documenta qué es
Si se necesita dos veces, hay que repetirlaSe escribe una vez y se usa varias
Difícil de depurarPuedes ejecutar la CTE sola para comprobarla

Un ejemplo con dos pasos, donde la ventaja es evidente:

WITH media_alumno AS (
    SELECT a.id_alumno, a.cod_grupo,
           a.apellidos || ', ' || a.nombre AS alumno,
           AVG(m.nota_final) AS media
    FROM   alumno a
           JOIN matricula m ON m.id_alumno = a.id_alumno
    WHERE  a.cod_grupo IS NOT NULL
    GROUP  BY a.id_alumno, a.cod_grupo, a.apellidos, a.nombre
),
media_grupo AS (
    SELECT cod_grupo, AVG(media) AS media_del_grupo
    FROM   media_alumno
    GROUP  BY cod_grupo
)
SELECT ma.cod_grupo, ma.alumno,
       ROUND(ma.media, 2)                      AS media_alumno,
       ROUND(mg.media_del_grupo, 2)            AS media_grupo,
       ROUND(ma.media - mg.media_del_grupo, 2) AS diferencia
FROM   media_alumno ma
       JOIN media_grupo mg ON mg.cod_grupo = ma.cod_grupo
WHERE  ma.cod_grupo = '2DAM'
ORDER  BY ma.media DESC;
COD_GRUPOALUMNOMEDIA_ALUMNOMEDIA_GRUPODIFERENCIA
2DAMAlemany Vidal, Martina8.46.182.22
2DAMCerdá Tomás, Nerea6.426.180.24
2DAMRipoll Agulló, Iván5.946.18-0.24
2DAMBrotons Iborra, Hugo5.86.18-0.38
2DAMBelda Quiles, Lucía5.386.18-0.8
2DAMDomènech Quiles, María5.156.18-1.03

6 filas

media_grupo es aquí la media de las medias de los alumnos del grupo: 6,18. No coincide con la media de todas las notas de 2DAM, que es 6,17 (AVG sobre las 31 notas del grupo), porque los alumnos no tienen el mismo número de matrículas y la media de medias pondera igual a todos. La diferencia es pequeña con estos datos, pero es real: es una decisión de cálculo que hay que documentar en el informe, porque en estadística escolar se usan las dos cifras y confundirlas es un error habitual.

WITH es SQL estándar y está en Oracle desde la versión 9i. Oracle admite además CTE recursivas (WITH … UNION ALL con referencia a sí misma) para recorrer jerarquías; es materia de ampliación, igual que la cláusula propietaria CONNECT BY.

Autoevaluación 3 preguntas
1 ¿Por qué SELECT nombre FROM profesor WHERE id_profesor NOT IN (SELECT id_tutor FROM grupo) no devuelve ninguna fila?
2 ¿Qué error devuelve Oracle en WHERE horas > (SELECT horas FROM modulo WHERE cod_ciclo = 'DAM')?
3 ¿Cuál es la ventaja principal de una cláusula WITH frente a la misma subconsulta anidada?

Sesión 9 1 h Teoría · Vistas con composiciones y operadores de conjuntos

7. Vistas con composiciones

7.1 De la consulta útil a la vista

En la UD05 creaste vistas sobre una sola tabla. Su verdadero valor aparece ahora: una vista puede encapsular una composición de varias tablas y ofrecerla al resto del centro como si fuera una tabla sencilla.

VentajaEn la práctica
ReutilizaciónEl acta se escribe una vez; secretaría, tutoría y jefatura la consultan
SimplicidadQuien consulta no necesita conocer las claves ajenas ni los JOIN
SeguridadSe concede SELECT sobre la vista, no sobre las tablas: el DNI no se expone
Independencia lógicaSi cambia la estructura de las tablas, se adapta la vista y las aplicaciones siguen funcionando
CoherenciaLa regla «aprobado es nota ≥ 5» está escrita en un sitio, no en cada informe

7.2 La vista v_acta

El acta de evaluación de un módulo es el informe más usado del centro: alumno, módulo, nota y calificación. Necesita tres tablas (MATRICULA, ALUMNO y MODULO), y la calificación es una regla de negocio que no conviene repetir en cada consulta.

Oracle 26ai
CREATE OR REPLACE VIEW v_acta AS
    SELECT m.id_matricula,
           a.cod_grupo,
           a.id_alumno,
           a.nia,
           a.apellidos || ', ' || a.nombre AS alumno,
           mo.codigo                       AS cod_modulo,
           mo.nombre                       AS modulo,
           m.curso_academico,
           m.convocatoria,
           m.nota_final                    AS nota,
           CASE WHEN m.nota_final IS NULL THEN 'NC'
                WHEN m.nota_final >= 5    THEN 'APTO'
                ELSE                           'NO APTO'
           END                             AS calificacion
    FROM   matricula m
           JOIN alumno a  ON a.id_alumno  = m.id_alumno
           JOIN modulo mo ON mo.id_modulo = m.id_modulo
WITH READ ONLY;
ElementoPor qué está ahí
OR REPLACEPermite corregir la vista sin perder los privilegios ya concedidos
Alias AS alumno, AS cod_modulo…Toda columna calculada necesita alias: sin él, CREATE VIEW falla con ORA-00998
CASE … 'NC'NC (no calificado) se distingue de NO APTO: un no presentado no es un suspenso
El CASE pregunta primero por NULLSi no, NULL >= 5 sería UNKNOWN y caería en el ELSE, marcando como NO APTO a quien no se ha presentado
WITH READ ONLYEs un informe: nadie debe modificar notas a través de él

Consultarla es como consultar una tabla:

-- Acta de Bases de datos (0484) del grupo 1DAM
SELECT nia, alumno, nota, calificacion
FROM   v_acta
WHERE  cod_grupo = '1DAM' AND cod_modulo = '0484'
ORDER  BY alumno;
NIAALUMNONOTACALIFICACION
10450185Brotons Soriano, Andrea5.75APTO
10450222Cerdá Pérez, Tomás5.25APTO
10450037Ferri Baeza, Adrián4.75NO APTO
10450148Ferri Baeza, Paula6.5APTO
10450074Iborra Ferri, Rubén4.75NO APTO
10450259Quiles Marco, Valeria6.5APTO
10450111Verdú Espí, Noelia8APTO

7 filas

Y, sobre todo, se puede agrupar como cualquier tabla. El resumen de la sesión de evaluación de 1DAW:

SELECT cod_modulo, modulo,
       SUM(CASE WHEN calificacion = 'APTO'    THEN 1 ELSE 0 END) AS aptos,
       SUM(CASE WHEN calificacion = 'NO APTO' THEN 1 ELSE 0 END) AS no_aptos,
       SUM(CASE WHEN calificacion = 'NC'      THEN 1 ELSE 0 END) AS nc
FROM   v_acta
WHERE  cod_grupo = '1DAW'
GROUP  BY cod_modulo, modulo
ORDER  BY cod_modulo;
COD_MODULOMODULOAPTOSNO_APTOSNC
0373Lenguajes de marcas y sistemas de gestión de información510
0483Sistemas informáticos420
0484Bases de datos600
0485Programación420
0487Entornos de desarrollo501

5 filas

Compara el tamaño de esta consulta con la que haría falta sin la vista: tres composiciones, el CASE repetido tres veces y la regla de aprobado escrita otra vez. Esa reducción es la razón de ser de las vistas.

Nombra las vistas con un prefijo (v_) y documenta su finalidad en el diccionario de datos:

COMMENT ON TABLE v_acta IS 'Acta de evaluación: una fila por matrícula con su calificación APTO/NO APTO/NC';

7.3 Vistas actualizables

Una vista no almacena datos, pero en algunos casos se puede modificar a través de ella y Oracle traslada el cambio a la tabla base. Para eso, la vista debe tener una tabla con clave preservada: Oracle tiene que poder identificar sin ambigüedad qué fila de qué tabla corresponde a cada fila de la vista.

Una vista no es actualizable si contieneError típico
GROUP BY, DISTINCT o funciones de agregadoORA-01732: data manipulation operation not legal on this view
Operadores de conjuntos (UNION, INTERSECT, MINUS)ORA-01732
Columnas calculadas (si se intenta modificar esa columna)ORA-01733: virtual column not allowed here
WITH READ ONLYORA-42399: cannot perform a DML operation on a read-only view

En una vista con composiciones, solo son modificables las columnas de las tablas cuya clave se preserva:

-- v_acta tiene WITH READ ONLY: este UPDATE falla (ORA-42399)
UPDATE v_acta SET nota = 7 WHERE id_matricula = 10001;

Si quisiéramos permitir al profesorado poner notas a través de una vista, habría que crearla sin WITH READ ONLY y comprobar que MATRICULA conserva su clave:

CREATE OR REPLACE VIEW v_notas_1dam AS
    SELECT m.id_matricula, m.id_alumno, m.id_modulo, m.nota_final, a.cod_grupo
    FROM   matricula m
           JOIN alumno a ON a.id_alumno = m.id_alumno
    WHERE  a.cod_grupo = '1DAM'
WITH CHECK OPTION CONSTRAINT ck_v_notas_1dam;

-- Comprobar qué columnas son actualizables
SELECT column_name, updatable, insertable, deletable
FROM   user_updatable_columns
WHERE  table_name = 'V_NOTAS_1DAM';
CláusulaEfecto
WITH READ ONLYProhíbe INSERT, UPDATE y DELETE a través de la vista
WITH CHECK OPTIONPermite modificar, pero rechaza los cambios que harían que la fila dejara de cumplir el WHERE de la vista (ORA-01402)
(ninguna)Se permite lo que Oracle considere actualizable

Criterio profesional: una vista de informe se crea siempre WITH READ ONLY. Es una decisión de seguridad gratuita que evita modificaciones accidentales y documenta la intención de la vista. Solo se omite cuando la vista existe precisamente para permitir modificaciones controladas, y entonces se añade WITH CHECK OPTION.

7.4 Consultar las vistas en el diccionario

SELECT view_name, read_only, text_length
FROM   user_views
ORDER  BY view_name;

-- Ver el SELECT que define una vista
SELECT text FROM user_views WHERE view_name = 'V_ACTA';

-- De qué tablas depende cada vista (útil antes de un ALTER TABLE)
SELECT name, referenced_name, referenced_type
FROM   user_dependencies
WHERE  name = 'V_ACTA';

DROP VIEW v_notas_1dam;

Si se elimina o se modifica una tabla de la que depende una vista, la vista queda inválida (status = 'INVALID' en USER_OBJECTS) y cualquier consulta falla con ORA-04063: view has errors. Antes de un ALTER TABLE o un DROP TABLE, consulta USER_DEPENDENCIES. Oracle no impide borrar una tabla porque tenga vistas encima: las vistas no son restricciones.


8. Múltiples selecciones: UNION, INTERSECT y MINUS

8.1 Qué problema resuelven

Las composiciones combinan tablas a lo ancho (añaden columnas). Los operadores de conjuntos las combinan a lo alto: toman los resultados de dos o más consultas y los apilan, los cruzan o los restan. Son la herramienta cuando la pregunta tiene forma de «esto y además aquello», «lo que está en los dos» o «lo que está aquí pero no allí».

OperadorDevuelveDuplicadosEstándar
UNIONFilas de las dos consultasLos eliminaSí
UNION ALLFilas de las dos consultasLos conservaSí
INTERSECTSolo las filas que están en ambasLos eliminaSí
MINUSFilas de la primera que no están en la segundaLos eliminaEXCEPT en el estándar y en PostgreSQL y SQL Server
flowchart LR
    subgraph UNION
        A1((A)) --- B1((B))
    end
    subgraph INTERSECT
        A2((A)) --- B2((B))
    end
    subgraph MINUS
        A3((A)) --- B3((B))
    end

MINUS es el nombre histórico de Oracle. Desde Oracle 21c se admite también EXCEPT, que es el nombre del estándar y el que usan PostgreSQL y SQL Server. En este curso se escribe MINUS, que funciona en todas las versiones de Oracle, y se indica aquí la equivalencia.

8.2 Requisitos de compatibilidad

Para combinar dos consultas con un operador de conjuntos:

  1. Deben tener el mismo número de columnas. Si no: ORA-01789: query block has incorrect number of result columns.
  2. Las columnas deben tener tipos compatibles, en el mismo orden. Si no: ORA-01790: expression must have same datatype as corresponding expression.
  3. Los nombres de las columnas los pone la primera consulta.
  4. El ORDER BY va una sola vez, al final, y se refiere a las columnas del resultado (por nombre de la primera consulta o por posición).

8.3 UNION ALL y UNION

-- Directorio del grupo 1DAM: alumnado y su tutor en un solo listado
SELECT 'Alumno' AS tipo, a.apellidos || ', ' || a.nombre AS persona, a.email
FROM   alumno a
WHERE  a.cod_grupo = '1DAM'
UNION ALL
SELECT 'Tutor', p.apellidos || ', ' || p.nombre, p.email
FROM   profesor p
       JOIN grupo g ON g.id_tutor = p.id_profesor
WHERE  g.cod_grupo = '1DAM'
ORDER  BY tipo, persona;
TIPOPERSONAEMAIL
AlumnoBrotons Soriano, Andreaandreabrotons5@alu.edugest.es
AlumnoCerdá Pérez, Tomástomascerda6@alu.edugest.es
AlumnoFerri Baeza, Adriánadrianferri1@alu.edugest.es
AlumnoFerri Baeza, Paulapaulaferri4@alu.edugest.es
AlumnoIborra Ferri, Rubénrubeniborra2@alu.edugest.es
AlumnoQuiles Marco, Valeriavaleriaquiles7@alu.edugest.es
AlumnoVerdú Espí, Noelianoeliaverdu3@alu.edugest.es
TutorFerrándiz Mora, Lucíalferrandiz@edugest.es

8 filas

La columna literal 'Alumno' / 'Tutor' es un recurso muy útil: etiqueta el origen de cada fila en el resultado combinado.

La diferencia entre UNION y UNION ALL se ve con los duplicados:

-- UNION ALL: 13 filas (6 + 7), con localidades repetidas
SELECT localidad FROM alumno WHERE cod_grupo = '1DAM'
UNION ALL
SELECT localidad FROM alumno WHERE cod_grupo = '1DAW';

-- UNION: 5 filas, cada localidad una sola vez
SELECT localidad FROM alumno WHERE cod_grupo = '1DAM'
UNION
SELECT localidad FROM alumno WHERE cod_grupo = '1DAW'
ORDER  BY 1;
LOCALIDAD
Alicante
El Campello
Elche
Mutxamel
Sant Joan d’Alacant

5 filas

Usa UNION ALL salvo que necesites eliminar duplicados. Para eliminarlos, UNION tiene que ordenar o aplicar un hash a todo el resultado, lo que cuesta tiempo y memoria. Y si las dos consultas son disjuntas por construcción (alumnado y profesorado, dos cursos académicos distintos), UNION hace ese trabajo para nada.

8.4 INTERSECT y MINUS

-- Códigos de módulo que existen tanto en DAM como en ASIR
SELECT codigo FROM modulo WHERE cod_ciclo = 'DAM'
INTERSECT
SELECT codigo FROM modulo WHERE cod_ciclo = 'ASIR'
ORDER  BY 1;
CODIGO
0373

1 fila

Un solo módulo transversal, Lenguajes de marcas (0373), común a los dos ciclos.

-- Módulos propios de ASIR: los que NO existen en DAM
SELECT codigo FROM modulo WHERE cod_ciclo = 'ASIR'
MINUS
SELECT codigo FROM modulo WHERE cod_ciclo = 'DAM'
ORDER  BY 1;
CODIGO
0369
0370
0371
0372

4 filas

MINUS no es simétrico: A MINUS B y B MINUS A devuelven cosas distintas. Aquí DAM MINUS ASIR daría 9 códigos. Antes de escribirlo, di en voz alta cuál es «lo que quiero» y cuál es «lo que descarto».

8.5 ¿Operador de conjuntos o composición?

Muchas preguntas se pueden resolver de las dos formas, y conviene saber elegir:

PreguntaCon conjuntosAlternativaCuál preferir
Localidades de DAM o de DAWUNIONWHERE cod_ciclo IN ('DAM','DAW') + DISTINCTLa alternativa: una sola pasada
Localidades de DAM y de DAWINTERSECTEXISTS dobleINTERSECT, más legible
Códigos de ASIR que no están en DAMMINUSNOT EXISTSMINUS si son listas, NOT EXISTS si hay que mostrar más columnas
Apilar dos informes con columnas distintasUNION ALL con etiqueta—UNION ALL

El criterio general: si las dos consultas son estructuralmente distintas (tablas distintas, filtros distintos), los operadores de conjuntos ganan en claridad. Si solo cambia una condición, suele ser mejor una sola consulta con IN o con OR.


Sesión 11 1 h Teoría · Funciones analíticas y optimización
Curiosidad Las funciones analíticas llegaron antes que el estándar

Oracle incorporó las funciones analíticas (RANK, LAG, SUM … OVER) en la versión 8i, hacia 1999. El estándar SQL no las recogió hasta SQL:2003.

9. Funciones analíticas

Este apartado es ampliación profesional. No forma parte del contenido mínimo del RA3, pero aparece en casi cualquier informe real y en las ofertas de empleo, y resuelve con una línea lo que de otro modo exige dos subconsultas. Está en el currículo de forma implícita, a través de las consultas resumen y de la optimización.

9.1 El problema: resumir sin perder el detalle

GROUP BY resume, pero destruye el detalle: si agrupas por módulo para calcular la media, pierdes las notas individuales. Si quieres ver cada nota junto a la media de su módulo, con GROUP BY necesitas una subconsulta correlacionada (apartado 6.5) o una CTE y una composición.

Las funciones analíticas (o funciones de ventana) resuelven esto: calculan un agregado sobre un conjunto de filas relacionadas, pero devuelven un valor por fila, sin colapsarlas.

función(argumentos) OVER ( [PARTITION BY columnas] [ORDER BY columnas] )
CláusulaSignificado
OVER (...)Marca la función como analítica en lugar de agregada
PARTITION BYDivide las filas en particiones (el equivalente al GROUP BY, pero sin colapsar)
ORDER BYOrdena dentro de cada partición: imprescindible para rangos, LAG y LEAD

9.2 AVG analítico frente a AVG agrupado

-- Notas de Programación (0485) en 1DAW, cada una junto a la media del módulo
SELECT a.apellidos                           AS alumno,
       mo.codigo,
       m.nota_final,
       ROUND(AVG(m.nota_final) OVER (PARTITION BY m.id_modulo), 2) AS media_modulo,
       ROUND(m.nota_final
             - AVG(m.nota_final) OVER (PARTITION BY m.id_modulo), 2) AS diferencia
FROM   matricula m
       JOIN alumno a  ON a.id_alumno  = m.id_alumno
       JOIN modulo mo ON mo.id_modulo = m.id_modulo
WHERE  a.cod_grupo = '1DAW' AND mo.codigo = '0485'
ORDER  BY m.nota_final DESC, a.apellidos;
ALUMNOCODIGONOTA_FINALMEDIA_MODULODIFERENCIA
Alemany Pérez04857.2561.25
Espí Marco04857.2561.25
Torregrosa Planelles04857.2561.25
Soriano Domènech0485660
Valero Cerdá04854.256-1.75
Iborra Alemany048546-2

6 filas

Seis filas de detalle y la media repetida en cada una. Con GROUP BY el resultado tendría una sola fila. Sin PARTITION BY, la media sería la de todas las filas del resultado (la misma, en este caso, porque solo hay un módulo).

9.3 Rangos: ROW_NUMBER, RANK y DENSE_RANK

Las tres numeran las filas dentro de cada partición según el ORDER BY, pero tratan los empates de forma distinta:

-- Clasificación de Programación de 1º de DAM (id_modulo 3)
SELECT a.apellidos || ', ' || a.nombre AS alumno, m.nota_final,
       ROW_NUMBER() OVER (ORDER BY m.nota_final DESC, a.apellidos) AS fila,
       RANK()       OVER (ORDER BY m.nota_final DESC)              AS rango,
       DENSE_RANK() OVER (ORDER BY m.nota_final DESC)              AS rango_denso
FROM   matricula m
       JOIN alumno a ON a.id_alumno = m.id_alumno
WHERE  m.id_modulo = 3
ORDER  BY fila;
ALUMNONOTA_FINALFILARANGORANGO_DENSO
Quiles Marco, Valeria8111
Cerdá Pérez, Tomás7.25222
Ferri Baeza, Adrián7.25322
Verdú Espí, Noelia6.75443
Brotons Soriano, Andrea4.75554
Iborra Ferri, Rubén4.5665
Ferri Baeza, Paula3.25776
Belda Quiles, Lucía2.75887

8 filas

FunciónCon dos empatados en el puesto 2Cuándo usarla
ROW_NUMBER()2 y 3 (elige arbitrariamente)Paginar, elegir una fila por grupo
RANK()2 y 2, y el siguiente es 4Clasificaciones deportivas o académicas
DENSE_RANK()2 y 2, y el siguiente es 3«Posiciones distintas de nota»

ROW_NUMBER() es no determinista si el ORDER BY del OVER no distingue las filas empatadas: cada ejecución puede asignar los números al revés. Si el resultado se va a publicar o comparar, añade siempre un criterio de desempate (aquí, a.apellidos).

Lucía Belda Quiles es de 2DAM y aparece en la clasificación de un módulo de primero: está repitiendo el módulo en segunda convocatoria. Los datos reales tienen estos casos, y conviene no dar por supuesto que «módulo de primero» equivale a «alumnado de primero».

9.4 Los mejores de cada grupo

Una función analítica no se puede usar en el WHERE (se evalúa después, junto al SELECT). Para filtrar por su resultado hay que envolverla en una subconsulta o en una CTE. Este es el patrón estándar del top-N por grupo:

WITH clasificacion AS (
    SELECT a.cod_grupo,
           a.apellidos || ', ' || a.nombre AS alumno,
           AVG(m.nota_final)                                                   AS media,
           RANK() OVER (PARTITION BY a.cod_grupo ORDER BY AVG(m.nota_final) DESC) AS puesto
    FROM   alumno a
           JOIN matricula m ON m.id_alumno = a.id_alumno
    WHERE  a.cod_grupo IS NOT NULL
    GROUP  BY a.cod_grupo, a.id_alumno, a.apellidos, a.nombre
)
SELECT cod_grupo, puesto, alumno, ROUND(media, 2) AS media
FROM   clasificacion
WHERE  puesto <= 2
ORDER  BY cod_grupo, puesto;
COD_GRUPOPUESTOALUMNOMEDIA
1ASIR1Pascual Brotons, Aitana8.4
1ASIR2Tomás Guillem, Víctor7.85
1DAM1Cerdá Pérez, Tomás6.95
1DAM2Brotons Soriano, Andrea6.7
1DAW1Espí Marco, Julia7.05
1DAW2Torregrosa Planelles, Alba6.85
2DAM1Alemany Vidal, Martina8.4
2DAM2Cerdá Tomás, Nerea6.42
2DAW1Sala Brotons, Mateo7.13
2DAW2Verdú Castelló, Nicolás6

10 filas

Fíjate en que la función analítica se aplica sobre un agregado (AVG(m.nota_final)): primero se agrupa por alumno y después se clasifica dentro del grupo. Es perfectamente válido, porque las funciones analíticas se evalúan después del GROUP BY.

9.5 LAG y LEAD: mirar la fila anterior y la siguiente

-- Historial de faltas de Martina Alemany Vidal (id_alumno 9) y días entre faltas
SELECT f.fecha, f.horas, f.justificada,
       LAG(f.fecha) OVER (ORDER BY f.fecha)            AS falta_anterior,
       f.fecha - LAG(f.fecha) OVER (ORDER BY f.fecha)  AS dias_desde_la_anterior
FROM   falta_asistencia f
       JOIN matricula m ON m.id_matricula = f.id_matricula
WHERE  m.id_alumno = 9
ORDER  BY f.fecha;
FECHAHORASJUSTIFICADAFALTA_ANTERIORDIAS_DESDE_LA_ANTERIOR
25/02/20263N(null)(null)
11/03/20261S25/02/202614
29/04/20263N11/03/202649

3 filas

La primera fila tiene NULL porque no hay fila anterior. LAG(expr, n, valor_por_defecto) admite un desplazamiento y un valor para ese caso: LAG(f.fecha, 1, f.fecha) daría 0 días en la primera. LEAD hace lo mismo mirando hacia delante.

Función analítica útilPara qué
SUM(x) OVER (ORDER BY f)Acumulado (total a fecha)
COUNT(*) OVER (PARTITION BY g)Tamaño del grupo junto a cada fila
LAG / LEADComparar con el periodo anterior o siguiente
FIRST_VALUE / LAST_VALUEEl mejor y el peor de la partición
NTILE(4)Repartir en cuartiles
RATIO_TO_REPORT(x)Porcentaje de cada fila sobre el total de su partición

10. Optimización de consultas y plan de ejecución

10.1 Qué significa optimizar

El criterio RA3.h pide aplicar criterios de optimización. Optimizar no es escribir la consulta más corta: es conseguir que el SGBD lea menos datos para responder a la misma pregunta, y comprobarlo con mediciones.

En Oracle, quien decide cómo se ejecuta una consulta es el optimizador basado en costes (CBO). Tú escribes qué quieres (SQL es declarativo) y él elige el plan: qué tabla leer primero, si usar un índice y qué algoritmo de composición aplicar. Sus decisiones se basan en las estadísticas de las tablas (número de filas, valores distintos, distribución). Si las estadísticas están obsoletas, el plan puede ser malísimo.

10.2 Obtener el plan de ejecución

Oracle 26ai
-- Opción 1: EXPLAIN PLAN (no ejecuta la consulta, solo la analiza)
EXPLAIN PLAN FOR
SELECT a.cod_grupo, COUNT(*) AS matriculas
FROM   matricula m JOIN alumno a ON a.id_alumno = m.id_alumno
GROUP  BY a.cod_grupo;

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
-- Opción 2: AUTOTRACE en SQLcl o SQL*Plus (ejecuta y muestra plan + estadísticas reales)
SET AUTOTRACE ON EXPLAIN STATISTICS
SET TIMING ON
SELECT ... ;
SET AUTOTRACE OFF
HerramientaCómoQué da
SQL Developer / VS CodeExplicar plan (F10)Plan estimado, en árbol
SQL DeveloperAutotrace (F6)Plan real más lecturas y ordenaciones
SQLcl / SQL*PlusEXPLAIN PLAN + DBMS_XPLAN.DISPLAYPlan en texto, fácil de guardar como evidencia
CualquieraDBMS_XPLAN.DISPLAY_CURSORPlan realmente usado en la última ejecución

10.3 Leer un plan

Un plan tiene este aspecto (los valores exactos de Rows, Bytes, Cost y Time dependen de las estadísticas, de la versión y del equipo, por lo que los tuyos no serán idénticos):

-------------------------------------------------------------------------------
| Id | Operation            | Name      | Rows | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------
|  0 | SELECT STATEMENT     |           |    5 |    65 |     7  (15)| 00:00:01 |
|  1 |  HASH GROUP BY       |           |    5 |    65 |     7  (15)| 00:00:01 |
|* 2 |   HASH JOIN          |           |  143 |  1859 |     6   (0)| 00:00:01 |
|  3 |    TABLE ACCESS FULL | ALUMNO    |   32 |   288 |     3   (0)| 00:00:01 |
|  4 |    TABLE ACCESS FULL | MATRICULA |  143 |   572 |     3   (0)| 00:00:01 |
-------------------------------------------------------------------------------

Cómo se lee:

  • De dentro hacia fuera y de arriba abajo: primero las operaciones más indentadas (3 y 4), después la que las usa (2), y así hasta la 0.
  • Rows es la cardinalidad estimada: cuántas filas cree el optimizador que devolverá ese paso. Si la estimación se aleja mucho de la realidad, las estadísticas son malas y el plan será malo.
  • Cost es una estimación relativa del esfuerzo (lecturas de disco y CPU). Sirve para comparar dos planes de la misma consulta; no es un tiempo ni se puede comparar entre consultas distintas.

Operaciones que debes reconocer:

OperaciónQué haceCuándo es buena señal
TABLE ACCESS FULLLee toda la tablaTablas pequeñas, o cuando se necesita gran parte de las filas
INDEX UNIQUE SCANLocaliza una fila por un índice únicoBúsqueda por clave primaria: óptimo
INDEX RANGE SCANRecorre un tramo del índiceFiltro selectivo: casi siempre lo deseable
TABLE ACCESS BY INDEX ROWIDVa a la tabla a por las filas que señaló el índiceNormal tras un RANGE SCAN
INDEX FAST FULL SCANLee el índice entero en vez de la tablaCuando el índice ya tiene todas las columnas pedidas
NESTED LOOPSPara cada fila de A, busca en B (normalmente por índice)A devuelve pocas filas y B está indexada
HASH JOINConstruye una tabla hash con la tabla pequeña y recorre la grandeComposición de tablas grandes: lo normal y eficiente
MERGE JOINOrdena las dos entradas y las recorre en paraleloCuando ya vienen ordenadas
SORT ORDER BY / HASH GROUP BYOrdena / agrupaNormal; vigila si gasta mucha memoria

En EduGest, con 32 alumnos y 143 matrículas, lo correcto es que aparezca TABLE ACCESS FULL. Leer una tabla de 143 filas completa cuesta una sola lectura de bloques; usar un índice costaría más. Que el optimizador ignore tus índices en tablas minúsculas no es un fallo: es la decisión acertada. Para ver diferencias medibles hace falta volumen, y eso es lo que construye la práctica 7.6 con una tabla de 500 000 filas.

10.4 Índices y composiciones

Oracle crea índices automáticamente para PRIMARY KEY y UNIQUE, pero no para las claves ajenas. Como las claves ajenas son justamente las columnas por las que se compone, indexarlas es la primera medida de optimización de cualquier esquema. EduGest ya lo hace en el script 01:

CREATE INDEX ix_matricula_modulo ON matricula (id_modulo);
CREATE INDEX ix_alumno_grupo     ON alumno (cod_grupo);
CREATE INDEX ix_imparte_profesor ON imparte (id_profesor);
Conviene indexarNo conviene indexar
Claves ajenas usadas en composicionesTablas muy pequeñas
Columnas muy selectivas en el WHEREColumnas con pocos valores distintos (turno, justificada)
Columnas de ORDER BY sobre resultados grandesColumnas que se actualizan constantemente
Conjuntos de columnas de una consulta crítica (índice de cobertura)«Por si acaso»: cada índice ralentiza los INSERT

10.5 Cómo estropear un índice sin darse cuenta

Un índice sobre columna solo se puede usar si el WHERE compara la columna tal cual. Si le aplicas una función o una operación, el índice queda inservible porque Oracle tendría que evaluar la función en todas las filas para saber cuáles cumplen:

-- (a) NO puede usar un índice sobre fecha_matricula: hay una función encima
SELECT * FROM matricula WHERE TRUNC(fecha_matricula) = DATE '2025-09-15';

-- (b) SÍ puede: la columna aparece sin modificar, el rango es equivalente
SELECT * FROM matricula
WHERE  fecha_matricula >= DATE '2025-09-15'
AND    fecha_matricula <  DATE '2025-09-16';
-- (c) Conversión implícita: nia es CHAR(8) y se compara con un número
SELECT * FROM alumno WHERE nia = 10450037;    -- no usa el índice
SELECT * FROM alumno WHERE nia = '10450037';  -- sí lo usa

Otros destructores de índices habituales:

PatrónPor qué no usa el índiceAlternativa
UPPER(email) = 'A@B.ES'Función sobre la columnaÍndice basado en función: CREATE INDEX … ON alumno (UPPER(email))
LIKE '%web%'El comodín inicial impide recorrer el árbolBúsqueda de texto (Oracle Text) o replantear el dato
nota_final + 1 > 6Operación sobre la columnanota_final > 5
cod_grupo <> '1DAM'Poco selectivo: devuelve casi todoSuele ser correcto el acceso completo
WHERE col IS NULLLos índices B*Tree no indexan filas con todas las columnas nulasÍndice compuesto o basado en función

Que una condición pueda usar un índice se llama que sea «sargable». La regla práctica es: deja la columna sola a un lado del operador y mueve todos los cálculos al otro lado.

10.6 Estadísticas

-- Recalcular las estadísticas de una tabla (necesario tras una carga masiva)
EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'MATRICULA');

-- Todo el esquema
EXEC DBMS_STATS.GATHER_SCHEMA_STATS(USER);

-- Comprobar cuándo se recogieron y qué cree Oracle que hay en la tabla
SELECT table_name, num_rows, blocks, last_analyzed
FROM   user_tables
ORDER  BY table_name;

Oracle recoge estadísticas automáticamente en una ventana de mantenimiento nocturna, pero después de cargar o borrar muchas filas hay que recogerlas a mano: si el optimizador cree que la tabla tiene 0 filas y en realidad tiene medio millón, elegirá un NESTED LOOPS catastrófico.

10.7 Antes y después: un ejemplo medible

Este es el experimento de la práctica 7.6, resumido. Sobre una tabla ACCESO_PORTAL con 500 000 filas generadas, la consulta SELECT COUNT(*) FROM acceso_portal WHERE id_alumno = 1234 devuelve 100 filas, es decir, el 0,02 % de la tabla: un filtro muy selectivo.

SituaciónOperación del planBloques leídosInterpretación
Sin índiceTABLE ACCESS FULL ACCESO_PORTALToda la tablaLee 500 000 filas para quedarse con 100
Con índice ix_acceso_alumno (id_alumno)INDEX RANGE SCAN ix_acceso_alumnoUnos pocos bloques del índiceSolo recorre el tramo del índice; como se pide COUNT(*), ni visita la tabla

El coste estimado baja en dos órdenes de magnitud, y el tiempo también. Pero el mismo índice no ayuda en WHERE seccion = 'NOTAS', que devuelve el 20 % de las filas: ahí el optimizador prefiere el acceso completo, y hace bien, porque ir y volver al índice 100 000 veces es más caro que leer la tabla de una pasada.

Mide siempre dos o tres veces cada consulta y quédate con las últimas: la primera ejecución carga los bloques en la caché de Oracle y es artificialmente lenta. Y recuerda que Cost es una estimación, no un tiempo: la prueba definitiva es el tiempo de respuesta y el número de bloques leídos (SET AUTOTRACE ON STATISTICS).

Gráfico: lecturas de bloque con y sin índice

Bloques leídos para localizar una fila (modelo simplificado)
Pasa el ratón o pulsa una barra para ver su valor.
Modelo: 100 filas por bloque y un B-tree con unas 200 claves por nodo. Pulsa el botón para cambiar a escala logarítmica.

Probar el modelo con otros tamaños:

Laboratorio · ¿Cuánto cuesta buscar una fila?
Recorrido completo (sin índice)
Búsqueda con índice B-tree

Modelo simplificado: 100 filas por bloque, un B-tree con ~200 claves por nodo y acceso a una sola fila por clave única. La barra usa escala logarítmica.

10.8 Lista de comprobación antes de dar por buena una consulta

  • Devuelve el número de filas que había predicho.
  • Todas las tablas del FROM tienen su condición de emparejamiento: ningún producto cartesiano involuntario.
  • Las composiciones externas no están anuladas por un filtro en el WHERE.
  • Los COUNT y SUM sobre tablas opcionales usan una columna de esa tabla y están protegidos con NVL si procede.
  • Solo se piden las columnas necesarias; nada de SELECT * en un informe.
  • Los filtros se aplican lo antes posible (en el WHERE, no en el HAVING).
  • Las condiciones del WHERE dejan la columna sola: no hay funciones ni conversiones implícitas sobre columnas indexadas.
  • UNION ALL en lugar de UNION cuando no puede haber duplicados.
  • Hay índices sobre las claves ajenas que participan en las composiciones.
  • Se ha mirado el plan de ejecución y se entiende cada operación.
  • La consulta lleva un comentario con la pregunta que responde y el número de filas esperado.

11. Errores frecuentes

SíntomaCausaSolución
ORA-00979: not a GROUP BY expressionUna columna del SELECT no está en el GROUP BY ni es un agregadoAñádela al GROUP BY, quítala del SELECT o resúmela con MIN, MAX o LISTAGG
ORA-00937: not a single-group group functionSe mezcla una columna normal con un agregado sin GROUP BYAñade el GROUP BY o elimina la columna
ORA-00934: group function is not allowed hereUn agregado en el WHERE, en el GROUP BY o en una restricciónLas condiciones sobre agregados van en el HAVING
ORA-00918: column ambiguously definedUna columna con el mismo nombre en dos tablas compuestas, sin cualificarUsa alias de tabla y cualifica todas las columnas
ORA-00904: invalid identifierAlias del SELECT usado en el WHERE o el HAVING, o columna mal escritaRepite la expresión, o dale nombre con una cláusula WITH
ORA-01427: single-row subquery returns more than one rowSubconsulta comparada con =, >… que devuelve varias filasResume con AVG/MAX, o usa IN, ANY o ALL
ORA-00933: SQL command not properly endedORDER BY en el medio de un UNION, o cláusulas desordenadasEl ORDER BY va una sola vez, al final del conjunto
ORA-01789: query block has incorrect number of result columnsLas consultas de un UNION/MINUS no tienen el mismo número de columnasIguala el número y el tipo de columnas, en el mismo orden
ORA-25154: column part of USING clause cannot have qualifierSe ha cualificado la columna común de un USINGEscríbela sin alias, o cambia a JOIN … ON
ORA-01732: data manipulation operation not legal on this viewUPDATE/INSERT sobre una vista con GROUP BY, DISTINCT o agregadosModifica la tabla base, o rediseña la vista
La consulta devuelve muchísimas más filas de lo esperadoFalta una condición de composición (producto cartesiano)Comprueba que hay N−1 condiciones ON para N tablas
Una composición externa «no funciona»Un filtro en el WHERE sobre la tabla opcional la ha convertido en internaMueve la condición al ON
Un COUNT vale 1 donde debería valer 0COUNT(*) en un LEFT JOIN cuenta la fila sin parejaCuenta una columna de la tabla opcional
NOT IN no devuelve nada y no da errorLa subconsulta contiene un NULLUsa NOT EXISTS o añade IS NOT NULL a la subconsulta

12. Buenas prácticas

  • Escribe la pregunta antes que el SQL. Una consulta sin una pregunta clara detrás no se puede comprobar ni revisar.
  • Predice el número de filas antes de ejecutar y compáralo después. Es la comprobación más rentable que existe.
  • Construye las composiciones de una en una, verificando el recuento en cada paso.
  • Usa siempre JOIN … ON, nunca la sintaxis con comas ni (+), y nunca NATURAL JOIN.
  • Alias de tabla cortos y significativos, y todas las columnas cualificadas.
  • En el ON, lo que empareja; en el WHERE, lo que filtra. En composiciones externas esta separación no es estilo: cambia el resultado.
  • Filtra lo antes posible: en el WHERE, no en el HAVING; y no filtres en la aplicación lo que puede filtrar el SGBD.
  • Cuidado con los nulos en los agregados. Decide y documenta si un no presentado cuenta como cero o no cuenta.
  • Prefiere NOT EXISTS a NOT IN cuando la subconsulta pueda devolver nulos.
  • Da nombre a los pasos intermedios con WITH en cuanto la consulta pase de dos niveles de anidamiento.
  • Encapsula en vistas los informes recurrentes, con WITH READ ONLY y un comentario que explique su finalidad.
  • Nombra las columnas calculadas con alias comprensibles: es lo que verá quien lea el informe.
  • Mira el plan de ejecución de cualquier consulta que vaya a ejecutarse de forma habitual, y guárdalo como evidencia cuando optimices.
  • Comenta las consultas que entregues: qué responden, qué decisiones has tomado y cuántas filas devuelven.

Repasa los términos de la UD07 Pulsa una tarjeta para girarla

13. Resumen

ConceptoIdea claveSintaxis esencial
Funciones de agregadoConvierten muchas filas en un valor; ignoran los nulos salvo COUNT(*)COUNT, SUM, AVG, MIN, MAX
GROUP BYUna fila por grupo; en el SELECT, solo lo agrupado o agregadoGROUP BY col1, col2
HAVINGFiltra grupos; WHERE filtra filasHAVING COUNT(*) >= 3
Orden lógicoFROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY—
Producto cartesianoTodas las combinaciones; involuntario = error, deliberado = rejillaCROSS JOIN
Composición internaSolo las filas que emparejanJOIN t ON cond
Composición externaConserva las filas sin pareja, con NULLLEFT / RIGHT / FULL OUTER JOIN
AnticomposiciónBuscar lo que no existeLEFT JOIN … WHERE t.pk IS NULL
AutocomposiciónUna tabla consigo misma, con dos aliasFROM profesor p JOIN profesor j ON …
SubconsultaUn SELECT dentro de otro; de una fila, de varias o correlacionadaIN, ANY, ALL, EXISTS
WITH (CTE)Da nombre a los pasos intermedios y mejora la legibilidadWITH paso AS (…) SELECT …
VistasConsulta guardada; reutilización, simplicidad y seguridadCREATE OR REPLACE VIEW … WITH READ ONLY
Operadores de conjuntosCombinan resultados a lo altoUNION, UNION ALL, INTERSECT, MINUS
Funciones analíticasResumen sin perder el detalleAVG(x) OVER (PARTITION BY g ORDER BY f)
OptimizaciónLeer menos datos; medirlo con el planEXPLAIN PLAN FOR + DBMS_XPLAN.DISPLAY

Ideas que conviene llevarse grabadas:

  1. Si una consulta devuelve más filas de las esperadas, falta una condición de composición.
  2. Si devuelve menos, sobra un filtro o falta una composición externa.
  3. COUNT(*) y COUNT(columna) no son lo mismo, y en un LEFT JOIN la diferencia es justo lo que quieres.
  4. NULL sigue siendo la causa de la mitad de las sorpresas: en AVG, en NOT IN y en las composiciones externas.

14. Autoevaluación

Autoevaluación 10 preguntas
1 MATRICULA tiene 143 filas, 6 con nota_final nula. ¿Qué pareja de valores devuelve SELECT AVG(nota_final), SUM(nota_final)/COUNT(*) FROM matricula?
2 ¿Cuál de estas consultas es correcta en Oracle?
3 CICLO tiene 4 filas y GRUPO 6, y los 6 grupos pertenecen a 3 de los ciclos. ¿Cuántas filas devuelve SELECT * FROM ciclo c LEFT JOIN grupo g ON g.cod_ciclo = c.cod_ciclo?
4 ¿Por qué SELECT g.cod_grupo, p.apellidos FROM grupo g LEFT JOIN profesor p ON p.id_profesor = g.id_tutor WHERE p.id_departamento = 1 pierde el grupo sin tutor?
5 En GROUP BY g.cod_grupo sobre un GRUPO LEFT JOIN ALUMNO, ¿qué devuelve COUNT(*) para un grupo sin alumnos?
6 ¿Qué hace falta para escribir una consulta que empareje cada profesor con el jefe de su departamento?
7 ¿Cuándo es preferible EXISTS a un JOIN?
8 ¿Qué diferencia hay entre UNION y UNION ALL?
9 ¿Cuál es la diferencia entre RANK() y DENSE_RANK() cuando dos filas empatan en el puesto 2?
10 Una consulta sobre una tabla de 500 000 filas usa WHERE TRUNC(fecha_hora) = DATE '2024-03-15' y el plan muestra TABLE ACCESS FULL pese a existir un índice sobre fecha_hora. ¿Por qué?

Referencias