Auto-increment-kentät ovat perinteinen tapa saada tietokantaan juokseva laskuri. Yleensä niitä käytetään primary key -sarakkeen arvona. Samalla ne kuitenkin muodostavat tietokannan skaalautuvuuteen pullonkaulan, jota on todella vaikea korjata jälkeenpäin. Lisäksi ne monimutkaistavat sovellusta tarpeettomasti.

Niin millä tavalla monimutkaistavat? Yleisin ongelmatilanne on sellainen, että vaikkapa blogiartikkeliin liittyvät kuvatiedostot pitäisi saada tallennettua levylle. Niiden tiedostonimessä pitäisi käyttää artikkelin ID:tä, jotta tiedosto ei mene päällekkäin muiden kanssa. Mutta silloin blogiartikkeli täytyy ensin tallentaa tietokantaan ilman kuvia, jotta sen auto-increment-ID saadaan selville. Vasta sitten kuvat voidaan tallentaa levylle sekä päivittää niiden tiedostonimet artikkeliin tietokannassa.

Monimutkaisemmissa järjestelmissä näitä tilanteita tulee vastaan vähän väliä. Auto-incrementin käyttö "pakottaa" tekemään aina ensin INSERT-lauseen päätietokantaan, jotta uusi ID saadaan selville. Kun tämä INSERT ei voi vielä sisältää kaikkea tarvittavaa tietoa, joudutaan tekemään jälkeenpäin turha UPDATE, jolla päivitetään puuttuvat tiedot.

Skaalautuvuuten kannalta auto-increment muodostaa "single point of failure" -pisteen. Koska numeron pitää olla juokseva ja uniikki, se on pakko generoida aina samalla palvelimella. Tätä voi yrittää kiertää käyttämällä esimerkiksi segmentointia (toisella palvelimella auto-increment-arvot alkavat 1 miljardista) tai jakojäännöksiä, mutta tällaiset ratkaisut ovat aina hankalia ja vaativat virittelyä, jos palvelimia lisätään.

UUID tulee apuun

Olen itse tullut siihen tulokseen, että auto-increment-kentistä kannattaa luopua ja siirtyä kokonaan UUID:den käyttöön. UUID on 128-bittinen numero, joka on yleensä helpointa tallentaa 32-merkkisenä heksakoodina, kuten esimerkiksi "57a0809876974df79983f8a97b260607".

Viralliseen määritelmään kuuluu myös muutama tavuviiva, jolloin UUID esitetään muodossa "57a08098-7697-4df7-9983-f8a97b260607". Niitä on kuitenkin turha tallentaa tietokantaan. Viivat voi aina lisätä tilanteissa, joissa jokin protokolla niitä edellyttää. URLeissa viivaton muoto on omasta mielestäni siistimpi ja kompaktimpi. Se on myös ihan käytännössä helpompi copy-pasteta, koska viivattoman arvon saa valittua tuplaklikkaamalla. ;-)

Pythonissa UUID:t voi generoida uuid-moduulilla. Itse käytän tavallisesti uuid.uuid4()-funktiota, joka arpoo täysin satunnaisen luvun. Tällaisessa UUID:sä ei ole mukana esimerkiksi kellonaikaa tai koneen MAC-osoitetta tai muita identifioivia lähdearvoja. Kahden satunnaisen UUID:n päällekkäisyysriski vuositasolla on sama kuin putoavan meteoriitin alle jääminen, jos vuodessa generoidaan kymmenen tuhatta miljardia UUID:tä. Itselleni tämä varmuus riittää, kun en työskentele pankissa.

Tietokantojen yhteydessä UUID voidaan generoida etukäteen ennen datan tallentamista tietokantaan. Dataobjektia voidaan siis käsitellä muistissa niinkuin se olisi jo olemassa. Kuvatiedostot voidaan nimetä UUID:n mukaan valmiiksi oikein ja mahdolliset viittaukset muihin järjestelmiin (esimerkiksi NoSQL-kantoihin) voidaan valmistella hyvissä ajoin. Sitten valmis objekti INSERTataan kerralla päätietokantaan ja voidaan olla varmoja siitä, että se on heti käytettävissä kaikkine oheisriippuvuuksineen.

Skaalautuvuuden kannalta tällainen UUID-perusteinen dataobjekti voidaan tallentaa mihin tahansa tietokantaan, jos käytössä sattuu olemaan useita rinnakkaisia palvelimia. Tämä helpottaa esimerkiksi horisontaalista partitiointia ja master-master-replikointiin perustuvia järjestelmiä. Objekteja voidaan myös siirrellä tietokannasta toiseen ilman riskiä ID:n päällekkäisyydestä.

Auto-increment vs. UUID

Tietokannan suunnittelussa täytyy huomioida se, että UUID-primary-key-sarakkeet eivät ole enää numeerisessa järjestyksessä. Sen vuoksi tarvitaan aina erillinen "pub_date"-kenttä, jonka perusteella rivit voi järjestää. Päivämääräkentän käyttäminen sorttaamiseen on muutenkin siistimpää kuin auto-increment-kenttiin luottaminen, sillä primary keyn arvoa on jälkeenpäin vaikeaa tai mahdotonta muuttaa.

UUID-kenttien käyttö tuo tietokantaan myös uuden tietoturvakerroksen. Juoksevat auto-increment-arvot tekevät yleensä helpoksi arvata muita ID:itä tai käydä läpi esimerkiksi kaikki ID:t ykkösestä miljoonaan. UUID:itä taas on käytännössä mahdoton arvailla, eikä sillä ole mitään merkitystä, vaikka olisikin nähnyt yhden.

URLeissa UUID on tietysti vähän rumempi kuin perinteinen juokseva auto-increment-numero. Mutta käyttäjille näkyvät URLit pitäisi muutenkin suunnitella niin, että niissä näkyy joku merkitykselinen tunniste. Esimerkiksi omalla webbisaitillani URLeissa käytetään aina päivämääriä, otsikoita tai kategorioiden nimiä, joten taustalla käytetyt ID:t eivät vaikuta mitään.

UUIDField Djangossa

Lopuksi vielä esimerkki UUIDField-kentästä Djangolle. Tätä kenttää voi käyttää omien tietomallien primary key -sarakkeena. Alkuperäinen implementaatio on kopioitu jostain päin nettiä ja sitä on hiukan viritelty.

from django.db import models
import uuid

class UUIDField(models.CharField):
    """A UUID based primary key field."""
    def __init__(self, *args, **kwargs):
        kwargs['max_length'] = kwargs.get('max_length', 64)
        kwargs['blank'] = True
        return super(UUIDField, self).__init__(*args, **kwargs)

    def pre_save(self, model_instance, add):
        if add:
            value = getattr(model_instance, self.attname) if hasattr(model_instance, self.attname) else None
            if not value:
                value = uuid.uuid4().hex
                setattr(model_instance, self.attname, value)
            return value
        else:
            return super(models.CharField, self).pre_save(model_instance, add)

Oma tietomalli määriteltäisiin sitten näin:

class Article(models.Model):
    uuid = UUIDField(primary_key=True, editable=False)
    title = models.CharField(max_length=254)
    # ...

Django huomaa automaattisesti, että mallille on määritelty primary_key, eikä se silloin lisää tauluun oma id-kenttäänsä. Periaatteessa kentän nimeksi voisi antaa myös "id", mutta itse olen käyttänyt nimeä "uuid" sen takia, että muistan ohjelmoidessa helpommin, minkä tyyppinen kenttä on.

Kokemusteni perusteella kaikki Django 1.2.x:n mukana tulevat sovellukset toimivat ongelmitta UUID-kenttien kanssa. Erityisesti Djangon sisäänrakennettu admin-käyttöliittymä toimii täydellisesti. Joissakin 3rd party -sovelluksissa voi teoriassa olla ongelmia, jos ne olettavat (tyhmästi), että primary key on integer-tyyppinen. Vielä tällaista ei ole kuitenkaan tullut vastaan.

Published 6.11.2010