SQL para TAI: 8 ejercicios resueltos de JOIN, NULL y GROUP BY
Practica SQL para TAI con dos tablas, ocho ejercicios ejecutables y sus resultados: INNER JOIN, LEFT JOIN, COUNT, HAVING y la trampa de NOT IN con NULL.
Para practicar SQL en la oposición de TAI, una buena prueba es predecir las filas que devolverá una consulta antes de ejecutarla. Si solo reconoces la sintaxis, puedes pasar por alto una duplicación, un valor NULL o un filtro que elimina justo la fila que querías conservar.
Aquí tienes ocho ejercicios originales con dos tablas pequeñas y resultados comprobados. Primero crea los datos; después resuelve cada caso con las soluciones cerradas. Las personas, departamentos y puntuaciones son ficticios. No representan notas de opositores ni datos de ninguna Administración.
El bloque III del programa de ingreso libre TAI del Anexo V de la Resolución de 18 de diciembre de 2025 incluye lenguajes de interrogación de bases de datos y estándar ANSI SQL. Esta pieza desarrolla una parte de esa materia; no anuncia el programa ni el formato de una convocatoria futura.
Prepara la base de datos del ejercicio
Utiliza una base de datos de prácticas vacía. El script crea dos tablas con nombres en inglés para que puedas copiar las consultas sin cambiar identificadores. No lo ejecutes sobre una base de datos de trabajo que ya contenga tablas con esos nombres.
CREATE TABLE departments (
id INTEGER PRIMARY KEY,
name VARCHAR(40) NOT NULL
);
CREATE TABLE people (
id INTEGER PRIMARY KEY,
name VARCHAR(40) NOT NULL,
department_id INTEGER REFERENCES departments(id),
score INTEGER
);
INSERT INTO departments (id, name) VALUES
(10, 'Desarrollo'),
(20, 'Sistemas'),
(30, 'Soporte');
INSERT INTO people (id, name, department_id, score) VALUES
(1, 'Ana', 10, 70),
(2, 'Bruno', 10, 80),
(3, 'Carla', 20, 90),
(4, 'Diego', NULL, 60),
(5, 'Elena', 20, NULL);
La tabla departments contiene tres departamentos. people tiene cinco personas: dos en Desarrollo, dos en Sistemas y una sin departamento asignado. Además, una de las personas no tiene puntuación. Son dos ausencias diferentes: Diego carece de departamento; Elena carece de puntuación.
| id | Persona | Departamento | Puntuación |
|---|---|---|---|
| 1 | Ana | 10 | 70 |
| 2 | Bruno | 10 | 80 |
| 3 | Carla | 20 | 90 |
| 4 | Diego | NULL | 60 |
| 5 | Elena | 20 | NULL |
Las consultas y sus salidas se han comprobado ejecutándolas en SQLite. Utilizan construcciones comunes de SQL; las referencias técnicas enlazadas son la documentación de PostgreSQL. No se ha verificado aquí la ejecución en todos los gestores ni se presupone que un examen utilice uno concreto. Si una pregunta especifica motor o versión, esa condición forma parte del enunciado.
Método: dibuja la tabla intermedia
Antes de ejecutar, escribe qué filas esperas tras la combinación y cuáles sobreviven al filtro. Después calcula grupos y agregados, si los hay. Esta es una forma de razonar el resultado, no una descripción del plan físico que elegirá un optimizador.
Para cada ejercicio conserva tres cosas: tu predicción, la salida real y una frase que explique la diferencia si has fallado. No empieces cambiando al azar la consulta hasta que aparezca el resultado deseado. Primero localiza si el error está en la relación, en el filtro, en el recuento o en el tratamiento de NULL.
Ejercicio 1. Personas con departamento: INNER JOIN
Reto: muestra el nombre de cada persona y el de su departamento, solo cuando exista coincidencia. Ordena por el identificador de la persona. Predice si aparecerán cuatro o cinco filas y explica qué ocurrirá con Diego y Elena.
Ver consulta, resultado y razonamiento del ejercicio 1
SELECT p.name AS person, d.name AS department
FROM people AS p
INNER JOIN departments AS d
ON d.id = p.department_id
ORDER BY p.id;
| person | department |
|---|---|
| Ana | Desarrollo |
| Bruno | Desarrollo |
| Carla | Sistemas |
| Elena | Sistemas |
Salen cuatro filas. Diego no tiene coincidencia por department_id. Elena sí la tiene: que su puntuación sea NULL no afecta a una condición que compara el departamento.
La comprobación importante es leer las columnas del ON. Si has excluido a Elena por tener un dato ausente, has aplicado una condición que la consulta no contiene. Si has incluido a Diego, has atribuido a esta combinación el comportamiento de otra distinta. Consulta la referencia de tablas combinadas de PostgreSQL para revisar la semántica de los JOIN.
Ejercicio 2. Conservar a todas las personas: LEFT JOIN
Reto: cambia la consulta para que aparezcan las cinco personas, aunque alguna no tenga departamento. ¿Qué tabla debe quedar a la izquierda? No basta con cambiar una palabra si también inviertes las tablas.
Ver consulta, resultado y razonamiento del ejercicio 2
SELECT p.name AS person, d.name AS department
FROM people AS p
LEFT JOIN departments AS d
ON d.id = p.department_id
ORDER BY p.id;
| person | department |
|---|---|
| Ana | Desarrollo |
| Bruno | Desarrollo |
| Carla | Sistemas |
| Diego | NULL |
| Elena | Sistemas |
Ahora aparece Diego, con NULL en la columna del departamento. Hemos partido de people, porque el objetivo era conservar a todas las personas. Si partes de los departamentos y conservas todas sus filas, respondes a otra pregunta: esa diferencia será importante en el siguiente ejercicio.
En este caso cada persona solo puede enlazar con un departamento porque departments.id es clave primaria. No generalices «LEFT JOIN devuelve exactamente tantas filas como la tabla izquierda»: una fila izquierda puede tener varias coincidencias y generar varias filas. Aquí la estructura de los datos permite el resultado de cinco.
Ejercicio 3. Departamento vacío: COUNT(*) frente a COUNT(p.id)
Reto: cuenta personas por departamento, incluyendo Soporte aunque esté vacío. Añade dos columnas de recuento: una con COUNT(*) y otra con COUNT(p.id). Predice las dos cifras para Soporte.
Ver consulta, resultado y razonamiento del ejercicio 3
SELECT d.id, d.name AS department,
COUNT(*) AS joined_rows,
COUNT(p.id) AS people_count
FROM departments AS d
LEFT JOIN people AS p
ON p.department_id = d.id
GROUP BY d.id, d.name
ORDER BY d.id;
| id | department | joined_rows | people_count |
|---|---|---|---|
| 10 | Desarrollo | 2 | 2 |
| 20 | Sistemas | 2 | 2 |
| 30 | Soporte | 1 | 0 |
Para Soporte hay una fila producida por el LEFT JOIN, pero su columna p.id es NULL. De ahí la diferencia entre una fila y cero personas enlazadas. Como people.id es una clave primaria, un identificador no nulo permite reconocer una persona real en este conjunto.
La documentación de funciones agregadas distingue el recuento de filas del recuento de valores no nulos de una expresión. Si has obtenido mentalmente un 1 como número de personas, dibuja la fila de Soporte antes de agrupar: verás qué está contando cada expresión.
Ejercicio 4. Un filtro en WHERE
Reto: predice esta salida. Queremos estudiar qué ocurre, no dar por supuesto que la consulta conserva todos los departamentos por contener LEFT JOIN.
SELECT d.id, p.name AS person, p.score
FROM departments AS d
LEFT JOIN people AS p
ON p.department_id = d.id
WHERE p.score >= 80
ORDER BY d.id, p.id;
Ver resultado y razonamiento del ejercicio 4
| id | person | score |
|---|---|---|
| 10 | Bruno | 80 |
| 20 | Carla | 90 |
Quedan dos filas. Ana tiene 70; Elena tiene puntuación NULL; la fila sin coincidencia de Soporte también presenta NULL en las columnas de people. Ninguna de esas filas satisface el filtro indicado. Diego no se incorpora por tener puntuación 60: no pertenece a ninguno de los departamentos de partida.
El error sería reconocer el tipo de JOIN y dejar de leer. Para resolver el ejercicio necesitas aplicar también el WHERE al resultado intermedio. No se está afirmando que todo filtro posterior convierta cualquier combinación en otra: el efecto depende de la condición concreta que escribas.
Ejercicio 5. El mismo filtro en ON
Reto: coloca la condición de puntuación en el ON, manteniendo la relación por departamento. ¿Aparece ahora Soporte? Compara el resultado con el anterior, no solo la forma de escribir la consulta.
Ver consulta, resultado y razonamiento del ejercicio 5
SELECT d.id, p.name AS person, p.score
FROM departments AS d
LEFT JOIN people AS p
ON p.department_id = d.id
AND p.score >= 80
ORDER BY d.id, p.id;
| id | person | score |
|---|---|---|
| 10 | Bruno | 80 |
| 20 | Carla | 90 |
| 30 | NULL | NULL |
Hay tres filas. Soporte se conserva como departamento sin una persona coincidente que cumpla las condiciones. En el ejercicio 4, su fila se eliminaba al aplicar el filtro posterior. En este ejercicio se está definiendo qué personas coinciden, sin añadir ese WHERE.
Utiliza los dos resultados como contraejemplo propio cuando alguien afirme que mover condiciones entre ON y WHERE nunca cambia una consulta. La referencia de expresiones de tabla permite contrastar esa diferencia. El resultado concreto de estas dos consultas está comprobado sobre los datos de la guía.
Ejercicio 6. Seleccionar grupos con HAVING
Reto: devuelve únicamente los departamentos con al menos dos personas. Debes conservar el recuento de personas del ejercicio 3, sin usar la puntuación como condición. ¿En qué parte sitúas la condición sobre el agregado?
Ver consulta, resultado y razonamiento del ejercicio 6
SELECT d.id, d.name AS department,
COUNT(p.id) AS people_count
FROM departments AS d
LEFT JOIN people AS p
ON p.department_id = d.id
GROUP BY d.id, d.name
HAVING COUNT(p.id) >= 2
ORDER BY d.id;
| id | department | people_count |
|---|---|---|
| 10 | Desarrollo | 2 |
| 20 | Sistemas | 2 |
Sistemas cuenta a Carla y Elena aunque Elena no tenga puntuación: estamos contando identificadores de personas, no puntuaciones disponibles. Soporte queda fuera porque su recuento es cero.
Este caso permite detectar dos errores diferentes. Si has eliminado a Elena antes de contar, has añadido un filtro que no se pedía. Si has intentado resolver el recuento del grupo como una condición sobre una sola fila original, revisa la distinción entre filtrar filas y seleccionar grupos. En la consulta hemos incluido las dos columnas no agregadas en GROUP BY para expresar claramente la agrupación.
Ejercicio 7. Buscar una ausencia concreta: IS NULL
Reto: encuentra las personas sin departamento. No te piden las personas sin puntuación. Predice qué devuelve cada una de las siguientes consultas.
SELECT name
FROM people
WHERE department_id IS NULL
ORDER BY id;
SELECT name
FROM people
WHERE department_id = NULL
ORDER BY id;
Ver resultados y razonamiento del ejercicio 7
La primera devuelve Diego. La segunda no devuelve filas. Si sustituyeras la columna por score IS NULL, el resultado sería Elena: de nuevo, no todas las ausencias significan lo mismo.
La documentación de predicados de comparación explica el tratamiento de NULL y los predicados IS NULL e IS NOT NULL. Para esta práctica, no lo interpretes como el texto «NULL» ni como el número cero. La consulta incorrecta utiliza una comparación que no resulta verdadera para seleccionar a Diego.
Al corregir, separa dos fallos posibles: elegir la columna equivocada y usar el operador equivocado. Si marcas ambos solo como «problema con NULL», será difícil saber qué debes repasar en la siguiente sesión.
Ejercicio 8. NOT IN y una subconsulta con NULL
Reto: queremos los departamentos sin personas. Predice primero esta consulta y comprueba qué valores devuelve su subconsulta.
SELECT d.id, d.name
FROM departments AS d
WHERE d.id NOT IN (
SELECT p.department_id
FROM people AS p
)
ORDER BY d.id;
Ver resultado, alternativa y razonamiento del ejercicio 8
La consulta no devuelve filas. La subconsulta incluye NULL por la fila de Diego. Para el departamento 30, que no coincide con los identificadores 10 o 20, la presencia de ese valor impide obtener el verdadero que se necesitaría para conservarlo con esa expresión.
Una forma de expresar la ausencia de personas relacionadas es esta:
SELECT d.id, d.name
FROM departments AS d
WHERE NOT EXISTS (
SELECT 1
FROM people AS p
WHERE p.department_id = d.id
)
ORDER BY d.id;
| id | name |
|---|---|
| 30 | Soporte |
Ahora sí se devuelve Soporte. La consulta comprueba si existe una persona relacionada con cada departamento. No necesita interpretar toda la lista anterior como si todos sus elementos fueran identificadores conocidos.
También podrías estudiar una versión de NOT IN que excluyera los nulos de su subconsulta. Lo importante no es memorizar «NOT IN está prohibido»: debes reconocer bajo qué condiciones aparece el problema. La documentación de expresiones de subconsulta desarrolla ese comportamiento. En este caso, el contraejemplo depende de un dato concreto: la fila de Diego.
Tabla final de resultados comprobados
| Ejercicio | Resultado que debes explicar |
|---|---|
| 1. INNER JOIN | Cuatro personas; Diego no coincide |
| 2. LEFT JOIN desde people | Cinco personas; Diego con departamento NULL |
| 3. Recuentos por departamento | Soporte tiene COUNT(*) = 1 y COUNT(p.id) = 0 |
| 4. Filtro en WHERE | Bruno y Carla |
| 5. Filtro en ON | Bruno, Carla y una fila de Soporte sin persona |
| 6. HAVING | Desarrollo y Sistemas, dos personas cada uno |
| 7. IS NULL frente a = NULL | Diego frente a ninguna fila |
| 8. NOT IN frente a NOT EXISTS | Ninguna fila frente a Soporte |
Los ORDER BY hacen que las salidas se puedan comparar en el orden mostrado. No deduzcas que una consulta sin ordenación explícita garantiza siempre ese mismo orden. Tampoco confundas el número de filas con el número de personas: el ejercicio 3 demuestra por qué hay que mirar qué representa cada fila.
Cómo continuar estudiando SQL para TAI
Repite la práctica cambiando un solo dato: añade una persona en Soporte, asigna un departamento a Diego o da puntuación a Elena. Antes de ejecutar, identifica qué ejercicios deberían cambiar y cuáles no. Si alteras toda la tabla a la vez, será más difícil atribuir cada diferencia a su causa.
Esta guía es una pieza de apoyo de bases de datos. Para organizar el resto, consulta los recursos de temario TAI. Si necesitas la materia desarrollada, revisa el alcance del Pack Temario TAI 2027: es un producto de pago y su ficha especifica los bloques y los materiales incluidos. El nombre de la edición no sustituye la convocatoria oficial aplicable.
Guarda tus resultados y clasifica los fallos por combinación, filtro, agregado o nulos. Termina la sesión explicando uno de los contraejemplos sin mirar el código. Si puedes justificar por qué Soporte aparece en una consulta y desaparece en otra, habrás practicado una habilidad que se transfiere a preguntas con datos distintos.
Preguntas frecuentes
¿Estos ejercicios SQL son de un examen oficial de TAI?
No. Son ejercicios originales de entrenamiento. El programa del Anexo V de la Resolución de 18 de diciembre de 2025 incluye lenguajes de interrogación de bases de datos y estándar ANSI SQL; esta práctica trabaja una parte de esa materia.
¿COUNT(*) y COUNT(columna) dan siempre lo mismo?
No. En el conjunto de datos de esta guía, el departamento sin personas produce una fila al usar LEFT JOIN: COUNT(*) devuelve 1 y COUNT(p.id) devuelve 0. El tercer ejercicio permite comprobar la diferencia.
¿Por qué un filtro WHERE puede eliminar filas de un LEFT JOIN?
Porque el filtro se aplica sobre el resultado del JOIN. Si exige una condición de la tabla derecha que no cumplen sus valores NULL, elimina esas filas. Los ejercicios 4 y 5 comparan el mismo filtro en WHERE y en ON.
¿Qué problema tiene NOT IN cuando la subconsulta devuelve NULL?
Puede producir un resultado desconocido en lugar de verdadero para valores sin coincidencia. En el ejemplo de esta guía, NOT IN no devuelve el departamento vacío; NOT EXISTS expresa correctamente la búsqueda y sí lo devuelve.
Artículos relacionados
Subredes IPv4 para TAI: 8 ejercicios CIDR resueltos
Calcula red, broadcast, hosts y prefijos con ocho ejercicios IPv4 para TAI. Incluye división de subredes, agregación y un caso VLSM con resultados comprobados.
Condiciones booleanas TAI: 12 ejercicios de lógica
Practica tablas de verdad, AND, OR, XOR, De Morgan y condiciones de programación con doce ejercicios originales para TAI y soluciones comprobadas.
Permisos Linux para TAI: 10 ejercicios de chmod y umask
Resuelve diez ejercicios de permisos Linux para TAI: modos octales, chmod, umask, directorios y borrado. Soluciones comprobadas en un sistema Linux.