Saltar a contenido

SQL Lab: joins

Ejercicio sql-lab-joins-annex.pdf · PDF

La generación 5 tiene 156 Pokémon. Los contaste tú mismo en la lección anterior. Ahora: ¿qué región es la generación 5? ¿Qué juego la introdujo? ¿En qué año?

Un fan responderá sin pestañear: Unova, Black and White, 2010. La tabla pokemon no puede. Sus cartas solo llevan un número, 5. Lo que ese número significa está escrito en otro sitio, en otra tabla. Las bases de datos reales están llenas de esto: una tabla dice "el cliente 4412 compró algo", otra dice quién es el 4412. Esta lección trata de leer dos tablas como una, y de las filas que desaparecen en silencio cuando lo haces.

Qué aprenderás

  • Cargar una segunda tabla, generations, y nombrar la columna que la une con pokemon: la clave
  • Escribir un INNER JOIN con ON y alias de tabla para leer columnas de dos tablas a la vez
  • Predecir cuántas filas devuelven un INNER JOIN y un LEFT JOIN, y explicar la diferencia
  • Desactivar la trampa del día: un join puede perder filas, sin ningún error
  • Encontrar las filas que descarta un INNER JOIN, con LEFT JOIN ... WHERE ... IS NULL
  • Combinar un join con GROUP BY para obtener una cifra por región

Una segunda caja: generations

Mantenemos la caja de zapatos de las dos últimas lecciones: pokemon es una caja con 949 cartas coleccionables. Hoy aparece sobre la mesa una segunda caja, diminuta: ocho cartas de región.

Descarga generations.csv desde la página Recursos y cárgalo igual que cargaste pokemon:

Paso Haz esto Deberías ver
1 En Superset: + (arriba a la derecha) → Data → Upload CSV to database El formulario de subida
2 CSV file: generations.csv. Database: Dades classe. Schema: public (nunca lo dejes vacío). Table name: generations. Delimiter: , El formulario rellenado
3 Pulsa Upload Un mensaje de confirmación
4 En SQL → SQL Lab, esquema public, ejecuta SELECT * FROM generations; 8 filas, 4 columnas
SELECT * FROM generations ORDER BY generation_id;
generation_id region first_game release_year
1 Kanto Red and Green 1996
2 Johto Gold and Silver 1999
3 Hoenn Ruby and Sapphire 2002
4 Sinnoh Diamond and Pearl 2006
5 Unova Black and White 2010
6 Kalos X and Y 2013
7 Alola Sun and Moon 2016
8 Galar Sword and Shield 2019

Ahí está la respuesta a la pregunta del principio, en la fila 5. Este tipo de tabla pequeña que explica un código se llama tabla de referencia (lookup table).

Si tu tabla tiene 16 filas

Subiste el archivo dos veces y la segunda subida se añadió a la primera. Súbelo de nuevo y pon If table already exists en Replace. Comprueba que hay 8 filas antes de seguir: con 16, todos los recuentos de esta página se duplican.

La clave: la línea impresa en las dos cartas

Una clave es una columna que dos tablas comparten y que te dice qué fila de una tabla pertenece a qué fila de la otra.

Mira las dos cajas. ¿Qué línea está impresa en las cartas de las dos? generation_id. Es lo único que conecta una carta de Pokémon con su carta de región.

flowchart LR
    subgraph P ["<b>pokemon</b> · 949 cartas"]
        direction TB
        P1["bulbasaur<br>generation_id = 1"]
        P2["pikachu<br>generation_id = 1"]
        P3["lucario<br>generation_id = 4"]
        P4["charizard-mega-x<br>generation_id = en blanco"]
    end
    subgraph G ["<b>generations</b> · 8 cartas"]
        direction TB
        G1["1 · Kanto · 1996"]
        G4["4 · Sinnoh · 2006"]
        G8["8 · Galar · 2019"]
    end
    P1 -- "1 = 1" --> G1
    P2 -- "1 = 1" --> G1
    P3 -- "4 = 4" --> G4

Tres cosas que notar en el dibujo:

  • Una carta de región, muchas cartas de Pokémon. Solo Kanto está unida a 151 Pokémon.
  • charizard-mega-x tiene el generation_id en blanco. No sale ninguna línea de él.
  • A Galar no llega ninguna línea. El archivo de Pokémon se detiene en la generación 7.

Quédate con las dos últimas. Son el argumento de esta lección.

INNER JOIN: une las cartas con un clip

Un join combina las filas de dos tablas en un solo resultado, emparejándolas con una condición. Un INNER JOIN se queda solo con las filas que han encontrado pareja.

Imagina un clip. Un join sujeta cada carta de Pokémon con la carta de región que tiene el mismo generation_id. El resultado es una carta ancha con las líneas de las dos.

SELECT p.pokemon, p.type_1, g.region, g.release_year
FROM pokemon AS p
INNER JOIN generations AS g ON p.generation_id = g.generation_id
ORDER BY p.id;
pokemon type_1 region release_year
bulbasaur grass Kanto 1996
ivysaur grass Kanto 1996
venusaur grass Kanto 1996
charmander fire Kanto 1996
... ... ... ...

Dos columnas de una caja, dos de la otra, en la misma fila. Tres piezas de gramática nueva:

Pieza Qué hace
FROM pokemon AS p p es un alias de tabla: un nombre corto para la tabla, válido solo en esta consulta. g es el alias de generations.
p.pokemon, g.region "La línea pokemon de la carta de la caja p", "la línea region de la carta de la caja g".
ON p.generation_id = g.generation_id La regla para sujetar con el clip: qué línea debe ser igual en las dos cartas.

El error que todo el mundo encuentra: ambiguous

Escribe ON generation_id = generation_id, sin los prefijos, y PostgreSQL responde column reference "generation_id" is ambiguous. Las dos cajas tienen esa línea: ¿a cuál te refieres? Siempre que un nombre de columna exista en las dos tablas, di de quién es: p.generation_id o g.generation_id.

Las filas que desaparecen

pokemon tiene 949 filas. Antes de ejecutar la siguiente consulta, adivina: ¿cuántas filas tiene el join?

SELECT COUNT(*)
FROM pokemon AS p
INNER JOIN generations AS g ON p.generation_id = g.generation_id;

802. No 949. Ningún error, ningún aviso. Han desaparecido 147 cartas.

Ya las conoces. En la lección anterior, agrupar por generation_id produjo un montón con la clave en blanco y exactamente 147 cartas. Un blanco nunca es igual a nada (recuerda = NULL de SQL Lab: filtros). Así que esas 147 cartas no encuentran pareja, y un INNER JOIN deja sobre la mesa, en silencio, una carta sin pareja.

Cuidado: un join no solo añade columnas

Es natural pensar "un join pega columnas extra a mi tabla, así que las filas siguen siendo las mismas". Falso. Un INNER JOIN conserva una fila solo si encuentra pareja. La consulta se ejecuta igual y el resultado sigue teniendo buen aspecto. Así que hazte un reflejo: después de cada join, cuenta las filas y compáralas con el recuento de antes.

LEFT JOIN: conserva todas las cartas de la caja izquierda

Un LEFT JOIN conserva todas las filas de la tabla izquierda, la que se escribe primero, después de FROM. Una fila sin pareja recibe NULL en todas las columnas de la otra tabla.

Cambia una palabra, INNER por LEFT, y cuenta otra vez:

SELECT COUNT(*)
FROM pokemon AS p
LEFT JOIN generations AS g ON p.generation_id = g.generation_id;

949. Todos han vuelto.

Join Con las dos cajas Filas
INNER JOIN Conserva solo las parejas unidas con el clip. Una carta sin pareja se queda fuera. 802
LEFT JOIN Conserva todas las cartas de la caja izquierda. ¿Sin pareja? Recibe una carta de región en blanco. 949

Un Pokémon muestra toda la diferencia. Busca charizard% con un LEFT JOIN:

SELECT p.pokemon, g.region
FROM pokemon AS p
LEFT JOIN generations AS g ON p.generation_id = g.generation_id
WHERE p.pokemon LIKE 'charizard%'
ORDER BY p.id;
pokemon region
charizard Kanto
charizard-mega-x
charizard-mega-y

Tres filas, dos de ellas con la región en blanco. Ejecuta la misma consulta con INNER JOIN y obtienes una fila: el charizard normal. Los dos Charizard Mega se esfuman.

Encontrar a los desaparecidos

Ese blanco es útil. Un LEFT JOIN seguido de "donde el otro lado está en blanco" lista exactamente las filas que un INNER JOIN descartaría:

SELECT p.pokemon, p.generation_id, g.region
FROM pokemon AS p
LEFT JOIN generations AS g ON p.generation_id = g.generation_id
WHERE g.region IS NULL
ORDER BY p.id;

147 filas. Las primeras: deoxys-attack, deoxys-defense, deoxys-speed, wormadam-sandy, wormadam-trash, shaymin-sky... Lee la lista hacia abajo y aparece un patrón: todas son formas alternativas de un Pokémon que ya existe. 48 de ellas son evoluciones Mega, 12 son Pikachu con disfraz, otras son formas de Alola. En este archivo, las formas alternativas no tienen generación.

¿Qué tabla está a la izquierda?

"Izquierda" solo significa escrita primero. Así que tú eliges qué caja se conserva entera. Da la vuelta al join:

SELECT g.region, COUNT(p.id) AS n
FROM generations AS g
LEFT JOIN pokemon AS p ON p.generation_id = g.generation_id
GROUP BY g.region
ORDER BY n DESC;
region n
Unova 156
Kanto 151
Hoenn 135
Sinnoh 107
Johto 100
Alola 81
Kalos 72
Galar 0

Galar aparece por primera vez, con 0 Pokémon. Era la carta de región a la que no apuntaba ninguna carta de Pokémon.

Esta consulta también muestra que un join y GROUP BY trabajan juntos: primero se unen las cartas con el clip, luego las cartas anchas se reparten en montones por región.

Cuidado: COUNT(*) diría que Galar tiene 1

Con COUNT(*) en lugar de COUNT(p.id), Galar muestra 1, no 0. El LEFT JOIN le da a Galar una carta ancha con las líneas de Pokémon en blanco, y COUNT(*) cuenta cartas. COUNT(p.id) cuenta solo las cartas donde de verdad se encontró un Pokémon. Es la diferencia entre COUNT(*) y COUNT(columna) de SQL Lab: grupos, ahora con consecuencias.

Tres recuentos, una tabla

Las mismas dos tablas, la misma clave, tres resultados distintos:

Join Filas Quién falta
pokemon INNER JOIN generations 802 Los 147 Pokémon sin generación y Galar
pokemon LEFT JOIN generations 949 Galar. Todos los Pokémon están; 147 tienen la región en blanco
generations LEFT JOIN pokemon 803 Los 147 Pokémon. Todas las regiones están; Galar tiene las columnas de Pokémon en blanco

Y la elección cambia las respuestas, no solo los recuentos. ¿Quién es el Pokémon más fuerte, y de qué región?

SELECT p.pokemon, p.attack, g.region
FROM pokemon AS p
INNER JOIN generations AS g ON p.generation_id = g.generation_id
ORDER BY p.attack DESC
LIMIT 3;
pokemon attack region
kartana 181 Alola
rampardos 165 Sinnoh
slaking 160 Hoenn

regigigas también tiene un ataque de 160: un empate, así que puede que veas ese en lugar de slaking.

SELECT p.pokemon, p.attack, g.region
FROM pokemon AS p
LEFT JOIN generations AS g ON p.generation_id = g.generation_id
ORDER BY p.attack DESC
LIMIT 3;
pokemon attack region
mewtwo-mega-x 190
heracross-mega 185
kartana 181 Alola

Los mismos datos, la misma pregunta, dos respuestas. Las evoluciones Mega no tienen generación en este archivo, así que el INNER JOIN tiró las cartas más fuertes de la caja. Ningún join es "el correcto". Tú eliges, y apuntas lo que has elegido.

Un join solo lee

Después de tanto clip, pokemon sigue teniendo 949 filas y generations sigue teniendo 8. Un join forma parte de un SELECT: construye un resultado en tu pantalla y no cambia nada en las tablas.

Pruébalo: las filas que desaparecen

En SQL Lab, con los alias p y g y siempre ON p.generation_id = g.generation_id. Para cada join, apunta cuántas filas salen.

N. Pregunta
1 Región y primer juego de pikachu, lucario y greninja, en una sola consulta.
2 ¿Cuántos Pokémon hay por región, con INNER JOIN y GROUP BY g.region? ¿Cuántas filas salen?
3 Lo mismo con LEFT JOIN. ¿Cuántas filas salen ahora? ¿Cuál es la nueva?
4 Ataque medio por región, la región más fuerte primero (redondeado, INNER JOIN).
Soluciones

1 — SELECT p.pokemon, g.region, g.first_game FROM pokemon AS p INNER JOIN generations AS g ON p.generation_id = g.generation_id WHERE p.pokemon IN ('pikachu', 'lucario', 'greninja'); → Kanto, Red and Green · Sinnoh, Diamond and Pearl · Kalos, X and Y.

2 — SELECT g.region, COUNT(*) AS n FROM pokemon AS p INNER JOIN generations AS g ON p.generation_id = g.generation_id GROUP BY g.region ORDER BY n DESC; → 7 filas: Unova 156, Kanto 151, Hoenn 135, Sinnoh 107, Johto 100, Alola 81, Kalos 72.

3 — Cambia INNER por LEFT → 8 filas: las mismas siete, más una fila con la región en blanco y 147.

4 — SELECT g.region, ROUND(AVG(p.attack)) AS avg_attack FROM pokemon AS p INNER JOIN generations AS g ON p.generation_id = g.generation_id GROUP BY g.region ORDER BY avg_attack DESC; → Alola 83, Unova 81, Sinnoh 80; Kanto, Hoenn y Kalos 73; Johto 68.

Si un join devuelve 7592 filas, la condición ON es siempre verdadera, o las tablas se unieron con una coma en lugar de con un JOIN: cada Pokémon se sujetó con el clip a cada región (949 × 8).

Ideas clave

  • Una clave es la columna que comparten dos tablas. Aquí es generation_id: una carta de región, muchas cartas de Pokémon.
  • Un join une con el clip filas que coinciden en una fila ancha. ON dice qué columnas deben ser iguales; los alias de tabla (p, g) dicen de quién es la columna.
  • Un INNER JOIN conserva solo las parejas unidas: 802 filas. Una clave en blanco nunca coincide, así que 147 Pokémon desaparecen sin error.
  • Un LEFT JOIN conserva todas las filas de la tabla escrita primero: 949 filas, con NULL donde no hay pareja.
  • LEFT JOIN ... WHERE g.region IS NULL lista las filas que descarta un INNER JOIN.
  • Después de cada join, cuenta las filas y compáralas con el recuento de antes. El join que elijas cambia la respuesta.

Comprobación rápida

1. ¿Cuál es la clave que une pokemon y generations?
  • region, porque es la columna más legible
  • generation_id, porque es la columna que tienen las dos tablas
  • id, porque todo Pokémon tiene uno
  • pokemon, porque es el nombre de la tabla

Una clave es la columna compartida por las dos tablas. Te dice qué carta de región pertenece a qué carta de Pokémon. region solo existe en generations, e id solo en pokemon.

2. pokemon tiene 949 filas. El INNER JOIN con generations devuelve 802. ¿Por qué?
  • El join elimina los Pokémon duplicados
  • SQL Lab solo muestra las primeras 802 filas
  • 147 Pokémon tienen el generation_id en blanco, no encuentran pareja y se quedan fuera
  • generations solo tiene sitio para 802 Pokémon

Un INNER JOIN conserva solo las filas que encuentran pareja. Un blanco (NULL) nunca es igual a nada, así que esas 147 formas alternativas no coinciden con ninguna región. No se muestra ningún error: solo lo notas si cuentas.

3. En FROM pokemon AS p LEFT JOIN generations AS g ON ..., ¿qué filas están garantizadas en el resultado?
  • Todas las filas de pokemon
  • Todas las filas de generations
  • Todas las filas de las dos tablas
  • Solo las filas que coinciden en las dos tablas

Un LEFT JOIN conserva entera la tabla izquierda, y la tabla izquierda es la que se escribe primero, después de FROM. Aquí es pokemon: 949 filas, 147 de ellas con la región en blanco. Galar sigue faltando.

4. ¿Qué consulta lista los Pokémon que descartaría un INNER JOIN?
  • ... INNER JOIN generations AS g ON ... WHERE g.region IS NULL
  • ... LEFT JOIN generations AS g ON ... WHERE g.region IS NULL
  • ... LEFT JOIN generations AS g ON ... WHERE g.region = NULL
  • ... INNER JOIN generations AS g ON ... WHERE p.generation_id = 0

El LEFT JOIN conserva las cartas sin pareja y rellena su región con NULL; IS NULL elige entonces exactamente esas. Con un INNER JOIN ya han desaparecido, y = NULL nunca es verdadero.

5. ¿Por qué PostgreSQL se queja de que la referencia a la columna generation_id es ambigua?
  • La columna tiene blancos
  • Las dos columnas tienen tipos de datos distintos
  • Las dos tablas tienen una columna con ese nombre, y la consulta no dice a cuál se refiere
  • La tabla generations se subió dos veces

Cuando un nombre de columna existe en las dos tablas, debes decir de quién es con el alias de tabla: p.generation_id o g.generation_id.

6. generations AS g LEFT JOIN pokemon AS p, agrupado por región, muestra Galar con 1 si usas COUNT(*) y con 0 si usas COUNT(p.id). ¿Cuál es correcto y por qué?
  • 1, porque toda región tiene al menos un Pokémon
  • 1, porque COUNT(*) siempre es el más exacto
  • 0, porque COUNT(p.id) solo cuenta las filas donde de verdad se encontró un Pokémon
  • Ninguno: Galar no debería aparecer

El LEFT JOIN le da a Galar una fila con las columnas de Pokémon en blanco. COUNT(*) cuenta esa fila; COUNT(p.id) se salta el blanco. Galar no tiene Pokémon en este archivo, así que 0 es el número honesto.

7. Una tienda tiene customers(customer_id, name) y orders(order_id, customer_id). Quieres los clientes que nunca han hecho un pedido. Elige las dos cosas que necesita tu consulta.
  • FROM customers AS c LEFT JOIN orders AS o ON c.customer_id = o.customer_id
  • WHERE o.order_id IS NULL
  • FROM customers AS c INNER JOIN orders AS o ON c.customer_id = o.customer_id
  • WHERE o.order_id = NULL

El mismo patrón que con las 147: pon a la izquierda la tabla que quieres conservar entera (customers), y quédate con las filas donde el otro lado está en blanco. Un INNER JOIN descartaría justo a los clientes que buscas.

Hoy has escrito buenas consultas. ¿Dónde estarán mañana? Siguiente: Datasets y destino, donde guardas la pregunta y almacenas la respuesta.