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 conpokemon: la clave - Escribir un
INNER JOINconONy alias de tabla para leer columnas de dos tablas a la vez - Predecir cuántas filas devuelven un
INNER JOINy unLEFT 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, conLEFT JOIN ... WHERE ... IS NULL - Combinar un join con
GROUP BYpara 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-xtiene elgeneration_iden 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 JOINse 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 JOINconserva todas las filas de la tabla izquierda, la que se escribe primero, después deFROM. Una fila sin pareja recibeNULLen 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.
ONdice qué columnas deben ser iguales; los alias de tabla (p,g) dicen de quién es la columna. - Un
INNER JOINconserva solo las parejas unidas: 802 filas. Una clave en blanco nunca coincide, así que 147 Pokémon desaparecen sin error. - Un
LEFT JOINconserva todas las filas de la tabla escrita primero: 949 filas, conNULLdonde no hay pareja. LEFT JOIN ... WHERE g.region IS NULLlista las filas que descarta unINNER 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_iden blanco, no encuentran pareja y se quedan fuera -
generationssolo 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
generationsse 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.