Monta taulua
Taulujen viittaukset ja kyselyt (References and queries)
Keskeinen idea tietokannoissa on, että taulun rivi voi viitata toisen taulun riviin. Tällöin voimme muodostaa kyselyn, joka kerää tietoa useista tauluista viittausten perusteella. Käytännössä viittauksena on yleensä toisessa taulussa olevan rivin id
-numero.
Esimerkki
Tarkastellaan esimerkkinä tilannetta, jossa tietokannassa on tietoa kursseista ja niiden opettajista. Oletamme, että jokaisella kurssilla on yksi opettaja ja sama opettaja voi opettaa monta kurssia.
Tallennamme tauluun Teachers
tietoa opettajista. Jokaisella opettajalla on id
-numero, jolla voimme viitata siihen.
id name---------- ----------1 Ahonen2 Isohanni3 Niemi4 Laaksonen
Taulussa Courses
on puolestaan tietoa kursseista ja jokaisen kurssin kohdalla viittaus kurssin opettajaan.
id name teacher_id---------- ---------------- -----------1 Basic programming 32 More programming 13 Algorithms 14 Scrum masters 45 Algebra 3
Voimme nyt hakea kurssit opettajineen seuraavalla kyselyllä, joka hakee tietoa samaan aikaan tauluista Courses
ja Teachers
:
SELECT Courses.name, Teachers.nameFROM Courses, TeachersWHERE Courses.teacher_id = Teachers.id;
Koska kyselyssä on monta taulua, ilmoitamme sarakkeiden taulut. Esimerkiksi Courses.name
viittaa taulun Courses
sarakkeeseen name
.
Kysely antaa seuraavan tuloksen:
name name------------------ ----------Basic programming NiemiMore programming AhonenAlgorithms AhonenScrum masters LaaksonenAlgebra Niemi
Mitä tässä oikeastaan tapahtui?
Yllä olevassa kyselyssä uutena asiana on, että kysely koskee useaa taulua (FROM Courses, Teachers), mutta mitä tämä tarkoittaa oikeastaan?
Ideana on, että kun kyselyssä on monta taulua, kyselyn tulosrivien lähtökohtana ovat kaikki tavat valita rivien yhdistelmiä tauluista. Tämän jälkeen WHERE
-osan ehdoilla voi määrittää, mitkä yhdistelmät ovat kiinnostuksen kohteena.
Hyvä tapa saada ymmärrystä monen taulun kyselyn toiminnasta on tarkastella ensin kyselyä, joka hakee kaikki sarakkeet ja jossa ei ole WHERE
-osaa. Yllä olevassa esimerkkitilanteessa tällainen kysely on seuraava:
SELECT * FROM Courses, Teachers;
Koska taulussa Courses
on 5 riviä ja taulussa Teachers
on 4 riviä, kyselyn tulostaulussa on 5 * 4 = 20 riviä. Tulostaulu sisältää kaikki mahdolliset tavat valita ensin jokin rivi taulusta Courses ja sitten jokin rivi taulusta Teachers:
id name teacher_id id name---------- ------------------ ----------- ---------- ----------1 Basic programming 3 1 Ahonen1 Basic programming 3 2 Isohanni1 Basic programming 3 3 Niemi1 Basic programming 3 4 Laaksonen2 More programming 1 1 Ahonen2 More programming 1 2 Isohanni2 More programming 1 3 Niemi2 More programming 1 4 Laaksonen3 Algorithms 1 1 Ahonen3 Algorithms 1 2 Isohanni3 Algorithms 1 3 Niemi3 Algorithms 1 4 Laaksonen4 Scrum masters 4 1 Ahonen4 Scrum masters 4 2 Isohanni4 Scrum masters 4 3 Niemi4 Scrum masters 4 4 Laaksonen5 Algebra 3 1 Ahonen5 Algebra 3 2 Isohanni5 Algebra 3 3 Niemi5 Algebra 3 4 Laaksonen
Suurin osa tulosriveistä ei ole kuitenkaan kiinnostavia, koska ne eivät liity toisiinsa mitenkään. Esimerkiksi ensimmäinen tulosrivi kertoo vain, että on olemassa kurssi Basic programming ja toisaalta on olemassa opettaja Ahonen. Tämän vuoksi rajaamme hakua niin, että opettajan id
-numeron tulee olla sama kummankin taulun riveissä:
SELECT * FROM Courses, TeachersWHERE Courses.teacher_id = Teachers.id;
Tämän seurauksena kysely alkaa antaa mielekkäitä tuloksia:
id name teacher_id id name---------- ---------------- ----------- ---------- ----------1 Basic programming 3 3 Niemi2 More programming 1 1 Ahonen3 Algorithms 1 1 Ahonen4 Scrum masters 4 4 Laaksonen5 Algebra 3 3 Niemi
Tämän jälkeen voimme vielä parantaa kyselyä valitsemalla meitä kiinnostavat sarakkeet:
SELECT Courses.name, Teachers.nameFROM Courses, TeachersWHERE Courses.teacher_id = Teachers.id;
Näin päädymme samaan tulokseen kuin aiemmin:
name name------------------ ----------Basic programming NiemiMore programming AhonenAlgorithms AhonenScrum masters LaaksonenAlgebra Niemi
Lisää ehtoja kyselyssä
Monen taulun kyselyissä WHERE
-osa kytkee toisiinsa meitä kiinnostavat taulujen rivit, mutta lisäksi voimme laittaa WHERE
-osaan muita ehtoja samaan tapaan kuin ennenkin. Esimerkiksi voimme suorittaa seuraavan kyselyn:
SELECT Courses.name, Teachers.nameFROM Courses, TeachersWHERE Courses.teacher_id = Teachers.id AND Teachers.name = 'Niemi';
Näin saamme haettua kurssit, joiden opettajana on Niemi:
name name---------------- ----------Basic programming NiemiAlgebra Niemi
Taulujen lyhyet nimet
Voimme tiivistää monen taulun kyselyä antamalla tauluille vaihtoehtoiset lyhyet nimet, joiden avulla voimme viitata niihin kyselyssä. Esimerkiksi kysely
SELECT Courses.name, Teachers.nameFROM Courses, TeachersWHERE Courses.teacher_id = Teachers.id;
Voidaan esittää lyhemmin näin:
SELECT C.name, T.nameFROM Courses AS C, Teachers AS TWHERE C.teacher_id = T.id;
Koska sana AS
ei ole pakollinen, eli voimme lyhentää kyselyä lisää:
SELECT C.name, T.nameFROM Courses C, Teachers TWHERE C.teacher_id = T.id;
Saman taulun toistaminen (Repeating a table)
Monen taulun kyselyssä voi esiintyä myös monta kertaa sama taulu, kunhan niille annetaan eri nimet. Esimerkiksi seuraava kysely hakee kaikki tavat valita kahden opettajan pari:
SELECT A.name, B.name FROM Teachers A, Teachers B;
Kyselyn tulos on seuraava:
name name---------- ----------Ahonen AhonenAhonen IsohanniAhonen NiemiAhonen LaaksonenIsohanni AhonenIsohanni IsohanniIsohanni NiemiIsohanni LaaksonenNiemi AhonenNiemi IsohanniNiemi NiemiNiemi LaaksonenLaaksonen AhonenLaaksonen IsohanniLaaksonen NiemiLaaksonen Laaksonen
Liitostaulut (Junction table)
Taulujen välillä esiintyy yleensä kahdenlaisia suhteita:
- Yksi moneen -suhde (One-to-many): Taulun A rivi liittyy enintään yhteen taulun B riviin. Taulun B rivi voi liittyä useaan taulun A riviin.
- Monta moneen -suhde (Many-to-many): Taulun A rivi voi liittyä useaan taulun B riviin. Taulun B rivi voi liittyä useaan taulun A riviin.
Tapauksessa 1 voimme lisätä tauluun A sarakkeen, joka viittaa tauluun B, kuten teimme edellisen osion esimerkissä. Tapauksessa 2 tilanne on kuitenkin hankalampi, koska yksittäinen viittaus kummankaan taulun rivissä ei riittäisi. Ratkaisuna on luoda kolmas liitostaulu, joka sisältää tiedot viittauksista.
Esimerkki
Tarkastellaan esimerkkinä tilannetta, jossa verkkokaupassa on tuotteita ja asiakkaita ja jokainen asiakas on valinnut tiettyjä tuotteita ostoskoriin. Tietyn asiakkaan korissa voi olla useita tuotteita, ja toisaalta tietty tuote voi olla usean asiakkaan korissa.
Rakennamme tietokannan niin, että siinä on kolme taulua: Products, Customers ja Purchases. Liitostaulu Purchases ilmaisee, mitä tuotteita on kunkin asiakkaan ostoskorissa. Sen jokainen rivi esittää yhden parin muotoa “asiakkaan X korissa on tuote Y”.
Oletamme, että taulujen sisällöt ovat seuraavat:
Productsid name price-- ------ -----1 radish 72 carrot 53 turnip 44 parsley 85 celery 4Customersid name-- ------1 Uolevi2 Maija3 AapeliPurchasescustomer_id product_id----------- ----------1 21 52 12 42 5
Nyt voimme hakea asiakkaat ja tuotteet seuraavasti:
SELECT C.name, P.nameFROM Customers C, Products P, Purchases OWHERE C.id = O.customer_id AND P.id = O.product_id;
Kyselyn ideana on hakea tauluista Customers
ja Products
taulun Purchases
rivejä vastaavat tiedot. Jotta saamme mielekkäitä tuloksia, kytkemme rivit yhteen kahden ehdon avulla. Kysely tuottaa seuraavan tulostaulun:
name name---------- ----------Uolevi carrotUolevi celeryMaija radishMaija parsleyMaija celery
Voimme lisätä kyselyyn lisää ehtoja, jos haluamme saada selville muuta ostoskoreista. Esimerkiksi seuraava kysely hakee Maijan korissa olevat tuotteet:
SELECT P.nameFROM Customers C, Products P, Purchases OWHERE C.id = O.customer_id AND P.id = O.product_id AND C.name = 'Maija';
name----------radishparsleycelery
Yhteenveto tauluista
Voimme käyttää koostefunktioita ja ryhmittelyä myös usean taulun kyselyissä. Ne käsittelevät tulostaulua samalla periaatteella kuin yhden taulun kyselyissä. Voimme esimerkiksi luoda yhteenvedon, joka näyttää jokaiselta asiakkaalta kuinka monta tuotetta heillä on ostoskoreissaan, ja mikä on kokonaishinta. Voimme tehdä sen seuraavasti:
SELECT C.name, COUNT(P.id), SUM(P.price)FROM Customers C, Products P, Purchases OWHERE C.id = O.customer_id AND P.id = O.product_idGROUP BY C.id;
Miten kysely toimii?
Tässä kyselyssä ryhmittelemme sarakkeella C.id
, mutta haemme sarakkeella C.name
. Tämä on sinänsä järkevää, koska sarake C.id
määrää sarakkeen C.name
, ja kysely toimii mainiosti SQLitessä.
Muissa tietokannoissa (kuten PostgreSQL) voi kuitenkin olla, että sellaisenaan haettavan sarakkeen tulee aina esiintyä myös ryhmittelyssä. Tällöin ryhmittelyn tulisi olla GROUP BY C.id, C.name
.
Kyselyn ideana on ryhmittää rivit asiakkaan id
-numeron mukaan, jolloin funktio COUNT (P.id)
antaa tuotteiden lukumäärän asiakkaan ostoskorissa ja funktio SUM(P.price)
antaa kyseisten tuotteiden kokonaishinnan. Tulostaulumme on seuraavanlainen:
name COUNT(P.id) SUM(P.price)---------- ----------- ------------Uolevi 2 9Maija 3 19
Tämä tarkoittaa, että Uolevin ostokset sisältävät 2 tuotetta, ja yhteishinta on 9. Maijalla on toisaalta 3 tuotetta ja kokonaishinta on 19. Kaikki näyttää hyvältä... Vai näyttääkö?
Ongelmana on, että kyselystämme puuttuu kolmas asiakas, Aapeli. Olemme törmänneet ongelmaan, jonka ratkaisemme tämän osion lopuksi.
JOIN-syntaksi (JOIN syntax)
Tähän mennessä olemme hakeneet tietoa tauluista listaamalla taulut kyselyn FROM
-osassa, mikä toimii yleensä hyvin. Kuitenkin joskus on tarpeen vaihtoehtoinen JOIN
-syntaksi. Siitä on hyötyä silloin, kun kyselyn tuloksesta näyttää “puuttuvan” tietoa.
Kyselytavat
Seuraavassa on kaksi tapaa toteuttaa sama kysely, ensin käyttäen ennestään tuttua tapaa ja sitten käyttäen JOIN
-syntaksia.
SELECT Courses.name, Teachers.nameFROM Courses, TeachersWHERE Courses.teacher_id = Teachers.id;SELECT Courses.name, Teachers.nameFROM Courses JOIN Teachers ON Courses.teacher_id = Teachers.id;
Jälkimmäisen kyselyn JOIN-syntaksissa taulujen nimien välissä esiintyy sana JOIN ja lisäksi taulujen rivit toisiinsa kytkevä ehto annetaan erillisessä ON
-osassa. Tämän jälkeen voisimme vielä käyttää WHERE
lisätäksemme lisää ehtoja, kuten aiemminkin.
Tässä tapauksessa JOIN-syntaksi on vain vaihtoehtoinen tapa toteuttaa kysely eikä se tuo mitään uutta. Kuitenkin näemme seuraavaksi, miten voimme laajentaa syntaksia niin, että se antaa meille uusia mahdollisuuksia kyselyissä.
Puuttuvan tiedon ongelma
Katsotaan tilannetta, missä meillä on esimerkkitaulut Courses
ja Teachers
, mutta yhdeltä kurssilta puuttuu opettaja:
id name teacher_id---------- ---------------- -----------1 Basic programming 32 More programming 13 Algorithms 14 Scrum masters5 Algebra 3
Rivillä 4 sarakkeessa teacher_id
arvo on NULL
, joten jos käytämme kumpaa tahansa aiempaa kyselyä, rivi 4 ei vastaa yhtäkään riviä taulusta Teachers
. Tämän takia tulostaulu ei sisällä riviä Scrum masters:
name name------------------ ----------Basic programming NiemiMore programming AhonenAlgorithms AhonenAlgebra Niemi
Ratkaisu ongelmaan on käyttää LEFT JOIN
-syntaksia. Tämä tarkoittaa, että jos vasemmanpuoleinen taulu ei viittaa riviin oikeanpuoleisessa taulussa, rivi sisällytetään silti tulostauluun. Tällaiselle riville viite oikeaan tauluun on NULL
.
Meidän tapauksessamme voisimme tehdä kyselyn täten:
SELECT Courses.name, Teachers.nameFROM Courses LEFT JOIN Teachers ON Courses.teacher_id = Teachers.id;
Nyt saamme kurssin Scrum masters ilman opettajaa:
name name------------------ ----------Basic programming NiemiMore programming AhonenAlgorithms AhonenScrum mastersAlgebra Niemi
JOIN-kyselyperhe
JOIN kyselyllä
on neljä muunnosta:
JOIN
: toimii kuten tavallinen kahden taulun kysely.LEFT JOIN
: jos vasemman taulun rivi ei yhdisty mihinkään oikean taulun riviin, se valitaan kuitenkin mukaan erikseen.RIGHT JOIN
: jos oikean taulun rivi ei yhdisty mihinkään vasemman taulun riviin, se valitaan kuitenkin mukaan erikseenFULL JOIN
: sekä vasemmasta että oikeasta taulusta valitaan erikseen mukaan rivit, jotka eivät yhdisty toisen taulun riviin
SQLiten rajoituksena on kuitenkin, että vain kaksi ensimmäistä kyselytapaa ovat mahdollisia. Onneksi LEFT JOIN
on yleensä se, mitä haluamme.
Venn-diagrammi-esitys on kutakuinkin seuraava:
Tässä kaaviossa on enemmän kuin neljä variaatiota. INNER JOIN
on sama kuin JOIN
, ja FULL OUTER JOIN
on FULL JOIN
. Muut variaatiot ovat ekslusiivisia variaatioita kyselyille.
Puuttuuvaa tietoa yhteenvetokyselyssä
Nyt voimme ratkaista puuttuvan Aapelin ongelman. Tietokannassamme meillä on seuraavat taulut:
Productsid name price-- ------ -----1 radish 72 carrot 53 turnip 44 parsley 85 celery 4Customersid name-- ------1 Uolevi2 Maija3 AapeliPurchasescustomer_id product_id----------- ----------1 21 52 12 42 5
Teimme ostosten yhteenvetokyselyn seuraavasti:
SELECT C.name, COUNT(P.id), SUM(P.price)FROM Customers C, Products P, Purchases OWHERE C.id = O.customer_id AND P.id = O.product_idGROUP BY C.id;
Tuloksena Aapeli puuttui tulostaulustamme:
name COUNT(P.id) SUM(P.price)---------- ----------- ------------Uolevi 2 9Maija 3 19
Ongelmamme syynä on ettei Aapelilla ole ostoksia, eli kun kyselymme valitsee yhdistelmän rivejä, ei ole olemassa riviä jossa Aapeli olisi läsnä. Ratkaisu on käyttää LEFT JOIN
seuraavasti:
SELECT C.name, COUNT(P.id), SUM(P.price)FROM Customers C LEFT JOIN Purchases OON C.id = O.customer_idLEFT JOIN Products PON P.id = O.product_idGROUP BY C.id;
Nyt saamme myös Aapelin tuloksiimme:
name COUNT(P.id) SUM(P.price)---------- ----------- ------------Uolevi 2 9Maija 3 19Aapeli 0
Koska aapelilla ei ole ostoksia, ostosten summa on NULL
.