Kom i gang med logisk replikering i Postgres
Dette er en praktisk innføring i logisk replikering – mekanismen vi bruker for å oppnå referensiell integritet på tvers av subgrafer. Du setter opp to lokale Postgres-databaser med Docker, replikerer en entitetstabell fra den ene til den andre, og utforsker hvordan mekanismen oppfører seg – også når ting går galt.
Alt skjer lokalt i et lekemiljø du kaster etterpå. Regn med å bruke 30–45 minutter.
Dette lærer du:
- Hvordan publikasjoner, abonnementer og replikeringsslots henger sammen.
- Hvordan kolonnelister begrenser hva som forlater eierens database.
- Hvorfor read-only må håndheves med rettigheter – og hva som skjer hvis det ikke gjøres.
- Hvorfor fremmednøkler ikke beskytter mot sletting hos eieren.
- Hvordan du ser status på replikeringen, og hvorfor en død mottaker er eierens problem.
For å holde fokus på mekanismen bruker vi superbrukeren postgres og passord i klartekst.
I ekte miljøer brukes dedikerte roller med minimale rettigheter og ordentlig
hemmelighetshåndtering – det dekkes i egne oppskrifter for å tilby og abonnere på data.
Vi bruker samme eksempel som i arkitekturdokumentet: utdanningsregisteret eier utdanningsinstansene, og opptak eier utdanningstilbud som viser til dem.
1. Start to databaser
Du trenger Docker. Lag en tom katalog med denne docker-compose.yaml:
services:
utdanningsregisteret:
image: postgres:18
environment:
POSTGRES_PASSWORD: eier
command: postgres -c wal_level=logical
opptak:
image: postgres:18
environment:
POSTGRES_PASSWORD: mottaker
Merk wal_level=logical hos utdanningsregisteret: transaksjonsloggen (WAL) må inneholde nok
informasjon til at endringer kan dekodes rad for rad. Dette trengs bare hos den som tilbyr data
– mottakeren trenger ingen spesiell konfigurasjon.
Start miljøet:
docker compose up -d
Du kommer inn i databasene med psql slik (kjør i hvert sitt terminalvindu, så slipper du å bytte
frem og tilbake):
docker compose exec utdanningsregisteret psql -U postgres
docker compose exec opptak psql -U postgres
Resten av veiledningen merker SQL-blokkene med hvem de kjøres hos.
2. Opprett data hos eieren
Hos utdanningsregisteret – lag entitetstabellen med litt innhold. Tabellen legges i
domeneskjemaet utdanning, slik utdanningsregisteret faktisk har det – i steg 4 ser du hvorfor
skjemanavnet betyr noe. I dette lekemiljøet gir vi entiteten en sammensatt naturlig nøkkel, så
du får se at replikeringen håndterer det like godt som en enkeltkolonne (den virkelige
utdanningsinstansen identifiseres av utdanningsinstansnr, se
arkitekturdokumentet). Legg også merke til
kolonnen intern_kommentar: den er eierens egen og skal aldri forlate denne databasen.
create schema utdanning;
create table utdanning.utdanningsinstans (
organisasjonskode text not null,
utdanningsmulighet_kode text not null,
periodekode text not null,
navn text not null,
intern_kommentar text,
primary key (organisasjonskode, utdanningsmulighet_kode, periodekode)
);
insert into utdanning.utdanningsinstans values
('7777', 'B-INFORMATIKK', '2026H', 'Bachelor i informatikk', 'Under revisjon'),
('7777', 'M-BIOLOGI', '2026H', 'Master i biologi', null),
('7778', 'AARS-HISTORIE', '2026H', 'Årsstudium i historie', 'Vurderes nedlagt');
3. Publiser entitetstabellen
Hos utdanningsregisteret – opprett en publikasjon. Kolonnelisten – nøkkelkolonnene pluss
navn – er minimeringsprinsippet i praksis: bare disse kolonnene forlater databasen, uansett hva
mottakeren måtte ønske seg. Merk at kolonnelisten alltid må inneholde nøkkelkolonnene.
create publication opptak_utdanningsinstans
for table utdanning.utdanningsinstans
(organisasjonskode, utdanningsmulighet_kode, periodekode, navn);
Publikasjonen er bare en deklarasjon av hva som tilbys – ingenting sendes før noen abonnerer. Navnet følger konvensjonen fra arkitekturdokumentet: mottaker og formål, slik at eieren ser hvem som er i andre enden.
For at UPDATE og DELETE skal kunne replikeres, må Postgres vite hvordan en rad identifiseres
hos mottakeren. Som standard brukes primærnøkkelen – derfor fungerer alt rett ut av boksen her.
Publiserer du en tabell uten primærnøkkel, vil UPDATE og DELETE feile hos eieren inntil du
har satt REPLICA IDENTITY eksplisitt.
4. Abonner hos mottakeren
Hos opptak – tabellen kopieres ikke automatisk, så først oppretter du den selv. Den må ha
samme skjemakvalifiserte navn som hos eieren: replikeringen matcher tabeller på fullt navn
(utdanning.utdanningsinstans) og kan ikke døpe dem om underveis. Navnerommet følger altså
eieren – utdanningsregisteret bruker domeneskjemaet utdanning, så da må mottakeren gjøre det
samme. Deretter oppretter du abonnementet:
create schema utdanning;
create table utdanning.utdanningsinstans (
organisasjonskode text not null,
utdanningsmulighet_kode text not null,
periodekode text not null,
navn text not null,
primary key (organisasjonskode, utdanningsmulighet_kode, periodekode)
);
create subscription utdanningsregisteret_utdanningsinstans
connection 'host=utdanningsregisteret port=5432 user=postgres password=eier dbname=postgres'
publication opptak_utdanningsinstans;
Postgres melder at det ble opprettet en replikeringsslot hos eieren – den kommer vi tilbake til i steg 8. Sjekk at den initielle synkroniseringen har skjedd:
select * from utdanning.utdanningsinstans;
Alle tre radene er på plass – med nøkkelkolonnene og navn, uten intern_kommentar.
5. Se endringene flyte
Hos utdanningsregisteret – gjør noen endringer:
insert into utdanning.utdanningsinstans values
('7778', 'M-KJEMI', '2026H', 'Master i kjemi', null);
update utdanning.utdanningsinstans
set navn = 'Bachelor i informatikk og teknologi'
where utdanningsmulighet_kode = 'B-INFORMATIKK';
Hos opptak – kjør select-spørringen fra steg 4 igjen. Begge endringene er der, typisk i
løpet av millisekunder. Fra nå av vedlikeholder Postgres tabellen for deg: hver committede
endring hos eieren dekodes fra WAL og sendes fortløpende til abonnenten.
6. Håndhev read-only
Prinsippet er at replikerte data er read-only hos mottakeren – men logisk replikering håndhever
ikke det for deg. Tabellen hos opptak er en helt vanlig tabell, og akkurat nå kan hvem som helst
med skrivetilgang endre den. Det skal håndheves med rettigheter: applikasjonsrollen får kun
SELECT.
Hos opptak:
create role opptak_app login password 'app';
grant usage on schema utdanning to opptak_app;
grant select on all tables in schema utdanning to opptak_app;
Prøv å skrive som applikasjonsrollen:
set role opptak_app;
insert into utdanning.utdanningsinstans
values ('9999', 'FALSK', '2026H', 'Falsk instans');
-- ERROR: permission denied for table utdanningsinstans
reset role;
Selve replikeringen påvirkes ikke av dette – endringer fra abonnementet skrives av abonnementets eier, ikke av applikasjonsrollen.
7. Se hva som skjer når noen skriver likevel
Hvorfor er read-only så viktig? La oss sabotere. Hos opptak, som superbruker, skriv en lokal rad rett inn i den replikerte tabellen:
insert into utdanning.utdanningsinstans
values ('7777', 'M-MUSIKK', '2026H', 'Lokalt påfunn');
Hos utdanningsregisteret – eieren oppretter så, uvitende om dette, en instans med samme nøkkel:
insert into utdanning.utdanningsinstans
values ('7777', 'M-MUSIKK', '2026H', 'Master i musikk', null);
Hos opptak stopper nå replikeringen: den innkommende raden kolliderer med den lokale på
primærnøkkelen. Du ser det i loggen (docker compose logs opptak) som
conflict detected on relation "utdanning.utdanningsinstans": conflict=insert_exists,
og i statistikken:
select subname, apply_error_count
from pg_stat_subscription_stats;
Abonnementet prøver på nytt med jevne mellomrom, men kommer ingen vei – og alle senere endringer fra eieren blir stående i kø bak den som feiler. Fiks det ved å fjerne den lokale raden:
delete from utdanning.utdanningsinstans
where utdanningsmulighet_kode = 'M-MUSIKK';
I løpet av noen sekunder går replikeringen videre av seg selv, og eierens «Master i musikk» dukker opp. Dette er lærdommen fra steg 6 i praksis: én lokal skriving kan stoppe hele datastrømmen. Derfor er read-only ikke en høflig anmodning, men en regel som håndheves.
8. Fremmednøkler – og sletting hos eieren
Nå til selve poenget med øvelsen: referanseintegritet. Hos opptak – lag opptakets egen entitet, utdanningstilbudet, med fremmednøkkel mot den replikerte entitetstabellen. Legg merke til at nøkkelkolonnene fra utdanningsregisteret inngår rett i tilbudets egen primærnøkkel:
create table utdanningstilbud (
opptakstype_kode text not null,
opptak_kode int not null,
organisasjonskode text not null,
utdanningsmulighet_kode text not null,
periodekode text not null,
primary key (opptakstype_kode, opptak_kode,
organisasjonskode, utdanningsmulighet_kode, periodekode),
foreign key (organisasjonskode, utdanningsmulighet_kode, periodekode)
references utdanning.utdanningsinstans
);
insert into utdanningstilbud values
('SO', 2026, '7777', 'M-BIOLOGI', '2026H');
Fremmednøkkelen virker som normalt for lokale skriv – et tilbud mot en utdanningsinstans som ikke finnes, avvises:
insert into utdanningstilbud values
('SO', 2026, '9999', 'FINNES-IKKE', '2026H');
-- ERROR: insert or update on table "utdanningstilbud" violates foreign key constraint
Men hva om eieren sletter en utdanningsinstans som opptak har tilbud knyttet til? Hos utdanningsregisteret:
delete from utdanning.utdanningsinstans
where utdanningsmulighet_kode = 'M-BIOLOGI';
Hos opptak – slettingen gikk rett igjennom, uten feil, og tilbudet peker nå på ingenting:
select t.opptak_kode, t.utdanningsmulighet_kode
from utdanningstilbud t
left join utdanning.utdanningsinstans u
on (u.organisasjonskode, u.utdanningsmulighet_kode, u.periodekode)
= (t.organisasjonskode, t.utdanningsmulighet_kode, t.periodekode)
where u.organisasjonskode is null;
Dette er ikke en feil i oppsettet: endringer fra abonnementet tas imot med
session_replication_role = replica, som skrur av fremmednøkkelsjekkene. Fremmednøkkelen
beskytter mot lokale feil, men ikke mot sletting hos eieren. Det er derfor arkitekturdokumentet
krever avtalte sletteregler
– entiteter andre refererer til, markeres som utgåtte i stedet for å slettes.
9. Ta en titt under panseret
Status på abonnementet ser du hos opptak:
select subname, worker_type, received_lsn, latest_end_time
from pg_stat_subscription;
Hos utdanningsregisteret ligger replikeringsslotten, som holder styr på hvor langt mottakeren har kommet:
select slot_name, active, confirmed_flush_lsn from pg_replication_slots;
Slotten er også eierens forpliktelse: WAL beholdes helt til mottakeren har bekreftet endringene. Se det selv – stopp mottakeren og gjør endringer hos eieren:
docker compose stop opptak
Hos utdanningsregisteret:
update utdanning.utdanningsinstans set navn = navn || ' (justert)';
select slot_name, active,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) as etterslep
from pg_replication_slots;
Slotten er nå inaktiv, og etterslepet vokser for hver endring. I et lekemiljø er dette noen kilobyte – i produksjon, med en mottaker som er nede over dager, kan det fylle disken hos eieren. Start mottakeren igjen og se at den tar igjen det tapte av seg selv:
docker compose start opptak
Dette er grunnen til at både eier og mottaker må overvåke replikeringen: mottakeren følger med
på at abonnementet løper (pg_stat_subscription_stats), eieren på slots som blir hengende etter
(pg_replication_slots).
10. Rydd opp
Hos opptak – fjern abonnementet først. Det kobler seg til eieren og fjerner replikeringsslotten samtidig, slik at det ikke blir liggende igjen en slot som holder på WAL:
drop subscription utdanningsregisteret_utdanningsinstans;
Riv så hele miljøet:
docker compose down -v
Oppsummering
Du har nå vært igjennom hele livsløpet: publisere med kolonneliste, abonnere med initiell synkronisering, håndheve read-only med rettigheter, håndtere en stoppet replikering, se hvorfor fremmednøkler ikke beskytter mot sletting hos eieren, og hvorfor slots gjør replikering til en forpliktelse for eieren. Det er i praksis alt du trenger for å forstå mønstrene i arkitekturdokumentet – oppsettet i ekte miljøer handler mest om roller, nettverk og hemmeligheter, og dekkes i egne oppskrifter.
Se også
- Referensiell integritet på tvers av subgrafer – prinsippene og mønstrene dette understøtter
- Logical Replication i Postgres-dokumentasjonen