Salta el contingut

SQL Lab: joins

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

La generació 5 té 156 Pokémon. Els vas comptar tu mateix a la lliçó anterior. Ara: quina regió és la generació 5? Quin joc la va introduir? En quin any?

Un fan et respondrà sense pestanyejar: Unova, Black and White, 2010. La taula pokemon no pot. Les seves cartes només porten un número, 5. El que aquest número vol dir està escrit en un altre lloc, en una altra taula. Les bases de dades reals en van plenes: una taula diu «el client 4412 ha comprat alguna cosa», una altra diu qui és el 4412. Aquesta lliçó va de llegir dues taules com si fossin una, i de les files que desapareixen en silenci quan ho fas.

Què aprendràs

  • Carregar una segona taula, generations, i anomenar la columna que l'enllaça amb pokemon: la clau
  • Escriure un INNER JOIN amb ON i àlies de taula per llegir columnes de dues taules alhora
  • Predir quantes files retornen un INNER JOIN i un LEFT JOIN, i explicar la diferència
  • Desactivar la trampa del dia: un join pot perdre files, sense cap error
  • Trobar les files que un INNER JOIN descarta, amb LEFT JOIN ... WHERE ... IS NULL
  • Combinar un join amb GROUP BY per obtenir una xifra per regió

Una segona capsa: generations

Mantenim la capsa de sabates de les dues últimes lliçons: pokemon és una capsa de 949 cartes col·leccionables. Avui apareix sobre la taula una segona capsa, minúscula: vuit cartes de regió.

Descarrega generations.csv de la pàgina Recursos i carrega'l de la mateixa manera que vas carregar pokemon:

Pas Fes això Hauries de veure
1 A Superset: + (a dalt a la dreta) → Data → Upload CSV to database El formulari de pujada
2 CSV file: generations.csv. Database: Dades classe. Schema: public (no el deixis mai buit). Table name: generations. Delimiter: , El formulari emplenat
3 Prem Upload Un missatge de confirmació
4 A SQL → SQL Lab, esquema public, executa SELECT * FROM generations; 8 files, 4 columnes
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

Aquí tens la resposta a la pregunta inicial, a la fila 5. Aquest tipus de taula petita que explica un codi s'anomena taula de cerca (lookup table).

Si la teva taula té 16 files

Has pujat el fitxer dues vegades i la segona pujada s'ha afegit a la primera. Torna a pujar-lo i posa If table already exists a Replace. Comprova que hi ha 8 files abans de continuar: amb 16, tots els recomptes d'aquesta pàgina es dupliquen.

La clau: la línia impresa a les dues cartes

Una clau és una columna que dues taules comparteixen i que et diu quina fila d'una taula pertany a quina fila de l'altra.

Mira les dues capses. Quina línia és impresa a les cartes de totes dues? generation_id. És l'únic que connecta una carta de Pokémon amb la seva carta de regió.

flowchart LR
    subgraph P ["<b>pokemon</b> · 949 cartes"]
        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 blanc"]
    end
    subgraph G ["<b>generations</b> · 8 cartes"]
        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 coses per notar al dibuix:

  • Una carta de regió, molts Pokémon. Només Kanto està enllaçada amb 151 Pokémon.
  • charizard-mega-x té el generation_id en blanc. No en surt cap línia.
  • A Galar no hi arriba cap línia. El fitxer de Pokémon s'atura a la generació 7.

Guarda't les dues últimes. Són la trama d'aquesta lliçó.

INNER JOIN: enganxa les cartes amb un clip

Un join combina les files de dues taules en un sol resultat, aparellant-les per una condició. Un INNER JOIN conserva només les files que han trobat parella.

Imagina un clip. Un join enganxa cada carta de Pokémon amb la carta de regió que té el mateix generation_id. El resultat és una carta ampla amb les línies de totes dues.

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
... ... ... ...

Dues columnes d'una capsa, dues de l'altra, a la mateixa fila. Tres peces de gramàtica nova:

Peça Què fa
FROM pokemon AS p p és un àlies de taula: un nom curt per a la taula, vàlid només en aquesta consulta. g és l'àlies de generations.
p.pokemon, g.region «La línia pokemon de la carta de la capsa p», «la línia region de la carta de la capsa g».
ON p.generation_id = g.generation_id La regla per enganxar: quina línia ha de ser igual a les dues cartes.

L'error que es troba tothom: ambiguous

Escriu ON generation_id = generation_id, sense els prefixos, i PostgreSQL respon column reference "generation_id" is ambiguous. Les dues capses tenen aquella línia: de quina parles? Sempre que un nom de columna existeix a les dues taules, digues de qui és: p.generation_id o g.generation_id.

Les files que desapareixen

pokemon té 949 files. Abans d'executar la consulta següent, endevina: quantes files té el join?

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

802. No 949. Cap error, cap avís. Han desaparegut 147 cartes.

Ja les coneixes. A la lliçó anterior, en agrupar per generation_id va sortir un munt amb la clau en blanc i exactament 147 cartes. Un blanc no és mai igual a res; recorda el = NULL de SQL Lab: filtres. Així que aquestes 147 cartes no troben parella, i un INNER JOIN deixa sobre la taula, en silenci, una carta sense parella.

Compte: un join no només afegeix columnes

És natural pensar «un join enganxa columnes extra a la meva taula, així que les files es queden igual». Fals. Un INNER JOIN conserva una fila només si troba parella. La consulta s'executa igualment i el resultat segueix semblant correcte. Per tant, fes-ne un reflex: després de cada join, compta les files i compara-les amb el recompte d'abans.

LEFT JOIN: conserva totes les cartes de la capsa de l'esquerra

Un LEFT JOIN conserva totes les files de la taula de l'esquerra, la que s'escriu primer, després de FROM. Una fila sense parella obté NULL a totes les columnes de l'altra taula.

Canvia una paraula, INNER per LEFT, i torna a comptar:

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

949. Tothom ha tornat.

Join Amb les dues capses Files
INNER JOIN Conserva només les parelles enganxades. Una carta sense parella queda fora. 802
LEFT JOIN Conserva cada carta de la capsa de l'esquerra. Sense parella? Obté una carta de regió en blanc. 949

Un sol Pokémon mostra tota la diferència. Cerca charizard% amb 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 files, dues amb la regió en blanc. Executa la mateixa consulta amb INNER JOIN i en obtens una: el charizard normal. Els dos Charizard Mega s'esvaeixen.

Trobar els desapareguts

Aquest blanc és útil. Un LEFT JOIN seguit de «on l'altre costat és en blanc» llista exactament les files que un INNER JOIN descartaria:

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 files. Les primeres: deoxys-attack, deoxys-defense, deoxys-speed, wormadam-sandy, wormadam-trash, shaymin-sky... Llegeix la llista cap avall i apareix un patró: tots són formes alternatives d'un Pokémon que ja existeix. 48 són evolucions Mega, 12 són Pikachu amb disfressa, d'altres són formes d'Alola. En aquest fitxer, les formes alternatives no tenen generació.

Quina taula és a l'esquerra?

«Esquerra» vol dir simplement escrita primer. Així que tries quina capsa es conserva sencera. Gira el 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 apareix per primera vegada, amb 0 Pokémon. Era la carta de regió a la qual cap carta de Pokémon apuntava.

Aquesta consulta també mostra que un join i GROUP BY treballen junts: primer s'enganxen les cartes, després les cartes amples es reparteixen en munts per regió.

Compte: COUNT(*) diria que Galar en té 1

Amb COUNT(*) en lloc de COUNT(p.id), Galar mostra 1, no 0. El LEFT JOIN dona a Galar una carta ampla amb les línies de Pokémon en blanc, i COUNT(*) compta cartes. COUNT(p.id) només compta les cartes on s'ha trobat un Pokémon de debò. És la diferència entre COUNT(*) i COUNT(column) de SQL Lab: grups, ara amb conseqüències.

Tres recomptes, una sola taula

Les mateixes dues taules, la mateixa clau, tres resultats diferents:

Join Files Qui hi falta
pokemon INNER JOIN generations 802 Els 147 Pokémon sense generació i Galar
pokemon LEFT JOIN generations 949 Galar. Tots els Pokémon hi són; 147 tenen la regió en blanc
generations LEFT JOIN pokemon 803 Els 147 Pokémon. Totes les regions hi són; Galar té les columnes de Pokémon en blanc

I l'elecció canvia respostes, no només recomptes. Quin és el Pokémon més fort, i de quina regió?

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 també té un atac de 160: un empat, així que potser veuràs aquest en lloc 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

Les mateixes dades, la mateixa pregunta, dues respostes. Les evolucions Mega no tenen generació en aquest fitxer, així que l'INNER JOIN va llençar les cartes més fortes de la capsa. Cap join és «el correcte». Tries tu, i deixes escrit què has triat.

Un join només llegeix

Després de tant enganxar, pokemon encara té 949 files i generations encara en té 8. Un join és part d'un SELECT: construeix un resultat a la teva pantalla i no canvia res a les taules.

Prova-ho: les files que desapareixen

A SQL Lab, amb els àlies p i g i sempre amb ON p.generation_id = g.generation_id. Per a cada join, anota quantes files en surten.

N. Pregunta
1 Regió i primer joc de pikachu, lucario i greninja, en una sola consulta.
2 Quants Pokémon hi ha per regió, amb INNER JOIN i GROUP BY g.region? Quantes files?
3 El mateix amb LEFT JOIN. Quantes files ara? Quina és la nova?
4 Atac mitjà per regió, primer la regió més forta (arrodonit, INNER JOIN).
Solucions

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 files: Unova 156, Kanto 151, Hoenn 135, Sinnoh 107, Johto 100, Alola 81, Kalos 72.

3 — Canvia INNER per LEFT → 8 files: les mateixes set, més una fila amb la regió en blanc i 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 i Kalos 73; Johto 68.

Si un join retorna 7592 files, la condició ON és sempre certa, o bé les taules s'han unit amb una coma en lloc d'un JOIN: cada Pokémon s'ha enganxat a cada regió (949 × 8).

Idees clau

  • Una clau és la columna que dues taules comparteixen. Aquí és generation_id: una carta de regió, moltes cartes de Pokémon.
  • Un join enganxa files que coincideixen en una sola fila ampla. ON diu quines columnes han de ser iguals; els àlies de taula (p, g) diuen de quina taula és la columna.
  • Un INNER JOIN conserva només les parelles enganxades: 802 files. Una clau en blanc no coincideix mai, així que 147 Pokémon desapareixen sense cap error.
  • Un LEFT JOIN conserva totes les files de la taula escrita primer: 949 files, amb NULL on no hi ha parella.
  • LEFT JOIN ... WHERE g.region IS NULL llista les files que un INNER JOIN descarta.
  • Després de cada join, compta les files i compara-les amb el recompte d'abans. El join que tries canvia la resposta.

Comprovació ràpida

1. Quina és la clau que enllaça pokemon i generations?
  • region, perquè és la columna més llegible
  • generation_id, perquè és la columna que tenen totes dues taules
  • id, perquè tots els Pokémon en tenen un
  • pokemon, perquè és el nom de la taula

Una clau és la columna compartida per les dues taules. Et diu quina carta de regió pertany a quina carta de Pokémon. region només existeix a generations, i id només a pokemon.

2. pokemon té 949 files. L'INNER JOIN amb generations en retorna 802. Per què?
  • El join elimina els Pokémon duplicats
  • SQL Lab només mostra les primeres 802 files
  • 147 Pokémon tenen el generation_id en blanc, no troben parella i queden fora
  • generations només té lloc per a 802 Pokémon

Un INNER JOIN conserva només les files que troben parella. Un blanc (NULL) no és mai igual a res, així que aquestes 147 formes alternatives no coincideixen amb cap regió. No es mostra cap error: només ho notes si comptes.

3. A FROM pokemon AS p LEFT JOIN generations AS g ON ..., quines files hi ha garantides al resultat?
  • Totes les files de pokemon
  • Totes les files de generations
  • Totes les files de les dues taules
  • Només les files que coincideixen a les dues taules

Un LEFT JOIN conserva sencera la taula de l'esquerra, i la taula de l'esquerra és la que s'escriu primer, després de FROM. Aquí és pokemon: 949 files, 147 d'elles amb la regió en blanc. Galar continua sense aparèixer.

4. Quina consulta llista els Pokémon que un INNER JOIN descartaria?
  • ... 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 les cartes sense parella i omple la seva regió amb NULL; IS NULL tria exactament aquestes. Amb un INNER JOIN ja han desaparegut, i = NULL no és mai cert.

5. Per què PostgreSQL es queixa que la referència de columna generation_id és ambigua?
  • La columna té blancs
  • Les dues columnes tenen tipus de dades diferents
  • Les dues taules tenen una columna amb aquest nom, i la consulta no diu quina de les dues vol dir
  • La taula generations s'ha pujat dues vegades

Quan un nom de columna existeix a les dues taules, has de dir de qui és amb l'àlies de taula: p.generation_id o g.generation_id.

6. generations AS g LEFT JOIN pokemon AS p, agrupat per regió, mostra Galar amb 1 si fas servir COUNT(*) i amb 0 si fas servir COUNT(p.id). Quin és el correcte, i per què?
  • 1, perquè totes les regions tenen almenys un Pokémon
  • 1, perquè COUNT(*) és sempre el més exacte
  • 0, perquè COUNT(p.id) només compta les files on s'ha trobat un Pokémon de debò
  • Cap dels dos: Galar no hauria d'aparèixer

El LEFT JOIN dona a Galar una fila amb les columnes de Pokémon en blanc. COUNT(*) compta aquesta fila; COUNT(p.id) se salta el blanc. Galar no té cap Pokémon en aquest fitxer, així que 0 és el nombre honest.

7. Una botiga té customers(customer_id, name) i orders(order_id, customer_id). Vols els clients que mai no han fet cap comanda. Tria les dues coses que necessita la teva 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 mateix patró que els 147: posa la taula que vols conservar sencera a l'esquerra (customers), i queda't amb les files on l'altre costat és en blanc. Un INNER JOIN descartaria exactament els clients que busques.

Avui has escrit unes quantes bones consultes. On seran demà? Tot seguit: Datasets i destí, on desaràs la pregunta i guardaràs la resposta.