Surrogaattiavaimien mallintaminen

Kirjoittanut Samu Lahdenperä · Julkaistu

Surrogaattiavain on tietovaraston oma, järjestelmän generoima kokonaislukutunniste joka korvaa lähdejärjestelmän luonnollisen avaimen dimension pääavaimena. Dimensioiden mallinnuksen perusteissa käydään läpi mitä surrogaattiavain on ja miksi se on aina kokonaisluku. Tällä sivulla käsitellään kolme asiaa jotka jäävät perusteista usein pois: missä medallion-arkkitehtuurin kerroksessa surrogaatit luodaan, miten suurten dimensioiden avainten hallinta kannattaa rakentaa, ja miten inkrementaalinen lataus toimii käytännössä.

Miksi surrogaattiavain — eikä luonnollinen avain?

Luonnollinen avain tulee lähdejärjestelmästä ja se on lähdejärjestelmän omistama. Tietomalli, joka rakentaa relaatiot luonnollisten avainten varaan, on riippuvainen lähdejärjestelmän päätöksistä. Kun lähdejärjestelmä uudelleennumeroi asiakkaat järjestelmänvaihdon yhteydessä, kaikki historialliset faktataulun rivit viittaavat väärään asiakkaaseen. Surrogaattiavain katkaisee tämän riippuvuuden.

Taulukko 1, Luonnollinen avain vs. surrogaattiavain – Käytännön erot tietovaraston näkökulmasta
Ominaisuus Luonnollinen avain (NK) Surrogaattiavain (SK)
Omistaja Lähdejärjestelmä — arvo voi muuttua ulkoisesta päätöksestä Tietovarasto — arvo pysyy muuttumattomana
Tietotyyppi Usein VARCHAR tai yhdistetty avain (useampi sarake) Aina INT — pakkautuu VertiPaqissa optimaalisesti
Uudelleenkäyttö Lähdejärjestelmä voi antaa saman tunnisteen uudelle entiteetille Ei koskaan uudelleenkäytetä — jokainen arvo on ainutkertainen
Historian säilytys Sama NK kaikissa versioissa → historia hajoaa tai ylikirjoittuu Jokainen SCD Type 2 -versio saa oman SK:n → historia säilyy täydellisenä
Usean lähteen yhdistäminen NK:t voivat törmätä: sama asiakastunnus eri järjestelmissä eri asiakkaalle SK on globaali — ei törmäyksiä lähteiden välillä

Missä surrogaattiavaimet kannattaa luoda medallion-arkkitehtuurissa?

Surrogaattiavaimet kannattaa luoda ennen esitystasoa, eli Silver-kerroksessa. Yleisin virhe on luoda ne liian aikaisin (Bronze) tai liian myöhään (Gold). Silver on se kerros jossa data puhdistetaan, yhdistetään useista lähteistä ja yhdenmukaistetaan yhteiseen muotoon — vasta silloin tiedetään varmuudella mitä entiteettiä NK edustaa.

Yksi ehto kuitenkin: Silver-kerroksen on oltava materialisoitu — fyysisiä tauluja, ei näkymiä. Surrogaattiavaimet siis kirjoitetaan levylle Silver-tasolla ennen kuin faktataulu ladataan, jotta faktan latauksessa on olemassa valmis avain johon viitata. Jos mapping-taulu ja silver.DimAsiakas ovat näkymiä, surrogaattiavainta ei kirjoiteta mihinkään: se lasketaan uudelleen joka kerta kun joku kysyy. Silloin faktataulun liitos vertaa yhä lähdejärjestelmän VARCHAR-tunnistetta, ja koko INT-avaimen hyöty jää saamatta. Materialisointi on tässä myös tekninen pakko eikä pelkkä suositus: IDENTITY on perustaulun sarakeominaisuus, joten sitä ei voi määritellä näkymässä. Tämän sivun mapping-taulut ovat väistämättä CREATE TABLE:ja.

Taulukko 2, Surrogaattiavaimien sijainti medallion-kerroksissa – Mikä rooli kullakin kerroksella on avainten hallinnassa
Kerros Rooli avainten kannalta Mitä tehdään
Bronze Raakadata sellaisenaan Tallennetaan NK lähdejärjestelmästä koskemattomana. Ei surrogaatteja. Data voi olla epätäydellistä, duplikoitua tai virheellistä — SK:ta ei luoda ennen kuin tiedetään mitä data tarkoittaa.
Silver Avainten luontipaikka — materialisoituna Rakennetaan mapping-taulu (NKSK). Jokainen uniikki NK saa oman SK:n. Jos sama entiteetti tulee useasta lähdejärjestelmästä, NK:t yhdistetään yhdeksi SK:ksi. Silver-dimensiotaulussa on sekä SK (PK) että NK (attribuuttina, debug-käyttöön).
Gold Käyttää vain SK:ta Faktataulut ja raportointimallit viittaavat yksinomaan SK:iin. Gold-kerroksessa ei enää viitata NK:iin relaatioissa. NK voi silti olla dimensiossa attribuuttina loppukäyttäjää varten, mutta se ei ole avain.
Miksi ei Bronze-kerroksessa?

Bronze-data on raakaa: siellä voi olla duplikaattirivejä, puuttuvia arvoja ja saman entiteetin useita versioita peräkkäin. Jos surrogaatti luodaan Bronze-kerroksessa, sama asiakas saa useamman SK:n ennen kuin data on puhdistettu. Tämä tarkoittaa, että Silver-kerroksessa pitää joka tapauksessa yhdistää nämä avaimet uudelleen — eli Bronze-tason SK oli turha välivaihe. Luodaan avain kerran, oikeassa paikassa.

Miksi ei Gold-kerroksessa?

Gold-kerroksessa voi olla useita erillisiä tauluja tai aggregointeja, jotka kaikki tarvitsevat saman asiakasdimension. Jos SK luodaan vasta Gold-tasolla, jokainen Gold-taulu luo omat avaimensa — ja ne eivät täsmää toisiinsa. Silver-tasolla luotu SK on se yhteinen nimittäjä, johon kaikki Gold-tasot voivat luottaa.

Suuren dimension avainten hallinta

Kun dimensiossa on miljoonia rivejä — esimerkiksi teleoperaattorin kaikki tilaajat tai verkkokaupan rekisteröityneet käyttäjät — avainten hallintaan tarvitaan erillinen mapping-taulu. Mapping-taulu on yksinkertainen viitetaulu joka pitää kirjaa siitä, mikä NK vastaa mitäkin SK:ta. Se on koko inkrementaalisen latauksen ydin.

Taulukko 3, Mapping-taulun rakenne – NK:n ja SK:n välinen viitetaulu Silver-kerroksessa
Sarake Tietotyyppi Rooli
AsiakasTunnus VARCHAR Luonnollinen avain lähdejärjestelmästä. Indeksoitu — AsiakasTunnukselle lisätään surrogaattiavain ja luontiaika jokaisessa latauksessa
AsiakasAvain INT IDENTITY Surrogaattiavain. Tietokanta generoi automaattisesti nousevan kokonaisluvun. Ei koskaan uudelleenkäytetä.
LuontiAika DATETIME2 Milloin rivi lisättiin. Hyödyllinen debug-työssä ja latausprosessin seurannassa.
Esimerkki: AsiakasDimensio — miltä mapping-taulu näyttää käytännössä

Lähdejärjestelmä käyttää asiakasnumerona merkkijonoa FI-VVVV-NNNNN. Viiden latauserän jälkeen silver.LookupAsiakasAvain näyttää tältä — IDENTITY on generoinut SK:t automaattisesti lisäysjärjestyksessä:

Taulukko 4, LookupAsiakasAvain — esimerkkirivejä viiden latauserän jälkeen
AsiakasAvain (SK) AsiakasTunnus (NK) LuontiAika
1 FI-2024-00001 2024-01-15 08:32:04
2 FI-2024-00002 2024-01-15 08:32:04
3 FI-2024-05821 2024-03-22 14:05:31
4 FI-2025-00134 2025-02-10 09:18:47
5 FI-2026-00891 2026-06-15 14:30:02

Mapping-tauluun liitytään dimensiolatauksessa NK:n perusteella. Tuloksena silver.DimAsiakas käyttää ainoastaan INT-surrogaattiavainta — VARCHAR-tunnus jää attribuutiksi debug-käyttöön:

Taulukko 5, DimAsiakas (Silver) — SK käytössä pääavaimena, NK attribuuttina
AsiakasAvain (PK) AsiakasTunnus AsiakasNimi Segmentti Maa
1 FI-2024-00001 Matti Meikäläinen Pienasiakas Suomi
2 FI-2024-00002 Yritys Oy B2B Suomi
3 FI-2024-05821 Tiina Testaaja Avainasiakas Suomi
4 FI-2025-00134 Nordic Corp AS B2B Norja
5 FI-2026-00891 Uusi Asiakas Ab Pienasiakas Ruotsi

SQL-koodi joka toteuttaa tämän inkrementaalisesti — ensin mapping-taulun luonti, sitten kaksi vaihetta jokaisessa latauserässä. IDENTITY(1,1) CREATE TABLE:ssa tarkoittaa: ensimmäinen luku on lähtöarvo (1 = aloitetaan ykkösestä), toinen luku on askel (1 = kasvatetaan yhdellä aina kun rivi lisätään). Tietokanta generoi AsiakasAvain-arvon automaattisesti — INSERT-lauseessa ei tarvitse mainita AsiakasAvain-saraketta lainkaan. Ensimmäinen asiakas saa arvon 1, toinen 2, kolmas 3, ja niin edelleen ikuisesti kasvavana sarjana.

-- 1. Mapping-taulun luonti (kerran)
CREATE TABLE silver.LookupAsiakasAvain (
    AsiakasAvain  INT         NOT NULL IDENTITY(1,1),
    AsiakasTunnus VARCHAR(50) NOT NULL,
    LuontiAika    DATETIME2   NOT NULL DEFAULT SYSDATETIME(),
    CONSTRAINT PK_LookupAsiakas PRIMARY KEY (AsiakasAvain),
    CONSTRAINT UQ_LookupAsiakas_NK UNIQUE (AsiakasTunnus)
);

-- 2. Lisää uudet NK:t — IDENTITY generoi SK:n automaattisesti
--    (ajetaan joka latauserässä ennen dimension päivitystä)
INSERT INTO silver.LookupAsiakasAvain (AsiakasTunnus)
SELECT DISTINCT s.AsiakasTunnus
FROM   bronze.Staging_Asiakas s
WHERE  NOT EXISTS (
    SELECT 1
    FROM   silver.LookupAsiakasAvain l
    WHERE  l.AsiakasTunnus = s.AsiakasTunnus
);

-- 3. Päivitä Silver-dimensio (SCD Type 1: ylikirjoita muuttunut data)
MERGE silver.DimAsiakas AS target
USING (
    SELECT l.AsiakasAvain,
           s.AsiakasTunnus,
           s.AsiakasNimi,
           s.Segmentti,
           s.Maa
    FROM   bronze.Staging_Asiakas       s
    JOIN   silver.LookupAsiakasAvain    l
               ON l.AsiakasTunnus = s.AsiakasTunnus
) AS source ON target.AsiakasAvain = source.AsiakasAvain
WHEN MATCHED THEN
    UPDATE SET
        AsiakasNimi = source.AsiakasNimi,
        Segmentti   = source.Segmentti,
        Maa         = source.Maa
WHEN NOT MATCHED THEN
    INSERT (AsiakasAvain, AsiakasTunnus, AsiakasNimi, Segmentti, Maa)
    VALUES (source.AsiakasAvain, source.AsiakasTunnus,
            source.AsiakasNimi, source.Segmentti, source.Maa);

Vaihe 2 lisää mapping-tauluun vain ne rivit joiden NK ei siellä vielä ole — olemassa olevat asiakkaat saavat saman SK:n kuin aiemmassa erässä. Vaihe 3 päivittää dimension: MATCHED-haara ylikirjoittaa muuttuneet attribuutit, NOT MATCHED -haara lisää kokonaan uudet rivit.

SCD Type 2 -mallinnuksessa sama NK voi esiintyä mapping-taulussa useaan kertaan — jokainen versio saa oman SK:n. Tällöin mapping-tauluun lisätään voimassaoloaika ja nykyisyyslippu, jotta tiedetään mikä SK on tällä hetkellä voimassa.

Esimerkki: SCD Type 2 — asiakas vaihtaa segmenttiä

Matti Meikäläinen (NK: FI-2024-00001, nykyinen SK: 1) siirtyy segmentistä Pienasiakas segmenttiin Avainasiakas 15.6.2026. SCD Type 2 -mallinnuksessa tämä ei ylikirjoita vanhaa riviä — se sulkee sen ja luo uuden rivin uudella SK:lla.

Ennen muutosta:

Taulukko 6, LookupAsiakasAvain ja DimAsiakas ennen segmenttimuutosta
AsiakasAvain AsiakasTunnus Segmentti AlkuPvm LoppuPvm OnkoNykyinen
1 FI-2024-00001 Pienasiakas 2024-01-15 9999-12-31 1

Latauserän jälkeen (15.6.2026): Vanha rivi suljetaan, uusi SK (7) generoidaan IDENTITY:llä ja uusi dimensiorivi lisätään uudella segmentillä.

Taulukko 7, LookupAsiakasAvain ja DimAsiakas segmenttimuutoksen jälkeen
AsiakasAvain AsiakasTunnus Segmentti AlkuPvm LoppuPvm OnkoNykyinen
1 FI-2024-00001 Pienasiakas 2024-01-15 2026-06-14 0
7 FI-2024-00001 Avainasiakas 2026-06-15 9999-12-31 1

FactMyynti-rivit ennen 15.6.2026 viittaavat AsiakasAvaimeen 1 → segmentti on edelleen Pienasiakas. Myyntirivit 15.6.2026 alkaen viittaavat AsiakasAvaimeen 7 → segmentti on Avainasiakas. Sama asiakas, sama NK — mutta kaksi erillistä historiaa.

SCD Type 2 -mapping-taulussa ei voi käyttää UNIQUE-rajoitetta AsiakasTunnukselle, koska sama NK esiintyy useita kertoja. CREATE TABLE eroaa SCD Type 1 -versiosta juuri tässä:

-- SCD Type 2 -mapping-taulu: sama NK voi esiintyä useasti
CREATE TABLE silver.LookupAsiakasAvain (
    AsiakasAvain  INT         NOT NULL IDENTITY(1,1),
    AsiakasTunnus VARCHAR(50) NOT NULL,
    AlkuPvm       DATE        NOT NULL,
    LoppuPvm      DATE        NOT NULL DEFAULT '9999-12-31',
    OnkoNykyinen  BIT         NOT NULL DEFAULT 1,
    CONSTRAINT PK_LookupAsiakas PRIMARY KEY (AsiakasAvain)
    -- Ei UNIQUE(AsiakasTunnus) — sama NK saa useamman SK:n eri versioille
);

-- Vaihe 1: Sulje vanha rivi mapping-taulusta
--          (ennen uuden SK:n luontia, jotta tuore rivi ei sulkeudu heti)
UPDATE l
SET    LoppuPvm     = DATEADD(day, -1, CAST(GETDATE() AS DATE)),
       OnkoNykyinen = 0
FROM   silver.LookupAsiakasAvain l
JOIN   silver.DimAsiakas         d  ON d.AsiakasTunnus = l.AsiakasTunnus
                                    AND d.OnkoNykyinen  = 1
JOIN   bronze.Staging_Asiakas    s  ON s.AsiakasTunnus = l.AsiakasTunnus
WHERE  d.Segmentti <> s.Segmentti
  AND  l.OnkoNykyinen = 1;

-- Vaihe 2: Luo uusi SK muuttuneelle riville
INSERT INTO silver.LookupAsiakasAvain (AsiakasTunnus, AlkuPvm)
SELECT DISTINCT s.AsiakasTunnus, CAST(GETDATE() AS DATE)
FROM   bronze.Staging_Asiakas s
JOIN   silver.DimAsiakas      d
    ON d.AsiakasTunnus = s.AsiakasTunnus
   AND d.OnkoNykyinen  = 1
WHERE  d.Segmentti <> s.Segmentti;   -- seurattava attribuutti muuttui

-- Vaihe 3: Sulje vanha dimensiorivi
UPDATE d
SET    LoppuPvm     = DATEADD(day, -1, CAST(GETDATE() AS DATE)),
       OnkoNykyinen = 0
FROM   silver.DimAsiakas      d
JOIN   bronze.Staging_Asiakas s ON s.AsiakasTunnus = d.AsiakasTunnus
WHERE  d.Segmentti   <> s.Segmentti
  AND  d.OnkoNykyinen = 1;

-- Vaihe 4: Lisää uusi dimensiorivi uudella SK:lla
INSERT INTO silver.DimAsiakas
       (AsiakasAvain, AsiakasTunnus, AsiakasNimi, Segmentti, Maa, AlkuPvm, LoppuPvm, OnkoNykyinen)
SELECT l.AsiakasAvain,
       s.AsiakasTunnus, s.AsiakasNimi, s.Segmentti, s.Maa,
       CAST(GETDATE() AS DATE), '9999-12-31', 1
FROM   bronze.Staging_Asiakas       s
JOIN   silver.LookupAsiakasAvain    l  ON l.AsiakasTunnus = s.AsiakasTunnus
                                       AND l.OnkoNykyinen  = 1
WHERE  NOT EXISTS (
    SELECT 1 FROM silver.DimAsiakas d WHERE d.AsiakasAvain = l.AsiakasAvain
);

Vaiheet 1 ja 3 sulkevat vanhan version: LoppuPvm = tänään - 1 ja OnkoNykyinen = 0. Sulkeminen tehdään ennen insertiä, jotta vaiheen 2 juuri luoma rivi ei sulkeutuisi heti syntymänsä jälkeen. Vaihe 4 lisää uuden version — IDENTITY antaa sille seuraavan vapaan kokonaisluvun. Faktatauluun ei kosketa yhdelläkään UPDATElla: voimassaoloaikaa luetaan vain latauksessa, kun faktariville valitaan tapahtumahetkellä voimassa ollut SK.

Inkrementaalinen lataus — uusien avainten tunnistaminen

Inkrementaalisessa latauksessa data päivitetään erissä: esimerkiksi puolen tunnin välein tai kerran päivässä. Jokaisessa latauksessa voi tulla sekä uusia entiteettejä (uudet asiakkaat) että muuttuneita entiteettejä (olemassa oleva asiakas vaihtanut segmenttiä). Mapping-taulu ratkaisee molemmat tapaukset yhdellä periaatteella: jos NK on jo mapping-taulussa, käytä olemassa olevaa SK:ta; jos ei ole, luo uusi.

Inkrementaalisen latauksen vaiheet (SCD Type 1):
  1. Lataa muuttunut data staging-tauluun. Staging sisältää vain ne rivit jotka ovat muuttuneet tai ovat uusia edellisen latauksen jälkeen (CDC, aikaleima tai versiotunnus).
  2. Tunnista uudet NK:t. Vertaa staging-taulun NK:ia mapping-tauluun. Staging-rivit joiden NK ei löydy mapping-taulusta ovat uusia entiteettejä.
  3. Luo uudet SK:t. Lisää uudet NK:t mapping-tauluun — tietokanta generoi automaattisesti uuden IDENTITY-arvon eli SK:n. Vanhoille NK:ille ei luoda uutta SK:ta.
  4. Liitä SK staging-tauluun. JOIN staging ↔ mapping NK:n perusteella. Nyt jokaisella staging-rivillä on oikea SK.
  5. MERGE Silver-dimensioon. Olemassa olevat rivit päivitetään (Type 1: ylikirjoita), uudet rivit lisätään.

SCD Type 2 -mallinnuksessa vaihe 3 muuttuu: kun dimensioattribuutti muuttuu (esim. segmentti vaihtuu), vanhalle riville kirjoitetaan sulkemispäivä ja uudelle versiolle luodaan uusi SK mapping-tauluun. Faktataulun seuraavat rivit viittaavat uuteen SK:hon — historialliset rivit jäävät viittaamaan vanhaan.

Taulukko 8, Latausvälin vaikutus — 30 min vs. päivittäinen – Sama malli, eri eräkoko
30 minuutin lataus Päivittäinen lataus
Eräkoko Pieni — tyypillisesti muutamia satoja tai tuhansia rivejä Suuri — koko päivän muutokset kerralla
Mapping-taulun käyttö Sama logiikka — NK haetaan mapping-taulusta jokaisessa erässä Sama logiikka — NK haetaan mapping-taulusta kerran päivässä
Erityishuomio Sama NK voi tulla kahdessa peräkkäisessä erässä (asiakas päivitetty kahdesti tunnissa). Mapping-taulu estää uuden SK:n luomisen — toinen erä löytää NK:n jo mapping-taulusta ja käyttää samaa SK:ta. Lataus voidaan ajaa hiljaisena aikana (esim. yöllä), eräkoko voi olla suurempi ilman suorituskykyhuolia. SCD Type 2 -sulkemiset ja avaukset tehdään kerralla koko päivän muutoksille.
Tuoreusviive Data on korkeintaan 30 minuuttia vanhaa — sopii operatiiviseen raportointiin Data on korkeintaan 24 tuntia vanhaa — riittää useimpiin liiketoimintaraportteihin
Faktataulun lataus Faktat ladataan samassa tai seuraavassa erässä — dimension SK:n on oltava olemassa ennen faktan latausta Dimensiot ladataan ensin, faktat perässä — sama järjestys kuin puolen tunnin mallissa
Esimerkki: uusi asiakas puolen tunnin erässä

Kello 14:00 Bronze-tauluun saapuu uusi asiakas AsiakasTunnus = "FI-2026-00892". Silver-latausprosessi käynnistyy 14:30:

  1. Staging sisältää rivin AsiakasTunnus = "FI-2026-00892".
  2. Mapping-taulussa ei ole tätä NK:ta → uusi rivi lisätään, tietokanta generoi AsiakasAvain = 6.
  3. Staging-rivi saa SK:n 6 JOIN-operaatiolla.
  4. Silver-dimensioon lisätään uusi asiakasrivi avaimella 6.
  5. Kello 15:00 saman asiakkaan nimi korjataan lähdejärjestelmässä. Seuraavassa erässä staging sisältää saman NK:n "FI-2026-00892" → mapping-taulu palauttaa 6 → Silver-rivillä päivitetään vain AsiakasNimi, SK pysyy muuttumattomana.
Dataneuvoksen mielipide