Normaalimuodot

Aiemmassa luvussa todettiin, että normalisoidun relaation rakenne vähentää turhaa toisteisuutta sekä auttaa välttämään ristiriitaisuuksien syntymistä datassa. Normalisointi ei kuitenkaan ole kaksiarvoinen käsite: relaatio ei ole vain joko normalisoimaton tai normalisoitu. Mahdollisia poikkeamia ja toisteisuuksia on erilaisia, ja joskus eri tapauksissa voidaan hyväksyä, että jokin relaation rakenne on enemmän "optimoitu" kuin toinen.

Relaatioiden normalisointi määritellään ja suoritetaan tasoittain. Normaalimuoto on joukko relaation rakenteeseen liittyviä vaatimuksia, joita relaatio voi täyttää. Vaatimukset esitellään yleensä funktionaalisten riippuvuuksien kautta. Relaatio, joka täyttää tietyn normaalimuodon vaatimukset, saa normaalimuodon mukaisia takeita poikkeamien sietokyvystä tai olemassaolosta. Normaalimuotoja on useita; tässä luvussa esitellään seuraavat normaalimuodot niiden vahvuusjärjestyksessä:

  1. Ensimmäinen normaalimuoto (1NF)
  2. Toinen normaalimuoto (2NF)
  3. Kolmas normaalimuoto (3NF)
  4. Boyce/Codd-normaalimuoto (BCNF)
  5. Neljäs normaalimuoto (4NF)

Korkeammat normaalimuodot tarjoavat vahvempia takeita poikkeamien puuttumisesta tai sietokyvystä. Ensimmäinen normaalimuoto on perusmuoto, jonka jokaisen relaatiomallin mukaisen tietokannan tulee täyttää. Seuraavat muodot määritellään aina edeltävien muotojen kautta: relaatio joka täyttää 3NF täyttää myös 2NF, ja relaatio, joka täyttää 2NF täyttää myös 1NF.

Kuvio: Normaalimuotojen nimet ja niiden suhde. Kaikki vahvemmassa normaalimuodossa olevat relaatiot täyttävät aina myös heikompia normaalimuotoja (eli 1NF<2NF<3NF<BCNF<4NF).
Kuvio: Normaalimuotojen nimet ja niiden suhde. Kaikki vahvemmassa normaalimuodossa olevat relaatiot täyttävät aina myös heikompia normaalimuotoja (eli 1NF<2NF<3NF<BCNF<4NF).

Normalisointi on aina relaatiokohtainen prosessi, eikä kaikkien tietokannan relaatioiden ei tarvitse olla samassa normaalimuodossa. Normalisointi myös yleensä suoritetaan tasoittain: ensin varmistetaan 1NF vaatimukset, sitten 2NF, jne. kunnes relaatio on normalisoitu halutulle tasolle. Jos relaatio ei täytä normaalimuodon vaatimuksia, relaatio voi korjata esimerkiksi jakamalla relaatio kahdeksi tai useammaksi relaatioksi. Jaot ja muutokset tulee kuitenkin tehdä niin, että kohdealueen dataa ei katoa.

Seuraavaksi esitellään neljän ensimmäisen normaalimuotojen vaatimuksia sekä ohjeet relaation saattamiseksi niihin. Mainittakoon, että tässä materiaalissa annetut vaatimukset eivät ole formaaleja, vaan ovat eri kirjailijoiden muotoilujen ([34], [13], [21]) perusteella tehtyjä yhteenvetoja.

Ennen jokaista muotoa esitellään esimerkkitaulu ja pohditaan, millaisia poikkeamia taulussa voi syntyä.

Huomautus

Luvussa 5.1 esiteltiin päättelysääntöjä, joiden perusteella funktionaalisia riippuvuuksia voi yhdistää. Riippuvuuksia, joita voi päätellä päättelysäännöillä ovat triviaaleja. Jos riippuvuutta ei voi keksiä päättelysäännöllä, vaan taulun tai kohdealueen perusteella, se on epätriviaali.

Tässä luvussa esitetyt vaatimukset koskevat aina epätriviaaleja riippuvuuksia ellei toisin sanota.

Ensimmäinen normaalimuoto

Esimerkki

Webbikauppa.com on erilaisia kodintarvikkeita ja sähkölaitteita myyvä yritys. Yritykselle perustetaan verkkosivu, jonka kautta asiakkaat voivat ostaa tuotteita.

Yritys asetti seuraavia vaatimuksia tietokannalle:

  • Asiakkaat, jotka haluavat osaa tavaroita verkkosivulta, tekevät itselleen ensin käyttäjätunnuksen.
  • Sen jälkeen asiakkaat voivat lisätä tavaroita ostoskoriin ja ostaa niitä.
  • Ostoskori tallennetaan tietokantaan, jotta käyttäjä voi helposti katsella ostoskoriaan eri laitteilla (tietokone, puhelin) ja jotta käyttäjä voisi halutessaan käydä tilaamassa tuotteet suoraan kivijalkaliikkeestä.

Sovellusta ja tietokantaa tekevä koodari päättyi tekemään taulun KÄYTTÄJÄ, joka sisältää käyttäjän tiedot ja hänen ostoskorinsa. Tauluun on saatu seuraavaa mallidataa:

käyttäjätunnus ostoskori
matti123 2 kpl tuote-00000001, 3 kpl tuote-00000002
hilda02 12 kpl tuote-00000003
asko_96 3 kpl tuote-00000004, 1 kpl tuote-00000002, 8 kpl tuote-00000003, 1 kpl tuote-00000001

Taulua luodessa taululle ei asetettu perusavainta. Tuotteet kuvataan tuotteen EAN-viivakoodilla (muotoa tuote-12345678), joiden tiedot löytyvät muualta.

Harjoittele

Katsele yllä olevaa taulua ja mallidataa. Pohdi, millaisia poikkeamia tai mahdollista toisteisuutta voi syntyä. Muista, että poikkeama on tiedon lisäyksestä, muokkauksesta tai poistosta johtuva syntyvä ristiriita taulun tiedoissa.

Mitä muita mahdollisia ongelmia voi syntyä, kun yllä olevaa taulua yrittää käyttää?

Kun olet pohtinut asiaa, voit lukea selityksen alla:

Avaa selitys taulun rakenteen ongelmista

Ensimmäinen normaalimuoto liittyy siis Luvussa 3.1 esiteltyihin relaatiomallin perusominaisuuksiin. Esimerkin mukaisiin ongelmiin voidaan päästä ainakin SQL-spesifikaation mukaisissa tietokannoissa, sillä spesifikaatio esimerkiksi ei vaadi, että taululla olisi perusavainta. Lisäksi monen arvon säilyttäminen yhdessä sarakkeessa johtaa helposti tietokannan käytön ongelmiin.

Ensimmäisen normaalimuoto käytännössä vaatii, että taulu on suunniteltu relaatiomallin sääntöjen mukaisesti.

Määritelmä

Relaatio on 1NF:ssä, jos se täyttää relaation vaatimukset:

  • Relaatiolle tulee olla valittuna sopiva perusavain.
  • Monikoiden (eli rivien) järjestyksellä ei saa olla merkitystä tiedon merkityksen kannalta.
  • Jokaisella attribuutilla (eli sarakkeella) tulee olla yksi selkeä tietotyyppi. Erityyppisen datan sekoittaminen samassa attribuutista on kielletty.
  • Jokainen attribuutti on atominen, eli ei sisällä montaa arvoa.

Jos relaatio ei täytä 1NF:ää, korjausvaiheet ovat:

  1. Jaa moniarvoiset attribuutit niin, että jokaiselle arvolle tulee oma rivi.
  2. Tarvittaessa lisää attribuutteja niin, että jokainen on selkeää tietotyyppiä.
  3. Valitse sopiva perusavain.

Esimerkki

Nyt selvästi yllä oleva esimerkki ei täytä 1NF.

Korjaukseksi ostoskori tulee jakaa kahteen attribuuttiin: ostoskori_tuote (EAN-koodi) ja tuote_määrä (numero). Lisäksi ostoskorin jokainen tuote tallennetaan omalle riville. Saadaan tulokseksi:

käyttäjätunnus ostoskori_tuote tuote_määrä
matti123 tuote-00000001 2
matti123 tuote-00000002 3
hilda02 tuote-00000003 12
asko_96 tuote-00000004 3
asko_96 tuote-00000002 1
asko_96 tuote-00000003 8
asko_96 tuote-00000001 1

Lisäksi valitaan perusavaimeksi {käyttäjätunnus, ostoskori_tuote}, jolloin jokaisella käyttäjällä voi olla usea tuote ostoskorissa, mutta samalle tuotteelle ei voi vahingossa lisätä toista riviä (vaan pitää päivittää lukumäärä). Lisäksi jos tuotteen tunnuksesta tehtäisiin viiteavain tauluun, joka sisältää kaikkien tuotteiden tieto, käyttäjille ei voisi edes vahingossa lisätä tuotteita, joita ei ole olemassa. Lisäksi RDBMS silloin automaattisesti hoitaisi mahdolliset EAN-koodien muutokset tai tuotteiden poistot.

Nyt taulu on 1NF:ssä. Huomaa, että 1NF ei ratkaise kaikkia poikkeamia: esimerkiksi jos tyhjentää hilda02:n ostoskorin poistamalla rivin, hilda02 häviää koko tietokannasta.

Toinen normaalimuoto

Esimerkki

Jatketaan yllä olevaa esimerkkiä. Yrityksen Webbikauppa.com verkkosivuja on kehitetty eteenpäin. Yritys haluaisi lisätä verkkosivuille erilaisia asiakastyyppejä, joilla olisi erilaisia bonuksia ja alennuksia. Yritys miettii alkuun kolmea asiakastyyppiä: "Perusasiakas", "Bonusasiakas" sekä "VIP-asiakas".

Sovellusta ja tietokantaa tekevä koodari päätti hetken pohdittuaan yksinkertaisesti laajentaa vaatimuksen perusteella KÄYTTÄJÄ-taulun lisäämällä siihen sarakkeen käyttajatyyppi. Tuloksena syntyi seuraava päivitetty taulu (perusavainsarakkeet alleviivattu):

käyttäjätunnus ostoskori_tuote tuote_määrä käyttäjätyyppi
matti123 tuote-00000001 2 Perusasiakas
matti123 tuote-00000002 3 Perusasiakas
hilda02 tuote-00000003 12 VIP-asiakas
asko_96 tuote-00000004 3 Bonusasiakas
asko_96 tuote-00000002 1 Bonusasiakas
asko_96 tuote-00000003 8 Bonusasiakas
asko_96 tuote-00000001 1 Bonusasiakas

Harjoittele

Katsele yllä olevaa taulua ja mallidataa. Pohdi, millaisia poikkeamia tai mahdollista toisteisuutta voi syntyä. Muista, että poikkeama on tiedon lisäyksestä, muokkauksesta tai poistosta johtuva syntyvä ristiriita taulun tiedoissa.

Mitä muita mahdollisia ongelmia voi syntyä, kun yllä olevaa taulua yrittää käyttää?

Kun olet pohtinut asiaa, voit lukea selityksen alla:

Avaa selitys taulun rakenteen ongelmista

Toisen normaalimuotoon tarvitaankin jo funktionaalisia riippuvuuksia. Tarkemmin, 2NF liittyy siihen, miten relaatiossa olevien funktionaalisten riippuvuuksien määräävä joukko ja relaation perusavain liittyvät toisiinsa.

Määritelmä

Relaatio on 2NF:ssä, jos se täyttää seuraavat vaatimukset:

  1. Relaatio on 1NF:ssä.
  2. Mikään relaation perusavaimeen kuulumaton attribuutti ei riipu ainoastaan perusavaimen osasta, vaan koko perusavaimesta.

Toisin sanoen, jos on voimassa funktionaalinen riippuvuus {X} -> {Y}, jossa X:ssä on relaation perusavaimeen kuuluvat attribuutit ja Y ei kuulu perusavaimeen, niin X:stä ei voi ottaa pois perusavaimeen kuuluvia attribuutteja niin, että funktionaalinen riippuvuus säilyisi. Tällaista funktionaalista riippuvuutta kutsutaan täydeksi funktionaaliseksi riippuvuudeksi.

Huomaa, että 2NF:ssä tulee tarkastaa vain ne funktionaaliset riippuvuudet, joiden määräävässä joukossa on mukana kaikki perusavaimeen kuuluvat attribuutit. Sellaiset riippuvuudet, jonka määräävässä joukossa on perusavaimeen kuulumattomia attribuutteja, eivät riko 2NF:ää. Seurauksena, jos relaation perusavaimessa on vain yksi attribuutti, relaatio on aina 2NF:ssä.

Toinen tapa ajatella 2NF on: perusavaimen ja perusavaimeen kuulumattomien attribuuttien välisen funktionaalisen riippuvuuden tulee olla epätriviaali (eli ei voi johtaa päättelysäännöistä). Erityisesti perusavainta ei voida jakaa osiin niin, että jotain perusavaimeen ja muiden attribuuttien riippuvuuksia säilyisi.

Jos relaatio ei täytä 2NF, sen saa korjattua tekemälle jokaiselle 2NF:ää rikkovalle funktionaaliselle riippuvuudelle oman relaation, jossa on funktionaaliseen riippuvuuden osapuolina olevat attribuutit. Määräävästä joukosta tulee uuden relaation perusavain.

Esimerkki

Jatketaan yllä olevaa esimerkkiä. Mietitään läpi, mitä kaikkia funktionaalisia riippuvuuksia taulusta löytyy ja miten ne täyttää 2NF:

  • {käyttäjätunnus, ostoskori_tuote} -> {tuote_määrä}
    • "Jos tiedetään tietyn käyttäjän tunnus ja tuote, tiedetään, kuinka monta kappaletta kyseinen käyttäjä on tilannut kyseistä tuotetta"
    • 2NF on OK: määräävä attribuuttijoukko on {käyttäjätunnus, ostoskori_tuote}, joka on sama kuin relaation perusavain JA jos siitä otettaisiin jompikumpi attribuuteista pois, riippuvuus ei enää voisi päteä.
  • {käyttäjätunnus} -> {käyttäjätyyppi}
    • "Jos tiedetään tietyn käyttäjän tunnus, tiedetään tämän käyttäjän käyttäjätyyppi"
    • 2NF FAIL: Määräävä attribuuttijoukko on {käyttäjätunnus}, joka on vain osa relaation perusavainta eikä koko perusavain. Toisin sanoin riippuvuus {käyttäjätunnus, ostoskori_tuote} -> {käyttäjätyyppi} on triviaali.

Jotta 2NF täyttyy, pitää relaatio jakaa. Tehdään kahdelle funktionaaliselle riippuvuudelle omat relaatiot:

KÄYTTÄJÄ_OSTOSKORI(käyttäjätunnus, ostoskori_tuote, tuote_määrä), joka vastaa funktionaalista riippuvuutta {käyttäjätunnus, ostoskori_tuote} -> {tuote_määrä}:

käyttäjätunnus ostoskori_tuote tuote_määrä
matti123 tuote-00000001 2
matti123 tuote-00000002 3
hilda02 tuote-00000003 12
asko_96 tuote-00000004 3
asko_96 tuote-00000002 1
asko_96 tuote-00000003 8
asko_96 tuote-00000001 1

KÄYTTÄJÄ(käyttäjätunnus, käyttäjätyyppi), joka vastaa funktionaalista riippuvuutta {käyttäjätunnus} -> {käyttäjätyyppi}:

käyttäjätunnus käyttäjätyyppi
matti123 Perusasiakas
hilda02 VIP-asiakas
asko_96 Bonusasiakas

Nyt saatiin kaksi taulua, joissa turha toisteisuus on poistettu sekä ratkaistu poikkeamiin liittyviä murheita:

  • Käyttäjille on oma taulu, joten nyt käyttäjä ei häviä tietokannasta vaikka hänellä ei olisi ostoskoria.
  • Vastaavasti käyttäjiä voidaan lisätä tietokantaan ilman että niillä olisi mitään ostoskorissa.
  • Jokaisella asiakkaalla käyttäjätyyppi ei toistu turhaan, eikä ole mahdollisuutta ristiriitaan.
  • Jos vielä määritetään sopivat viiteavaimet (esim. KÄYTTÄJÄ_OSTOSKORI.käyttäjätunnus -> KÄYTTÄJÄ.käyttäjätunnus), saataisiin RDBMS:n takaaman viite-eheyden ja sen hienoudet: ostoskoreja ei voisi lisätä olemattomille käyttäjille, käyttäjän tunnuksen muutos tai poisto saisi RDBMS:n automaattisesti käsittelemään ostoskorin muutokset tai poistot.

Periaatteessa jotain pieniä vaaran paikkoja vielä voi löytyä. Esimerkiksi on mahdollista, että tietokannassa olisi vain perusasiakkaita, jolloin muista käyttäjätyypeistä ei olisi tietoa. Tosin kohdealueesta riippuen tämä ei välttämättä ole kriittinen asia. Jos se on, seuraava normaalimuoto ottaa kantaa tähänkin tapaukseen.

Kolmas normaalimuoto

Esimerkki

Webbikauppa.com-yrityksen verkkosivujen kehitys jatkuu. Yritys haluaisi selkeyttää käyttäjätyyppien ansaitsemista lisäämällä sovellukseen numeerisen bonustason. Bonustaso on numero 1-9, ja bonustaso nousee aina, kun asiakas tilaa tuotteita. Jatkossa käyttäjän käyttäjätyyppi määräytyisi bonustason perusteella seuraavasti:

  • Bonustaso 1-3: Peruskäyttäjä
  • Bonustaso 4-6: Bonusasiakas
  • Bonustaso 7-9: VIP-asiakas

Sovellusta ja tietokantaa tekevä koodari toteaa, että käyttäjän bonustaso on selvästi käyttäjän ominaisuus kuten käyttäjätyyppi, ja siten se kuuluu KÄYTTÄJÄ-tauluun. Siispä koodari lisää tauluun sarakkeen bonustaso. Taulu näyttää muutoksen jälkeen seuraavalta:

käyttäjätunnus käyttäjätyyppi bonustaso
matti123 Perusasiakas 3
hilda02 VIP-asiakas 8
asko_96 Bonusasiakas 5

Tällä kertaa koodari on valistunut normalisoinnista. Koodari toteaa, että taulu on selvästi edelleen 1NF:ssä, sillä perusavain ei muuttunut, bonustaso-sarakkeella on selkeä tietotyyppi (numero), eikä uusi sarake sisällä moniarvoisia arvoja.

Koodari vielä miettii läpi funktionaaliset riippuvuudet. Hän toteaa, että perusavaimesta tulee funktionaalinen riippuvuus

{käyttäjätunnus} -> {käyttäjätyyppi, bonustaso}

Tämä ei riko 2NF, sillä määräävä attribuuttijoukko on koko perusavain eikä sitä voi jakaa osiin enempää. Lisäksi koodari toteaa, että nyt bonustason perusteella voidaan päätellä yksikäsitteisesti käyttäjän tyyppi, eli syntyi uusi funktionaalinen riippuvuus:

{bonustaso} -> {käyttäjätyyppi}

Tämäkään ei kuitenkaan riko 2NF:ää, sillä määräävä attribuuttijoukko {bonustaso} ei ole ollenkaan perusavaimessa. 2NF:n vaatimukset koskevat vain niitä funktionaalisia riippuvuuksia, joissa määräävässä attribuuttijoukossa on perusavaimeen kuuluvia attribuutteja.

Harjoittele

Katsele yllä olevaa taulua ja mallidataa. Pohdi, millaisia poikkeamia tai mahdollista toisteisuutta voi syntyä. Muista, että poikkeama on tiedon lisäyksestä, muokkauksesta tai poistosta johtuva syntyvä ristiriita taulun tiedoissa.

Mitä muita mahdollisia ongelmia voi syntyä, kun yllä olevaa taulua yrittää käyttää?

Kun olet pohtinut asiaa, voit lukea selityksen alla:

Avaa selitys taulun rakenteen ongelmista

Kolmannen normaalimuodon ytimessä ovat ns. transitiiviset funktionaaliset riippuvuudet, eli riippuvuudet, jotka riippuvat perusavaimesta epäsuorasti toisen attribuutin kautta. Yllä olevassa esimerkissä käyttäjätyyppi riippuu bonustasosta ({bonustaso} -> {käyttäjätyyppi}), joka puolestaan riippuu perusavaimesta ({käyttäjätunnus} -> {bonustaso}). Eli voidaan sanoa, että käyttäjätyyppi riippuu transitiivisesti käyttäjätunnuksesta; se voidaan merkitä seuraavasti:

{käyttäjätunnus} -> {bonustaso} -> {käyttäjätyyppi}

Määritelmä

Relaatio on 3NF:ssä, jos kaikki seuraavat vaatimukset pätevät:

  1. Relaatio on 2NF:ssä.
  2. Kaikki relaation perusavaimeen kuulumattomat attribuutit riippuvat vain, ainoastaan ja kokonaan perusavaimesta eikä mistään muusta attribuutista.

Poikkeus: Jos funktionaalisen riippuvuuden määräävä attribuuttijoukko on relaation avainehdokas (eli se yksilöi kaikki relaation attribuutit), se ei riko 3NF:ää.

Toisin sanoen 3NF vaatii, että jokainen perusavaimeen kuulumaton attribuutti riippuu vain perusavaimesta. Ainoat funktionaaliset riippuvuudet, jotka 3NF käytännössä sallii, on avainehdokkaista johtuvat riippuvuudet.

Huomaa erityisesti, että vaikka 2NF:n vaatimukset automaattisesti täyttyvät relaatioissa, joissa on vain yhden attribuutin perusavain, 3NF:n vaatimus ei välttämättä päde.

3NF:n vaatimus voi halutessaan muotoilla myös transitiivisen riippuvuuden määritelmän kautta: relaatio on 3NF:ssä, jos mikään relaatioon perusavaimeen kuulumaton attribuutti ei ole transitiivisesti riippuvainen relaation perusavaimesta.

Jos relaatio ei ole 3NF:ssä, se korjataan käytännössä tekemällä rikkoville funktionaalisille riippuvuuksille omat relaatiot. Taas, tässä tapauksessa voi käyttää omaa harkintaa sen mukaan, kuinka monta relaatiota tekee.

Esimerkki

Palataan yllä olevaan esimerkkiin ja korjataan se 3NF:ää täyttäväksi.

Kuten todettiin, uudella taululla KÄYTTÄJÄ(käyttäjätunnus, käyttäjätyyppi, bonustaso) on kaksi funktionaalista riippuvuutta:

  • {käyttäjätunnus} -> {bonustaso}
    • "Jos tiedetään käyttäjän tunnus, tiedetään hänen bonustaso."
    • 3NF OK: Määräävä joukko on {käyttäjätunnus}, joka on perusavain.
  • {bonustaso} -> {käyttäjätyyppi}
    • "Jos tiedetään käyttäjän bonustaso, tiedetään hänen käyttäjätyyppi."
    • 3NF FAIL: Määräävä joukko on {bonustaso}, joka ei ole relaation perusavain.

Tässä perusavaimesta johtuva riippuvuus {käyttäjätunnus} -> {bonustaso, käyttäjätyyppi} on triviaali, sillä sen voi johtaa transitiivisuussäännöstä.

Ratkaisuna siis poistetaan KÄYTTÄJÄ-taulusta sarake käyttäjätyyppi, jolloin lopputulos on:

käyttäjätunnus bonustaso
matti123 3
hilda02 8
asko_96 5

Lisäksi luodaan rikkovalle funktionaaliselle riippuvuudelle {bonustaso} -> {käyttäjätyyppi} oma taulu BONUSTASO_TYYPPI ja syötetään sinne bonustasojen ja käyttäjätyyppien väliset yhteydet:

bonustaso käyttäjätyyppi
1 Perusasiakas
2 Perusasiakas
3 Perusasiakas
4 Bonusasiakas
5 Bonusasiakas
6 Bonusasiakas
7 VIP-asiakas
8 VIP-asiakas
9 VIP-asiakas

Tällä ratkaisulla oikeastaan kaikki murheet korjautuu:

  • Kun käyttäjän bonustaso nousee ei tarvitse samalla päivittää käyttäjätyyppiä, sillä sen saa pääteltyä eri taulusta.
  • On helppoa muuttaa bonustasojen sääntöjä: on helppoa lisätä uusia bonustasoja tai muuttaa käyttäjätyyppien nimityksiä muokkaamalla vain BONUSTASO_TYYPPI-taulun rivejä
  • Bonustasosta voidaan tehdä nyt viiteavain KÄYTTÄJÄ.bonustaso -> BONUSTASO_TYYPPI.bonustaso, jolloin saadaan kaikki RDBMS:n tarjoamat viite-eheystarkastukset: käyttäjän bonustaso ei voisi enää vahingossa asettaa muuksi numeroksi kuin 1-9; bonustason poisto ja muokkaus välittyy automaattisesti käyttäjiin, joita muutos koskee.
  • Plussana on, että tietokantaan on koodattu kaikki mahdolliset käyttäjätyypit. On tosi helppoa selvittää, mitä eri käyttäjätyyppejä on olemassa.

Boyce/Codd-normaalimuoto

Boyce/Codd-normaalimuoto on vain pieni muunnelma 3NF:stä, joka karsii pois lopullisetkin "puutteelliset" riippuvuudet. Esitellään heti alkuun BCNF:n määritelmä vertailuksi:

Määritelmä

Relaatio on BCNF:ssä, jos seuraavat vaatimukset pätevät:

  1. Relaatio on 3NF:ssä.
  2. Kaikki relaation attribuutit riippuvat vain, ainoastaan ja kokonaan jostain avainehdokkaasta eikä mistään muusta attribuutista.

Toisin sanoen, funktionaaliset riippuvuudet, jossa määräävä joukko ei ole relaation jokin avainehdokas, on kielletty.

Huomaa vaatimusero 3NF:n ja BCNF:n välillä:

  • 3NF: "Kaikki relaation perusavaimeen kuulumattomat attribuutit riippuvat vain, ainoastaan ja kokonaan perusavaimesta eikä mistään muusta attribuutista."
  • BCNF: "Kaikki relaation attribuutit riippuvat vain, ainoastaan ja kokonaan jostain avainehdokkaasta eikä mistään muusta attribuutista."

Huomaa, että BCNF vaatii, että relaatiosta tunnistetaan kaikki avainehdokkaat (eli attribuuttijoukot, jotka yksilöivät relaation kaikki muut attribuutit). Muista, että perusavain on myös avainehdokas. Toisaalta BCNF:n määritelmä on hieman yleisempi ja voi olla helpompi muistaa.

Käytännössä ero BCNF:n ja 3NF:n välillä on hyvin pieni. Hyvin usein 3NF:ssä olevat relaatiot ovat myös BCNF:ssä. Yleensä BCNF menee rikki tapauksissa, jossa relaatiossa on useampi "päällekkäinen" avainehdokas, eli jotkut attribuutit esiintyvät useassa eri avainehdokkaassa.

Katsotaan kuitenkin eräs esimerkki, jossa 3NF pätee, mutta BCNF ei.

Esimerkki

Eräs tapaus, jossa BCNF ei välttämättä päde, on resurssien tai aikojen samanaikaisuuteen liittyvissä vaatimuksissa.

Webbikauppa.com-yritys haluaa tarjota isoille yritysasiakkaille mahdollisuuden varata aikoja konsultointiin (esim. hintavertailu, isojen ostosten suunnittelu, yms). Konsultointi pidetään tiettynä päivänä ja kellonaikana kaupan kivijalkaliikkeessä ja sitä varten varataan huone ja myyjä. Myyjä varaa huoneensa koko päiväksi. Eri päivinä samalla myyjällä voi olla eri huone, sillä huoneet voivat olla varattu.
Asiakkaalle varataan vain yksi konsultointi samalle päivälle, jotta kaikille asiakkaille olisi tilaa.

Konsultointeja varten tietokantaan tulee lisätä taulu, jossa pidetään kirjaa konsultoinnin tiedoista (aika, asiakas, kuka myyjä hoitaa, huone).

Vaatimusten perusteella koodari loi seuraavan taulun:

käyttäjätunnus päivä kellonaika myyjätunnus huonenro
matti123 2025-01-30 12:00 myyjä01 huone101
matti123 2025-02-10 12:00 myyjä02 huone102
hilda06 2025-01-30 15:00 myyjä01 huone101
asko_96 2025-03-01 13:00 myyjä03 huone100

Taulun ja vaatimusten perusteella koodari päätteli seuraavat funktionaaliset riippuvuudet:

  • {käyttäjätunnus, päivä} -> {kellonaika, myyjätunnus, huonenro}
    • "Jos tiedetään käyttäjän tunnus ja konsultoinnin päivä, tiedetään konsultointiin osallistuvan myyjän, varatun huoneen ja konsultoinnin kellonajan"
    • Tämän perusteella {käyttäjätunnus, päivä} on avainehdokas, se on myös tässä tapauksessa perusavain
  • {myyjätunnus, päivä, kellonaika} -> {käyttäjätunnus, huonenro}
    • "Jos tiedetään myyjän tunnus ja konsultoinnin päivämäärä, tiedetään konsultointiin tulevan asiakkaan ja varatun huoneen"
    • Tämä pätee, sillä myyjä voi olla samaan aikaan vain yhdessä konsultoinnissa
    • Huom: tämän perusteella {myyjätunnus, päivämäärä, kellonaika} on myös avainehdokas
  • {huonenro, päivä, kellonaika} -> {käyttäjätunnus, myyjätunnus}
    • "Jos tiedetään konsultoinnin päivämäärä ja huone, tiedetään, mikä asiakas ja myyjä tulevat paikalle"
    • Tämä pätee, sillä solussa voi olla samaan aikaan vain yksi konsultointi, eli solun numerosta ja päivämäärästä tiedetään, ketkä konsultointiin osallistuu
    • Huom: tämän perusteella {huonenro, päivä, kellonaika} on myös avainehdokas
  • {myyjätunnus, päivä} -> {huonenro}
    • "Jos tiedetään myyjän tunnus ja päivä, tiedetään, minkä huoneen hän on varannut päiväksi"
    • Huom: {myyjätunnus, päivä} ei ole avainehdokas! Myyjän tunnuksella ja päivällä voidaan vaatimusten perusteella tietää vain myyjän varaaman huoneen numero, muttei sitä, kuka asiakas on tulossa.

Huomaa, että 1NF, 2NF ja 3NF pätevät:

  • 1NF on OK, sillä perusavain on valittu, attribuuteilla on selkeä tietotyyppi eikä moniarvoisia attribuutteja ole.
  • 2NF on OK, sillä riippuvuus {käyttäjätunnus, päivämäärä} -> {kellonaika, myyjätunnus, huonenro} on täysi.
    • kellonaika riippuu täysin perusavaimesta {käyttäjätunnus, päivämäärä} eikä vaan sen osasta; ilman käyttäjätunnusta tai päivämäärää ei tiedetä yksikäsitteisesti, mihin aikaan konsultointi voisi olla
    • myyjätunnus riippuu täysin perusavaimesta {käyttäjätunnus, päivämäärä} eikä vain sen osasta; ilman käyttäjätunnusta tai päivämäärää ei tiedetä, mikä myyjä osallistuu konsultointiin
    • huonenro riippuu täysin perusavaimesta {käyttäjätunnus, päivämäärä} eikä vaan sen osasta; ilman käyttäjätunnusta tai päivämäärää ei tiedetä, missä solussa konsultointi on
  • 3NF on OK poikkeussäännön takia: kaikissa funktionaalisissa riippuvuuksissa määräävä joukko on joko perusavain tai avainehdokas.

Nyt kuitenkin BCNF vaatii, että jokaisen attribuutin tulee riippua vain ja ainoastaan relaation avainehdokkaista. Funktionaaliselle attribuutille {myyjätunnus, päivä} -> {huonenro} näin ei päde, sillä {myyjätunnus, päivä} ei ole avainehdokas.

Ratkaisuna on siirtää rikkova funktionaalinen riippuvuus omaksi tauluksi. Lopuksi siis päädytään tauluun KONSULTOINTI_VARAUS, joka kertoo varatun konsultoinnin ajan ja myyjän:

käyttäjätunnus päivä kellonaika myyjätunnus
matti123 2025-01-30 12:00 myyjä01
matti123 2025-02-10 12:00 myyjä01
hilda06 2025-01-30 15:00 myyjä02
asko_96 2025-03-01 13:00 myyjä03

Lisäksi funktionaaliselle riippuvuudelle {myyjätunnus, päivä} -> {huonenro} tehdään taulu HUONE_VARAUS, joka kertoo, minkä huoneen myyjä varasi itselleen päiväksi:

myyjätunnus päivä huonenro
myyjä01 2025-01-30 huone101
myyjä01 2025-02-10 huone102
myyjä02 2025-01-30 huone101
myyjä03 2025-03-01 huone100

Neljäs normaalimuoto

Yleensä BCNF riittää tosi hyvin normalisointiin. Tietyille tauluille, jotka mallintavat moniarvoista dataa, funktionaalisen riippuvuuden määritelmä ei tunnistamaan poikkeamia.

Esimerkki

Katsotaan viimeistä kertaa Webbikauppa.com-yrityksen tietokantaa.

Webbikauppa.com haluaa alkaa myydä omia valmiiksi rakennettuja pöytätietokoneita. Pöytätietokoneista tarjotaan erilaisia malleja, ja jokaisella mallilla on joukko muokattavia ominaisuuksia, kuten keskusmuistin määrä, prosessorin malli ja näytönohjaimen malli. Tietokantaan haluttaisiin mallintaa, mitä ominaisuuksia on tarjolla eri malleille.

Vaatimuksen perusteella tehtiin taulu PC_MALLI_VERSIOT:

malli keskusmuisti prosessorimalli näytonohjainmalli
Palikka 4 GB AMD Ryzen 3 NVIDIA GeForce GTX 1080
Palikka 8 GB AMD Ryzen 3 NVIDIA GeForce GTX 1080
Palikka 4 GB AMD Ryzen 5 NVIDIA GeForce GTX 1080
Palikka 8 GB AMD Ryzen 5 NVIDIA GeForce GTX 1080
Palikka 4 GB AMD Ryzen 3 NVIDIA GeForce RTX 2060
Palikka 8 GB AMD Ryzen 3 NVIDIA GeForce RTX 2060
Palikka 4 GB AMD Ryzen 5 NVIDIA GeForce RTX 2060
Palikka 8 GB AMD Ryzen 5 NVIDIA GeForce RTX 2060
Möykky 32 GB Intel Core i7 NVIDIA GeForce GTX 1080
Möykky 64 GB Intel Core i7 NVIDIA GeForce GTX 1080
Möykky 32 GB Intel Core i9 NVIDIA GeForce GTX 1080
Möykky 64 GB Intel Core i9 NVIDIA GeForce GTX 1080
Möykky 32 GB Intel Core i7 NVIDIA GeForce RTX 2060
Möykky 64 GB Intel Core i7 NVIDIA GeForce RTX 2060
Möykky 32 GB Intel Core i9 NVIDIA GeForce RTX 2060
Möykky 64 GB Intel Core i9 NVIDIA GeForce RTX 2060

Taulun perusteella tarjolla on kaksi PC-mallia: Palikka ja Möhkäle. Jokaisella mallilla on kaksi eri keskusmuisti-, prosessori- ja näytönohjainvaihtoehtoa.

Huomaa, että perusavaimessa on kaikki sarakkeet {malli, keskusmuisti, prosessorimalli, näytönohjainmalli}. Funktionaalisia riippuvuuksia ei tässä ole, sillä mikään sarake ei yksilöi mitään toista saraketta: mallilla voi olla monta eri ominaisuusvaihtoehtoa, ja yksittäinen ominaisuus (esim. prosessorin malli) voi olla käytössä eri mallilla.

Funktionaalisia riippuvuuksia ei ole, eikä siis ole mitään, mikä rikkoisi BCNF:n vaatimuksia.

Harjoittele

Katsele yllä olevaa taulua ja mallidataa. Pohdi, millaisia poikkeamia tai mahdollista toisteisuutta voi syntyä. Muista, että poikkeama on tiedon lisäyksestä, muokkauksesta tai poistosta johtuva syntyvä ristiriita taulun tiedoissa.

Mitä muita mahdollisia ongelmia voi syntyä, kun yllä olevaa taulua yrittää käyttää?

Kun olet pohtinut asiaa, voit lukea selityksen alla:

Avaa selitys taulun rakenteen ongelmista

Yllä olevassa esimerkissä sarakkeilla ei ole funktionaalisia riippuvuuksia. Voidaanko silti jotain sanoa siitä, miten sarakkeet liittyvät toisiinsa? Esimerkkitaulusta voidaan todeta, että:

  • Palikka-mallilla on vain tasan kaksi keskusmuistivaihtoehtoa: 4 GB tai 8 GB. Keskusmuistivaihtoehdot eivät riipu prosessorimallista tai näytönohjainmallista.
  • Samoin Möykky-mallilla on tasan kaksi keskusmuistivaihtoehtoa: 32 GB 64 GB eikä sekään riipu prosessorista tai näytönohjaimesta.
  • Vastaavasti malleilla on tasan kaksi vaihtoehtoa prosessorimalli-sarakkeen arvolle, samoin näytönohjainmalli-sarakkeelle eivätkä niiden arvot riipu suoraan toisistaan.

Funktionaalisessa riippuvuudessa attribuutti voi yksilöidä jonkun toisen attribuutin yksittäisiä arvoja. Tässä esimerkissä ei ole niin, mutta ollaan lähellä: attribuutti malli yksilöi attribuutin keskusmuisti arvojoukon. Toisin sanoen, jos tiedetään malli, voidaan tietää, mitä keskusmuistin vaihtoehtoja sillä on. Yleistetään tämä huomio määritelmään:

Määritelmä

Jos joukko relaation R attribuutteja X yksilöi jonkun toisen joukon saman relaation attribuuttien Y arvojoukon riippumatta relaation muista attribuuteista, sanotaan, että relaatiossa pätee moniarvoinen riippuvuus {X} ->> {Y}.

Toisin sanoen: Jos relaatio sisältää attribuuttijoukot X, Y ja Z, niin jokaisella monikolla, jolla attribuuttijoukon X arvo on a, on attribuuttijoukon Y arvojen joukko aina b riippumatta attribuuttijoukon Z arvoista.

Lisäksi, jos määräävä ja riippuva attribuuttijoukko yhdessä {X, Y} sisältävät relaation kaikki attribuutit, sanotaan, että riippuvuus {X} ->> {Y} on triviaali. Muussa tapauksessa riippuvuus on epätriviaali.

Yllä olevan esimerkin perusteella voidaan siis todeta, että {malli} ->> {keskusmuisti}, {malli} ->> {prosessorimalli} ja {malli} ->> {näytönohjain}. Toisin sanoen, keskusmuistin (tai prosessorimallin tai näytönohjaimen) mahdolliset arvovaihtoehdot riippuvat vain mallista eikä mistään muusta attribuutista.

Tämän määritelmän pohjalta voidaan esittää neljännen normaalimuodon vaatimukset:

Määritelmä

Relaatio on 4NF:ssä, jos kaikki seuraavat vaatimuksen täyttyvät:

  1. Relaatio on BCNF:ssä.
  2. Relaation kaikkien epätriviaalien moniarvoisten riippuvuuksien määräävä attribuuttijoukko tulee olla relaation avainehdokas.

Toisin sanoen, jos relaatiossa on moniarvoinen riippuvuus {X} ->> {Y}, niin attribuuttijoukon X tulee olla relaation avainehdokas.

Huomaa, että 4NF koskee vain epätriviaaleja riippuvuuksia. Tämän johdosta 4NF soveltuu käytännössä vain tauluihin, joissa on enemmän kuin kaksi saraketta. Rajoituksen takia myös binääristä N-M -suhdetta mallintavat taulut täyttävät 4NF.

Käytännössä 4NF voi mennä rikki vain, jos tietokannan suunnitteluvaiheessa samalla taululla yritetään mallintaa monta N-M -suhdetta samanaikaisesti. Jos transformoinnin suorittaa oikein ja sääntöjen mukaan, taulussa ei pitäisi esiintyä epätriviaaleja moniarvoisia riippuvuuksia lainkaan.

Esimerkki

Normalisoidaan vielä lopuksi esimerkkitaulu. Kuten todettiin, löydettiin seuraavat moniarvoiset riippuvuudet:

  • {malli} ->> {keskusmuisti}
    • "Jos tiedetään malli, tiedetään yksikäsitteisesti kaikki keskusmuistivaihtoehdot"
  • {malli} ->> {prosessorimalli}
    • "Jos tiedetään malli, tiedetään yksikäsitteisesti kaikki prosessorimallivaihtoehdot"
  • {malli} ->> {näytönohjainmalli}
    • "Jos tiedetään malli, tiedetään yksikäsitteisesti kaikki näytönohjainvaihtoehdot"

Kaikki kolme riippuvuutta ovat epätriviaaleja: määräävän ja riippuvan joukon yhdiste ei ole koko taulu.

Lisäksi {malli} ei ole taulun perusavain {malli, keskusmuisti, prosessorimalli, näytönohjainmalli}, joten 4NF ei päde. Ratkaisuna tehdään kullekin riippuvuudelle oma taulu:

MALLI_KESKUSMUISTIT:

malli keskusmuisti
Palikka 4 GB
Palikka 8 GB
Möykky 32 GB
Möykky 64 GB

MALLI_PROSESSORIMALLIT:

malli prosessorimalli
Palikka AMD Ryzen 3
Palikka AMD Ryzen 5
Möykky Intel Core i7
Möykky Intel Core i9

MALLI_NÄYTÖNOHJAINMALLIT:

malli näytönohjain
Palikka NVIDIA GeForce GTX 1080
Palikka NVIDIA GeForce RTX 2060
Möykky NVIDIA GeForce GTX 1080
Möykky NVIDIA GeForce RTX 2060

Huomaa erityisesti, että rivien määrä on saatu vähennettyä: alkuperäisestä 16 rivistä on saatu 12 riviä. Lisäksi nyt malleille on nyt helpompaa lisätä uusia ominaisuuksia: uuden näytönohjainvaihtoehdon lisäys tarkoittaa vain parin rivin lisäämistä tauluun.

Loppusanat normalisoinnista

Tätä materiaalia varmasti lukee kolme eri selkeää joukkoa: tulevat ohjelmoijat sekä tietokantojen ylläpitäjät, tulevat tutkijat sekä tietokantojen suunnittelijat ja niitä, jotka mahdollisesti pysyttelevät kaukana tietokannoista tämän materiaalin jälkeen (toivottavasti ei kuitenkaan tämän materiaalin johdosta).

Ohjelmoijat ja ylläpitäjät: Normalisoinnin tarkastelu voi olla hankalaa, ja se vaatii kohdealueen hyvää ymmärtämistä. Normalisointia voisi oikeastaan tiivistää pitkälti kahteen pääsääntöön, jonka perusteella tauluja kannattaa suunnitella:

  • Tauluun tulee tallentaa vain kohteeseen välittömästi liittyviä tietoja.
    • Esimerkiksi opiskelijan postitoimipaikka on postinumeroon liittyvä tieto, ei opiskelijan. Jos taulu tallentaa eri kohteiden tietoja, se viestii tarpeesta luoda oma taulu.
  • Kunkin tiedon päivitys tulee tapahtua vain yhteen paikkaan.
    • Esimerkiksi jos opiskelijalle vaihtuu postinumero, ja sen takia pitää päivittää sekä postinumero että postitoimipaikka kahdesta eri sarakkeesta, se viestii tarpeesta luoda oma taulu.

Seuraamalla näitä kahta sääntöä pääsee hyvin pitkälle tietokannan suunnittelussa.

Tutkijat ja suunnittelijat: Käsitteellinen mallintaminen on erittäin kannattavaa. Jos ER-kaavio on tehty kohdealueen tarpeita huomioiden ja transformointi suoritettu oikeaoppisesti, tuloksena on usein 3NF tai jopa BCNF toteuttavia relaatioita.

Mainittakoon, että normaalimuotoja on todellisuudessa muitakin. Jotkin niistä ovat vaativuudeltaan käsiteltyjen normaalimuotojen välissä, mutta useimmat ovat käsiteltyjä normaalimuotoja vahvempia. Erityisesti viides normaalimuoto vaatisi formaalia tarkastelua.

Normalisointi on pyritty tässä luvussa esittämään mahdollisimman yksinkertaisesti menemättä liian formaaleihin määritelmiin. Tässä luvussa esitellyt määritelmät eivät ole suinkaan virallisia; tarkasta määritelmästä kiinnostuneet voivat jatkaa aiheeseen tutustumista esimerkiksi Elmasrin ja Navathen kirjassa [15].

Muut lukijat: Toivottavasti tämä ei ollut liian raskas luku 😅. Olennaisin asia, joka pitäisi jäädä mieleen on, että taulujen rakenteella voidaan vaikuttaa tietokannan helppokäyttöisyyteen sekä "idioottivarmuuteen". Toisaalta mitä enemmän tietoja jakaa eri tauluihin, sitä hitaammat tietokantakyselyt ovat. Lisäksi nykyisissä tietokannanhallintajärjestelmissä normalisointi 2NF:n yli voi jopa lisätä levytilan käyttöä [36]. Siispä voi todeta, että oikeastaan normalisointi ei ole aina edes kannattavaa. Nykyään tallennustila on varsin halpaa, ja käyttäjät haluavat yhä nopeampia sovelluksia. Siinä mielessä voi olla perusteltua lisätä vähän toisteisuutta dataan jos se vähentää tietokantakyselyiden määrää ja monimutkaisuutta. Puhutaankin jopa tietokantojen denormalisoinnista: tauluja jätetään normalisoimatta tai muutetaan alempaan normaalimuotoon jotta kyselyt olisivat nopeampia.

# moninf

These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.