Recuperar información de varias tablas en MySQL

Las instrucciones creadas hasta el momento obtienen datos de una base de datos. Pero MySQL puede esforzarse más. Ya hemos mencionado que la capacidad de un DBMS relacional radica en su capacidad para combinar información de varias tablas para responder a preguntas que no se pueden solucionar con tablas individuales. En este tutorial veremos cómo escribir instrucciones para ello.

Un tipo de operación que selecciona información de varias tablas son las combinaciones, que generan un resultado al combinar información de una tabla con la de otra. Para ello, se comparan los valores comunes de las tablas. Otro tipo de operación con varias tablas usa una instrucción SELECT anidada dentro de otra. La instrucción anidada se denomina subconsulta.
Veamos un ejemplo de combinación. En un tutorial anterior, presentamos una consulta para recuperar puntuaciones de notas sin apenas explicarla. La consulta implica una combinación triple, y la crearemos en dos pasos. En primer lugar, creamos una consulta para seleccionar notas de una fecha concreta:

mysql> SELECT student_id, date, score, category
-> FROM grade_event INNER JOIN score
-> ON grade_event.event_id = score.event_id
-> WHERE date = '2008-09-23';

student_id date score category
1 2008-09-23 15 Q
2 2008-09-23 12 Q
3 2008-09-23 11 Q
5 2008-09-23 13 Q
6 2008-09-23 18 Q

La consulta busca la fila grade_event con la fecha proporcionada (‘2008-09-23’) y utiliza el ID de evento de dicha fila para localizar puntuaciones con el mismo ID. Por cada combinación de grade_event y score que coincida, muestra el ID de alumno, la nota, la fecha y la categoría del evento.
La consulta se diferencia de las anteriores en dos aspectos principales:

  • La cláusula FROM hace referencia a más de una tabla ya que recuperamos datos de más de una tabla:
    FROM grade_event INNER JOIN score
  • La cláusula ON especifica que las tablas grade_event y score se combinan mediante la comparación de valores event_id en ambas tablas:
    ON grade_event.event_id = score.event_id

Hacemos referencia a las columnas event_id como grade_event.event_id y score.event_id con la sintaxis nombre_tabla.nombre_columna, ya que event_id aparece en ambas tablas. El resto de columnas de la consulta (date, score y category) se pueden usar sin un calificador de tabla ya que solamente aparecen en una de las tablas y son inequívocas.
Por lo general, resulta preferible que califiquemos todas las columnas de una combinación para saber a qué tabla pertenece cada una de las columnas. En su forma calificada, la consulta es la siguiente:

SELECT score.student_id, grade_event.date, score.score, grade_event.category
FROM grade_event INNER JOIN score
ON grade_event.event_id = score.event_id
WHERE grade_event.date = ‘2008-09-23’;

La primera consulta utiliza la tabla grade_event para asignar una fecha a un ID de evento, que posteriormente utiliza para buscar las calificaciones que coincidan en la tabla score. El resultado de la consulta contiene valores student_id pero los nombres tendrían más sentido. Al utilizar la tabla student, podemos asignar ID de alumnos a nombres, el segundo paso. Para mostrar los nombres, recuerde que las tablas score y student tienen columnas student_id que permiten vincular sus filas. La consulta restante es la siguiente:

mysql> SELECT
-> student.name, grade_event.date, score.score, grade_event.category
-> FROM grade_event INNER JOIN score INNER JOIN student
-> ON grade_event.event_id = score.event_id
-> AND score.student_id = student.student_id
-> WHERE grade_event.date = '2008-09-23';

name date score category
Megan 2008-09-23 15 Q
Joseph 2008-09-23 12 Q
Kylr 2008-09-23 11 Q
Abby 2008-09-23 13 Q
Nathan 2008-09-23 18 Q
...

Esta consulta presenta varias diferencias con respecto a la anterior según podemos ver a continuación:

  • La cláusula FROM incluye la tabla student ya que la instrucción la utiliza además de las tablas grade_event y score
  • La columna student_id era inequívoca, por lo que podemos hacer referencia a la misma de forma cualificada (student_id) o sin cualificar (score.student_id).
    Ahora ya no es inequívoca ya que aparece en las tablas score y student. Por ello, es necesario cualificarla como score.student_id o student.student_id para indicar qué tabla se utiliza.
  • La cláusula ON especifica que las filas de la tabla score se comparan con la tabla student en función del ID del alumno:
    ON ... score.student_id = student.student_id
  • La consulta muestra el nombre del alumno y no el ID. Para mostrar los dos, basta añadir student.student_id a la lista de columnas.

Con esta consulta, puede introducir cualquier fecha y recuperar las calificaciones de dicha fecha, junto con el nombre de los alumnos y la categoría de la calificación. No es necesario conocer el ID de alumno ni de evento. MySQL se encarga de determinar los correspondientes valores de ID y de utilizarlos para comparar las filas.
Otra de las tareas que componen nuestro proyecto de calificaciones consiste en resumir las faltas de asistencia de los alumnos, que se registran por identificador (ID) de alumno y por fecha dentro de la tabla absence. Para obtener nombres de alumnos (no solamente sus ID), tendremos que combinar la tabla absence a la tabla student, en función del valor student_id. La siguiente consulta enumera el ID y nombre del alumno junto al número de faltas de asistencia:

mysql> SELECT student.student_id, student.name,
-> COUNT(absence.date) AS absences
-> FROM student INNER JOIN absence
-> ON student.student_id = absence.student_id
-> GROUP BY student.student_id;

student_id name absences
3 Kyle 1
5 Abby 1
10 Peter 2
17 Will 1
20 Avery 1

Nota: Aunque la columna GROUP BY tiene un cualificador, no es estrictamente necesario. Hace referencia a columnas de resultados y sólo hay una de éstas con el nombre student_id, por lo que MySQL sabe a qué hacemos referencia.

El resultado generado por la consulta es correcto si solamente queremos saber los alumnos ausentes. Pero puede que nos pregunten por el resto de alumnos, por lo que necesitamos un valor para cada alumno, conocer el número de faltas de asistencia incluso para los alumnos que no han faltado a clase. Como el objetivo es diferente, la consulta también será diferente.
Para resolver esta pregunta, utilizaremos LEFT JOIN en lugar de una combinación interna. LEFT JOIN indica a MySQL que genere una fila de resultados por cada fila seleccionada de la tabla que aparece primero en la combinación (es decir, la tabla indicada a la izquierda de las palabras clave LEFT JOIN). Al incluir primero la tabla student, obtendremos resultados para todos los alumnos, incluso lo que no están en la tabla absence. Para crear esta consulta, utilice LEFT JOIN entre las tablas mencionadas en la cláusula FROM (en lugar de separarlas por coma) y una cláusula ON que indique cómo comparar las filas en ambas tablas:

mysql> SELECT student.student_id, student.name
-> COUNT(absence.date) AS absences
-> FROM student LEFT JOIN absence
-> ON student.student_id = absence.student_id
-> GROUP BY student.student_id;

student_id name absences
3 Megan 0
2 Joseph 0
3 Kyle 1
4 Katie 0
5 Abby 1
6 Nathan 0
7 Liesl 0

En un tutorial anterior, ejecutamos una consulta que generaba la caracterización numérica de los datos en la tabala score. El resultado de la consulta enumeraba ID de eventos pero no incluía fechas ni categorías ya que desconocíamos cómo combinar la tabla score con la tabla grade_event para asignar los ID a fechas y categorías. Ahora ya lo sabemos. La siguiente consulta es similar a la anterior, pero muestra las fechas y categorías en lugar de simples ID numéricos:

mysql> SELECT
-> grade_event.date, grade_event.category,
-> MIN(score.score) AS minimum,
-> MAX(score.score) AS maximum,
-> MAX(score.score) - MIN(score.score)+1 AS span,
-> SUM(score.score) AS total,
-> AVG(score.score) AS average,
-> COUNT(score.score) AS count
-> FROM score INNER JOIN grade_event
-> ON score.event_id = grade_event.event_id
-> GROUP BY grade_event.date;

date category minimum maximum span total average count
2008-09-03 Q 9 20 12 439 15.1379 29
2008-09-06 Q 8 19 12 425 14.1667 30
2008-09-09 T 60 97 38 2425 78.2258 31
2008-09-16 Q 7 20 14 379 14.0370 27
2008-09-23 Q 8 20 13 383 14.1852 27
2008-10-01 T 62 100 39 2325 80.1724 29

Puede utilizar funciones como COUNT() y AVG() para generar un resumen de varias columnas, aunque provengan de tablas distintas. La siguiente consulta determina el número de calificaciones y la nota de todas las combinaciones de fecha y género de los alumnos:

mysql> SELECT grade_evernt.date, student.sex,
-> COUNT(score.score) AS count, AVG(score.score) AS average
-> FROM grade_event INNER JOIN score INNER JOIN student
-> ON grade_event.event_id = score.event_id
-> AND score. student_id = student.student_id
-> GROUP BY grade_event.date, student.sex;

date sex count average
2008-09-03 F 14 14.6429
2008-09-03 M 15 15.6000
2008-09-06 F 14 14.7143
2008-09-06 M 16 13.6875
2008-09-09 F 15 77.4000
2008-09-09 M 16 79.0000
2008-09-16 F 13 15.3077
2008-09-16 M 14 12.8571
2008-09-23 F 12 14.0833
2008-09-23 M 15 14.2667
2008-10-01 F 14 77.7857
2008-10-01 M 15 82.4000

Podemos utilizar una consulta similar para realizar una de las tareas del proyecto de calificaciones: calcular la nota total por cada alumno al final del semestre:

SELECT student.student_id, student.name
SUM(score.score) AS total, COUNT(score.score) AS n
FROM grade_event INNER JOIN score INNER JOIN student
ON grade_event.event_id = score.event_id
AND score.student_id = student.student_id
GROUP BY score.student_id
ORDER BY tota;

No es obligatorio que una combinación se realice entre tablas diferentes. Puede parecer extraño pero puede combinar una tabla a sí misma. Por ejemplo, puede determinar si hay presidentes nacidos en la misma ciudad, si comprueba el lugar de nacimiento de todos los presidentes:

mysql> SELECT p1.last_name, p1.first_name, p1.city, p1.state
-> FROM president AS p1 INNER JOIN president AS p2
-> ON p1.city = p2.city AND p1.state = p2.state
-> WHERE (p1.last_name <> p2.last_name OR p1.first_name <> p2.first_name)
-> ORDER BY state, city, last_name;

last_name first_name city state
Adams John Quincy Braintree MA
Adams John Braintree MA

Esta consulta presenta dos peculiaridades:

  • Es necesario hacer referencia a dos instancias de la misma tabla, por lo que crearemos alias de tabla (p1, p2). Como sucede con los alias de columna, la palabra clave AS es opcional al hacer referencia a los alias.
  • Cada fila de presidente coincide consigo misma pero no queremos que aparezca en el resultado. La cláusula WHERE no permite que esto suceda y garantiza que en las filas se comparen distintos presidentes.

Una consulta similar busca presidentes nacidos el mismo día. Sin embargo, no se pueden comparar directamente las fechas de nacimiento ya que perderíamos presidentes nacidos en años diferentes. En su lugar, utilizamos MONTH() y DAYOFMONTH() para comparar el mes y el día de la fecha de nacimiento:

mysql> SELECT p1.last_name, p1.first_name, p1.birth
-> FROM president AS p1 INNER JOIN president AS p2
-> WHERE MONTH(p1.birth) = MONTH(p2.birth)
-> AND DAYOFMONTH(p1.birth) = DAYOFMONTH(p2.birth)
-> AND (p1.last_name <> p2.last_name OR p1.first_name <> p2.first_name)
-> ORDER BY p1.last_name;

last_name first_name birth
Harding Warren G. 1865-11-02
Polk James K. 1795-11-02

El uso de DAYOFYEAR() en lugar de la combinación de MONTH() y DAYOFMONTH() generaría una consulta más sencilla pero resultados incorrectos al comparar fechas de años bisiestos con fechas de años que no lo son.
Otro tipo de recuperación de varias tablas utiliza una subconsulta, una instrucción SELECT anidada dentro de otra. Imagine que desea identificar los alumnos con una asistencia perfecta. Es lo mismo que determinar qué alumnos no aparecen en la tabla absence, de esta forma:

mysql> SELECT * FROM student
-> WHERE student_id NOT IN (SELECT student_id FROM absence);

name sex student_id
Megan F 1
Joseph M 2
Katie F 4
Nathan M 6
Liesl F 7
...

La instrucción SELECT anidada determina el conjunto de valores student_id presentes en la tabla absence y la instrucción SELECT exterior recupera filas student que no coinciden con dichos ID.
Una subconsulta también proporciona una solución a la pregunta formulada en un tutorial anterior sobre presidentes nacidos antes que Andrew Jackson. En la solución original se utilizaban dos instrucciones y una variable de usuario pero se puede conseguir con la siguiente subconsulta:

mysql> SELECT last_name, first_name, birth FROM president
-> WHERE birth < (SELECT birth FROM president
-> WHERE last_name = 'Jackson' AND first_name = 'Andrew');

last_name first_name birth
Washington George 1732-02-22
Adam John 1735-10-30
Jefferson Thomas 1743-04-13
Madison James 1751-03-16
Monroe James 1758-04-28

La instrucción SELECT interna determina la fecha de nacimiento de Andrew Jackson y la externa recupera presidentes nacidos antes.

Comparte