Enemmän irti sql-tietokannoista - MikroPC
Enemmän irti sql-tietokannoista - MikroPC
Enemmän irti sql-tietokannoista - MikroPC
You also want an ePaper? Increase the reach of your titles
YUMPU automatically turns print PDFs into web optimized ePapers that Google loves.
HYVÄTNEUVOT<br />
HYVÄT NEUVOT: SQL-TIETOKANNAT<br />
TEKSTI: JONI MOILANEN<br />
Enemmän <strong>irti</strong><br />
<strong>sql</strong>-<strong>tietokannoista</strong><br />
SQL-TIETOKANTAKIELESTÄ KERTOVAN KAKSIOSAINEN SARJAN AVAUSOSA<br />
KESKITTYY HARVOIN KÄYTETTYIHIN, MUTTA TUIKI HYÖDYLLISIIN<br />
SQL:N PIIRTEISIIN. SEURAAVA OSA KESKITTYY KANNAN OPTIMOINTIVINKKEIHIN.<br />
Sql (Structured Query Language) on monipuolinen tiedon<br />
määrittelyyn, muokkaukseen ja hakemiseen kehitetty tietokantakieli.<br />
Sql:n yleisyys mahdollistaa <strong>sql</strong>-taitojen hyödyntämisen<br />
eri valmistajien tietokantapalvelimien kanssa, vaikkakin<br />
pieniä eroja toteutuksissa löytyy yhä.<br />
Tähän kaksiosaiseen sarjaan on koottu käyttökelpoisia vinkkejä <strong>sql</strong>-kannan<br />
tehokkaampaan hyödyntämiseen. Saadakseen sarjasta täyden hyödyn,<br />
on lukijalla oltava entuudestaan perustiedot relaatio<strong>tietokannoista</strong> ja<br />
<strong>sql</strong>:stä.<br />
SARAKKEIDEN YHDISTÄMINEN<br />
Sql:llä on helppo yhdistää sarakkeita suoraan SELECT-osiossa. Tämä on<br />
usein myös tehokkaampaa kuin itse sovelluksessa tehtynä.<br />
Relaationaalista tietokantaa suunniteltaessa ensin normalisoidaan tiedot,<br />
eli jaotellaan tauluihin ja sarakkeisiin. Ensimmäisessä normalisointisäännössä<br />
edellytetään, että sarake saa koostua vain yhdestä tiedosta. Esimerkiksi<br />
ihmisten nimet olisi hyvä pilkkoa erillisiin Etunimi ja Sukunimi -sarakkeisiin,<br />
jotta molempiin tietoihin pääsee tarvittaessa erikseen käsiksi.<br />
Käytännössä nimitietoja tarvitaan kuitenkin usein yhdessä ja yhdistäminen<br />
käy helposti jo tietokannassa. Huomaa, että välilyönti on lisättävä itse:<br />
SELECT Etunimi || ' ' || Sukunimi As 'Nimi'<br />
FROM Henkilot<br />
Kaksi putkimerkkiä ( || )on ANSI-SQL:n määrittelemä menetelmä sarakkeiden<br />
yhdistämiseen, jota kaikki tietokannat eivät noudata. Oracle ja PostgreSQL<br />
tukevat, mutta esimerkiksi Microsoftin Accessissa merkki on & ja<br />
SQL Serverissä yhdistämiseen on käytettävä plussaa (+) ja MySQL:ssä<br />
Concat() -funktiota.<br />
Samoin sarakkeita yhdistäessä olisi nimettävä tämä uusi, muodostettu<br />
sarake jollakin sarakesynonyymillä (esimerkin As 'Nimi'), jotta siihen olisi<br />
mahdollista viitata ohjelmallisesti ja jotta tulosjoukko olisi selkeämpi lukea.<br />
SQL JA LASKUTOIMITUKSET<br />
Myös laskutoimitukset antavat paljon mahdollisuuksia. Kaikki peruslaskutoimitukset<br />
onnistuvat suoraan sarakkeiden ja vakioiden välillä. Tietokannasta<br />
riippuen voi löytyä myös paljon hyödyllisiä funktioita, joita voi hyödyntää.<br />
Ajatellaan, että meillä on Tuotteet taulu, jossa on sarake Hinta, joka sisältää<br />
tuotteen hinnan euroissa ja haluamme tulosjoukossa myös sarakkeen,<br />
josta näkee hinnan markoissa. SQL:llä tämä onnistuu helposti:<br />
SELECT Hinta As 'Hinta (eur)', Hinta * 5.94573 As 'Hinta (mk)'<br />
FROM Tuotteet<br />
Desimaalierotin on aina piste. Tulosjoukkoon tulee tällä kyselyllä pitkiä<br />
desimaaleja, joten käytännössä voisi olla järkevää pyöristää tulos esimerkiksi<br />
Cast()-funktion avulla. Cast() on tarkoitettu tietotyyppien muuntamiseen<br />
ja sitä tarvitaan erityisesti silloin, kun haluaa yhdistellä erityyppisiä numeroita<br />
ja merkkijonoja sisältäviä sarakkeita keskenään.<br />
Seuraava hieman muokattu kysely pyöristää luvut luettavampaan muotoon:<br />
SELECT Hinta As 'Hinta (eur)', Cast(Hinta * 5.94573 As Decimal(15,2))<br />
As 'Hinta (mk)'<br />
FROM Tuotteet<br />
Muunnettava arvo tulee sulkeisiin ensimmäisenä, jonka jälkeen tulee As<br />
ja kohdetyyppi, joka on tässä tapauksessa Decimal. Decimal-tyyppiin muuntaessa<br />
tarvitaan tieto tarkkuudesta ja desimaalipilkun jälkeisistä numeroista.<br />
Ensimmäinen luku (15) kertoo siis, kuinka paljon numeroita luvussa voi<br />
olla yhteensä pilkun molemmin puolin ja jälkimmäinen luku (2) kertoo,<br />
kuinka monta numeroa halutaan pilkun oikealle puolelle.<br />
ULKOLIITOKSET<br />
Ulkoliitokset ovat erittäin käteviä silloin harvoin, kun niitä tarvitaan. Jotta<br />
ulkoliitokset ymmärtäisi, on hyvä palauttaa ensin mieleen, miten tavalliset<br />
(sisä-)liitokset oikeastaan toimivat.<br />
58 <strong>MikroPC</strong> 12 / 2002 W W W . M I K R O P C . N E T
ASIAKKAAT<br />
AsiakasID Etunimi Sukunimi<br />
10 Anssi Kestotilaaja<br />
20 Mikko Peesee<br />
30 Matti Meikäläinen<br />
Sanotaan, että meillä on tyypillinen suhde Tilaukset ja Asiakkaat -taulujen<br />
välillä. Tilaukset -taulussa oleva viiteavain (AsiakasID) viittaa Asiakkaat -<br />
taulun perusavaimeen (AsiakasID).<br />
Standardin mukaisella liitoksella nämä taulut voidaan yhdistää seuraavanlaisella<br />
<strong>sql</strong>-kyselyllä:<br />
SELECT *<br />
FROM Tilaukset t JOIN Asiakkaat a<br />
ON t.AsiakasID = a.AsiakasID<br />
Mikäli tietokanta ei tue tätä ANSI SQL-92:n määrittelemää liitosta tai<br />
olet muuten mieltynyt perinteiseen liitokseen, niin sama saadaan aikaan<br />
myös sen avulla:<br />
SELECT *<br />
FROM Tilaukset t, Asiakkaat a<br />
WHERE t.AsiakasID = a.AsiakasID<br />
TILAUKSET<br />
TilausID AsiakasID Tuote<br />
1001 10 <strong>MikroPC</strong><br />
1002 10 Talouselämä<br />
1003 20 Tietoviikko<br />
1004 40 <strong>MikroPC</strong><br />
Tämä liitos vastaa siis kysymykseen: "Hae kaikki tilaukset ja niihin liittyvät<br />
asiakkaat".<br />
SISÄLIITOS<br />
TilausID AsiakasID Tuote AsiakasID Etunimi Sukunimi<br />
1001 10 <strong>MikroPC</strong> 10 Anssi Kestotilaaja<br />
1002 10 Talouselämä 10 Anssi Kestotilaaja<br />
1003 20 Tietoviikko 20 Mikko Peesee<br />
▲ Sisäliitoksen palauttama tulosjoukko. Pois jäävät AsiakasID 30 ja<br />
TilausID 1004.<br />
SELECT *<br />
FROM Tilaukset t RIGHT OUTER JOIN Asiakkaat a<br />
ON t.AsiakasID = a.AsiakasID<br />
Vastaavasti RIGHTin tilalle laitettaisiin LEFT, mikäli haluttaisiin korostaa<br />
Tilaukset-taulua. Lisäksi on vielä FULL-ulkoliitos, joka korostaa molempia<br />
puolia. Perinteisessä liitoksessa laitetaan yhtäsuuruusmerkin vasemmalle<br />
tai oikealle puolelle tähti (*) kuvaamaan ulkoliitosta.<br />
Tulosjoukossa on NULL kaikissa niissä Tilaukset -taulun riveissä, joille ei<br />
löydy vastinparia Asiakkaat -taulusta. Läheskään aina ulkoliitos ei välttämättä<br />
palauta mitään sisäliitoksesta poikkeavaa, vaan se edellyttää vastinparittomia<br />
rivejä.<br />
ASIAKKAITA KOROSTAVA ULKOLIITOS<br />
TilausID AsiakasID Tuote AsiakasID Etunimi Sukunimi<br />
1001 10 <strong>MikroPC</strong> 10 Anssi Kestotilaaja<br />
1002 10 Talouselämä 10 Anssi Kestotilaaja<br />
1003 20 Tietoviikko 20 Mikko Peesee<br />
NULL NULL NULL 30 Matti Meikäläinen<br />
▲ Asiakas-taulua korostava ulkoliitos palauttaa myös ne asiakkaat,<br />
jotka eivät ole tilanneet mitään.<br />
TILAUKSIA KOROSTAVA ULKOLIITOS<br />
TilausID AsiakasID Tuote AsiakasID Etunimi Sukunimi<br />
1001 10 <strong>MikroPC</strong> 10 Anssi Kestotilaaja<br />
1002 10 Talouselämä 10 Anssi Kestotilaaja<br />
1003 20 Tietoviikko 20 Mikko Peesee<br />
1004 40 <strong>MikroPC</strong> NULL NULL NULL<br />
MOLEMPIA PUOLIA KOROSTAVA ULKOLIITOSKYSELY<br />
TilausID AsiakasID Tuote AsiakasID Etunimi Sukunimi<br />
1001 10 <strong>MikroPC</strong> 10 Anssi Kestotilaaja<br />
1002 10 Talouselämä 10 Anssi Kestotilaaja<br />
1003 20 Tietoviikko 20 Mikko Peesee<br />
1004 40 <strong>MikroPC</strong> NULL NULL NULL<br />
NULL NULL NULL 30 Matti Meikäläinen<br />
Entä jos joukossa on asiakkaita, jotka eivät ole vielä tilanneet tuotteita<br />
tai tilaukset-taulussa on tilauksia, joihin liitetyt asiakkaat ovat syystä tai<br />
toisesta poistettu?<br />
Yllä oleva kysely jättää nämä rivit kokonaan palauttamatta. Jos vaikka<br />
halutaan tulosjoukkoon mukaan myös ne asiakkaat, joilla ei ole tilausta, selvitään<br />
helpoiten ulkoliitoksella, vaikka muitakin keinoja siihen toki on.<br />
Ulkoliitokset on helpoin tehdä siten, että aloittaa ensin tavallisella sisäliitoksella,<br />
jonka muuttaa sen jälkeen oikeaksi tai vasemmaksi ulkoliitokseksi.<br />
Saadaksesi tulosjoukkoon myös ne asiakkaat, jotka eivät ole tehneet<br />
lainkaan tilauksia, on lisättävä RIGHT OUTER (tai pelkkä RIGHT) JOIN-sanan<br />
eteen. RIGHT sen vuoksi, sillä korostettava Asiakkaat-taulu on kyselyssä<br />
JOIN-sanan oikealla puolella. Lopullinen kysely näyttää siis tältä:<br />
Ulkoliitokset ovat siis käteviä myös rikkinäisten linkkien löytämiseksi,<br />
mikäli puutteellinen liiketoimintalogiikka on päästänyt sellaisia tietokantaan.<br />
Kun suodattaa WHERE-ehdolla kaikki NULLeja sisältävät rivit (WHERE<br />
sarake IS NULL), jää ylimääräiset tietueet helposti tarkasteltavaksi.<br />
Huomaa, että esimerkkikyselyssä on käytetty SELECT *-muotoa, jota ei<br />
tulisi tuotantokoodissa koskaan käyttää, vaan sarakkeet tulisi aina luetella,<br />
vaikka haluaisi kaikki ja rivitkin tulee käytännössä aina rajata.<br />
HELPPOA TILASTOINTIA<br />
Tietokannat sisältävät paljon markkinnoinnissakin hyödyllistä tietoa, mikäli<br />
sitä osaa kaivaa esille.<br />
GROUP BY auttaa ryhmittelemään tiedot annettujen ryhmittelyehtojen<br />
W W W . M I K R O P C . N E T <strong>MikroPC</strong> 12 / 2002 59
mukaisesti. GROUP BY:n avulla on helppoa vastata kysymyksiin, kuten<br />
"Kuinka paljon asiakkaita on kaupungeittain ja mikä on heidän ostoksiensa<br />
keskiarvo?".<br />
Ennen kuin GROUP BY:tä lähtee käyttämään, on syytä ymmärtää, että<br />
se toimii aivan erilailla, kuin tyypilliset SELECT-lauseet. SELECT ilman<br />
GROUP BY:tä palauttaa rivejä taulusta tai tauluista sellaisenaan.<br />
GROUP BY:tä hyödyntävä kysely puolestaan yhdistää useamman rivin<br />
annetulla, yhdistävällä tekijällä, joka voisi olla maa, kaupunki tai viiteavain.<br />
Ryhmittelyn jälkeen kysely kertoo esimerkiksi ryhmään kuuluvien rivien<br />
määrän tai summan.<br />
SELECTin jälkeen valitaan sarake, jonka mukaan tiedot halutaan ryhmitellä<br />
tarkastelua varten, jonka jälkeen koostefunktiolla kerrotaan, millä periaatteella<br />
sarakkeen tiedot niputetaan. Koostefunktiot on listattu oheisessa<br />
taulukossa. Osa koostefunktiosta hyväksyy myös DISTINCTin, jolloin huomioidaan<br />
vain erilaisten rivien arvot.<br />
Tulos olisi hieman selkeämpi, jos kategorioissa näkyisi numeroiden sijaan<br />
itse kategorioiden nimet, mutta se on ratkaistu yksinkertaisella liitoksella<br />
ja vaihtamalla ryhmittelysarakkeeksi CategoryName:<br />
SELECT CategoryName, Count(*) As 'Määrä', Sum(UnitPrice) As 'Summa',<br />
Avg(UnitPrice) As 'Keskiarvo'<br />
FROM Products p JOIN Categories c ON p.CategoryID = c.CategoryID<br />
GROUP BY CategoryName<br />
Lopputuloksena on hieman luettavampi tulosjoukko:<br />
RYHMITELTY TULOSJOUKKO<br />
SQL-FUNKTIOITA<br />
Koostefunktio/syntaksi<br />
Sum(ALL | DISTINCT sarake)<br />
AVG(ALL | DISTINCT sarake)<br />
COUNT(ALL | DISTINCT sarake)<br />
COUNT(*)<br />
Min (sarake)<br />
Max (sarake)<br />
Merkitys<br />
Arvojen summa.<br />
Keskiarvo.<br />
Arvojen määrä (NULLeja ei huomioida).<br />
Rivien määrä (laskee mukaan myös<br />
NULLit).<br />
Pienin arvo.<br />
Suurin arvo.<br />
Esimerkiksi seuraava kysely kertoo tuotteet-taulun (Products) tuotteiden<br />
määrän kategorioittain ja laskee niiden hintojen summan ja keskiarvon.<br />
▲ Liitokset ryhmittelyiden kanssa selkeyttävät kyselyä.<br />
--- Esimerkki SQL Serverin NorthWind-esimerkkitietokannalla:<br />
SELECT CategoryID, Count(*) As 'Määrä', Sum(UnitPrice) As 'Summa', Avg(UnitPrice) As 'Keskiarvo'<br />
FROM Products<br />
GROUP BY CategoryID<br />
Ryhmittelemme tuotteet kategorian perusteella ja kohdistamme yhdistettyihin<br />
riveihin erilaisia koostefunktioita. Tuloksena on tämän näköinen<br />
tulosjoukko:<br />
GROUP BY -TULOSJOUKKO<br />
Huomaa, ettei sarakkeita voi tavallisen<br />
SELECT-lauseen tavoin lisätä mielin määrin,<br />
ellei niitä käytä ryhmittelyyn. Esimerkiksi<br />
myynnit voi ryhmitellä ensin paikkakunnittain<br />
ja sitten tuotteittain.<br />
Ryhmittelyyn sopivia sarakkeita etsiessä<br />
kannattaa hakea sellaisia, joissa toistuu<br />
usein sama arvo, kuten kaupunki tai toiseen tauluun viittaava viiteavain, kuten<br />
esimerkin CategoryID.<br />
Mikäli GROUP BY:tä hyödyntävässä kyselyssä käyttää WHERE:ä, karsitaan<br />
suodatetut rivit ennen ryhmittelyä ja tulosjoukon näkymistä. Entä jos<br />
haluaa karsia vielä rivejä ryhmittelyn jälkeen? Se onnistuu HAVING-lauseella,<br />
jota käytetään, kuin WHEREä.<br />
Esimerkiksi edellisen kyselyn perään voisi lisätä:<br />
HAVING SUM(UnitPrice) > 400<br />
Koska vain Beverages-kategoria (juomat) täyttää tämän ehdon, on se ainoa<br />
rivi tulosjoukossa. ■<br />
▲ GROUP BY -esimerkin palauttama tulosjoukko kertoo tietoa<br />
tuotteiden määrästä kategorioittain ja hintojen summista ja<br />
keskiarvoista.<br />
60 <strong>MikroPC</strong> 12 / 2002 W W W . M I K R O P C . N E T