Genskab tilstand ud fra hændelser
Fold hændelser sammen til den aktuelle tilstand
Genskab tilstand ud fra hændelser er en gratis SQL Academy-lektion på CoddyKit. Dette er lektion 4 af 4. Du kan læse hele lektionen gratis nedenfor — og derefter øve dig praktisk i browseren med en indbygget kodeeditor og en AI-vejleder, der er tilgængelig døgnet rundt. Den er en del af læringsforløbet i SQL Academy, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. SQL Academy-kurset indeholder 4 lektioner i alt.
Hvad betyder det at genopbygge tilstanden
I event sourcing gemmes data som en uforanderlig log af hændelser i stedet for som foranderlige rækker. Hvis du vil kende den aktuelle tilstand for noget, skal du afspille hændelserne og samle dem til ét resultat.
Det kaldes at genopbygge tilstanden ud fra hændelser. Tænk på en bankkonto: I stedet for at gemme saldoen gemmer du hver indbetaling og hævning. Saldoen er altid summen af alle disse hændelser.
En enkel hændelsestabel
Lad os begynde med at oprette en minimal hændelseslog til et system med bankkonti. Hver række repræsenterer noget, der skete — en indbetaling eller en hævning — med beløbet og tidsstemplet.
Denne tabel bliver aldrig opdateret eller slettet fra. Nye fakta føjes altid til som nye rækker.
CREATE TABLE account_events (
event_id SERIAL PRIMARY KEY,
account_id INT NOT NULL,
event_type VARCHAR(20) NOT NULL, -- 'deposit' or 'withdrawal'
amount NUMERIC(12, 2) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
INSERT INTO account_events (account_id, event_type, amount, created_at) VALUES
(1, 'deposit', 1000.00, '2024-01-01 09:00:00+00'),
(1, 'deposit', 500.00, '2024-01-03 14:00:00+00'),
(1, 'withdrawal', 200.00, '2024-01-05 10:00:00+00'),
(1, 'deposit', 300.00, '2024-01-07 11:00:00+00'),
(1, 'withdrawal', 150.00, '2024-01-09 16:00:00+00');Sammenfoldning af hændelser til en saldo
For at genopbygge den aktuelle saldo sammenlægger vi alle hændelser. Indbetalinger lægges til saldoen, og hævninger trækkes fra. Et CASE-udtryk lader os behandle hver hændelsestype med det korrekte fortegn, før vi summerer.
Denne ene forespørgsel giver os den aktuelle tilstand, som udelukkende er afledt af den historiske hændelseslog.
SELECT
account_id,
SUM(
CASE event_type
WHEN 'deposit' THEN amount
WHEN 'withdrawal' THEN -amount
ELSE 0
END
) AS current_balance
FROM account_events
WHERE account_id = 1
GROUP BY account_id;Tilstand på et bestemt tidspunkt
En af de mest kraftfulde egenskaber ved event sourcing er muligheden for at genskabe tilstanden på et hvilket som helst tidspunkt. Du skal blot tilføje et WHERE created_at <= :target_time-filter før sammenlægningen.
Det giver dig en tidsrejseforespørgsel uden behov for yderligere skemaændringer — historikken findes allerede i hændelsesloggen.
-- What was the balance at the end of January 5th?
SELECT
account_id,
SUM(
CASE event_type
WHEN 'deposit' THEN amount
WHEN 'withdrawal' THEN -amount
ELSE 0
END
) AS balance_at_snapshot
FROM account_events
WHERE account_id = 1
AND created_at <= '2024-01-05 23:59:59+00'
GROUP BY account_id;Løbende saldo med vinduesfunktioner
I stedet for én samlet total kan vi beregne en løbende saldo — saldoen efter hver hændelse. Vinduesfunktionen SUM(...) OVER (ORDER BY ...) beregner den kumulative sum, efterhånden som hændelserne samles i kronologisk rækkefølge.
Det er særdeles nyttigt til revisionsspor og fejlfinding af tilstandsovergange.
SELECT
event_id,
created_at,
event_type,
amount,
SUM(
CASE event_type
WHEN 'deposit' THEN amount
WHEN 'withdrawal' THEN -amount
ELSE 0
END
) OVER (PARTITION BY account_id ORDER BY created_at, event_id)
AS running_balance
FROM account_events
WHERE account_id = 1
ORDER BY created_at, event_id;Materialisering af tilstand i en øjebliksbilledtabel
Det kan blive dyrt at afspille alle hændelser ved hver forespørgsel, efterhånden som loggen vokser. En almindelig optimering er at materialisere den aktuelle tilstand i en øjebliksbilledtabel og genopbygge den med jævne mellemrum eller efter behov.
Øjebliksbilledet gemmer det sammenfoldede resultat, så forespørgsler læser fra øjebliksbilledet i stedet for at afspille hele loggen hver gang.
CREATE TABLE account_snapshots (
account_id INT PRIMARY KEY,
current_balance NUMERIC(12, 2) NOT NULL,
as_of_event_id INT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Populate / refresh the snapshot from the event log
INSERT INTO account_snapshots (account_id, current_balance, as_of_event_id, updated_at)
SELECT
account_id,
SUM(CASE event_type WHEN 'deposit' THEN amount WHEN 'withdrawal' THEN -amount ELSE 0 END),
MAX(event_id),
NOW()
FROM account_events
GROUP BY account_id
ON CONFLICT (account_id) DO UPDATE
SET current_balance = EXCLUDED.current_balance,
as_of_event_id = EXCLUDED.as_of_event_id,
updated_at = EXCLUDED.updated_at;Trinvise opdateringer af øjebliksbilledet
Når der kommer nye hændelser, behøver du ikke at afspille hele historikken igen. Hvis du gemte det senest behandlede event_id i øjebliksbilledet, kan du kun anvende ændringen — de hændelser, der kom til, efter øjebliksbilledet blev taget.
Dette trinvise mønster holder opdateringen af øjebliksbilleder hurtig, selv i store logge.
-- Apply only new events since the last snapshot
UPDATE account_snapshots AS snap
SET
current_balance = snap.current_balance + delta.net,
as_of_event_id = delta.max_event_id,
updated_at = NOW()
FROM (
SELECT
ae.account_id,
SUM(CASE ae.event_type WHEN 'deposit' THEN ae.amount WHEN 'withdrawal' THEN -ae.amount ELSE 0 END) AS net,
MAX(ae.event_id) AS max_event_id
FROM account_events ae
JOIN account_snapshots s ON s.account_id = ae.account_id
WHERE ae.event_id > s.as_of_event_id
GROUP BY ae.account_id
) AS delta
WHERE snap.account_id = delta.account_id;Temporale tabeller og systemversionering
SQL:2011 introducerede systemversionerede temporale tabeller, som databasen selv vedligeholder. Hver række får automatisk kolonnerne valid_from og valid_to, som styres af databasemotoren.
PostgreSQL understøtter ikke dette indbygget, men du kan efterligne det. Andre databaser som MariaDB og SQL Server understøtter WITH SYSTEM VERSIONING direkte.
-- Emulating a temporal table in PostgreSQL
CREATE TABLE account_state_history (
account_id INT NOT NULL,
current_balance NUMERIC(12, 2) NOT NULL,
valid_from TIMESTAMPTZ NOT NULL,
valid_to TIMESTAMPTZ NOT NULL DEFAULT 'infinity'
);
-- Insert initial state
INSERT INTO account_state_history (account_id, current_balance, valid_from)
VALUES (1, 1000.00, '2024-01-01 09:00:00+00');
-- On update: close old row, insert new row
UPDATE account_state_history
SET valid_to = '2024-01-03 14:00:00+00'
WHERE account_id = 1 AND valid_to = 'infinity';
INSERT INTO account_state_history (account_id, current_balance, valid_from)
VALUES (1, 1500.00, '2024-01-03 14:00:00+00');Forespørgsler i temporal historik
Når den efterlignede temporale tabel er på plads, kan du spørge, hvad saldoen var på et hvilket som helst tidligere tidspunkt, ved at filtrere på gyldighedsintervallet. Den række, hvis interval indeholder måltidspunktet, repræsenterer tilstanden på det tidspunkt.
Dette mønster adskiller forespørgselslogikken fra hændelsesafspilningen — tabellen med tilstandshistorikken er allerede sammenfoldet.
-- What was the account balance on January 4th?
SELECT
account_id,
current_balance,
valid_from,
valid_to
FROM account_state_history
WHERE account_id = 1
AND valid_from <= '2024-01-04 00:00:00+00'
AND valid_to > '2024-01-04 00:00:00+00';Event sourcing med flere entiteter
Virkelige systemer sporer hændelser for mange entiteter på én gang. En fælles hændelseslog med kolonnerne entity_id og entity_type lader dig genopbygge tilstanden for ethvert objekt fra én enkelt tabel.
Her sporer vi lagerbevægelser på tværs af flere produkter. At genopbygge den aktuelle lagerbeholdning for hvert produkt er igen blot en grupperet sammenlægning.
CREATE TABLE inventory_events (
event_id SERIAL PRIMARY KEY,
product_id INT NOT NULL,
event_type VARCHAR(20) NOT NULL, -- 'received', 'shipped', 'adjusted'
quantity INT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
INSERT INTO inventory_events (product_id, event_type, quantity, created_at) VALUES
(101, 'received', 200, '2024-03-01 08:00:00+00'),
(101, 'shipped', 50, '2024-03-02 12:00:00+00'),
(101, 'shipped', 30, '2024-03-04 15:00:00+00'),
(102, 'received', 150, '2024-03-01 08:00:00+00'),
(102, 'adjusted', -10, '2024-03-03 09:00:00+00');
-- Rebuild current stock for all products
SELECT
product_id,
SUM(CASE event_type WHEN 'received' THEN quantity WHEN 'shipped' THEN -quantity ELSE quantity END) AS stock_on_hand
FROM inventory_events
GROUP BY product_id
ORDER BY product_id;Brug af CTE'er for bedre overskuelighed
Forespørgsler, der genopbygger tilstand, kan blive komplekse. Hvis du pakker sammenfoldningstrinnet ind i en CTE, bliver forespørgslen lettere at læse, og du kan nemt forbinde den genopbyggede tilstand med andre tabeller.
Her genopbygger vi kontosaldi og forbinder dem derefter med en referencetabel for konti, så ejeres navne kommer med i resultatet.
CREATE TABLE accounts (
account_id INT PRIMARY KEY,
owner_name VARCHAR(100) NOT NULL
);
INSERT INTO accounts (account_id, owner_name) VALUES
(1, 'Alice'),
(2, 'Bob');
INSERT INTO account_events (account_id, event_type, amount, created_at) VALUES
(2, 'deposit', 2000.00, '2024-01-02 10:00:00+00'),
(2, 'withdrawal', 400.00, '2024-01-06 11:00:00+00');
WITH rebuilt_balances AS (
SELECT
account_id,
SUM(CASE event_type WHEN 'deposit' THEN amount WHEN 'withdrawal' THEN -amount ELSE 0 END) AS balance
FROM account_events
GROUP BY account_id
)
SELECT
a.account_id,
a.owner_name,
rb.balance
FROM accounts a
JOIN rebuilt_balances rb USING (account_id)
ORDER BY a.account_id;Test din viden
Test din forståelse af, hvordan man genopbygger tilstand fra hændelser i SQL.
Opsummering af lektionen
I denne lektion lærte du, hvordan du genopbygger aktuel og historisk tilstand fra en uforanderlig hændelseslog ved hjælp af SQL.
Vigtigste pointer:
- Tilstanden afledes ved at sammenfolde (sammenlægge) hændelser med et CASE-udtryk med fortegn inde i
SUM. - Et tidsstempelfilter giver dig forespørgsler på et bestemt tidspunkt uden ekstra arbejde.
- Vinduesfunktioner producerer en løbende tilstand efter hver hændelse.
- Øjebliksbilledtabeller materialiserer det sammenfoldede resultat af hensyn til ydeevnen, mens trinvise opdateringer kun anvender nye hændelser.
- Efterlignede temporale tabeller gemmer på forhånd sammenfoldede tilstandsrækker med gyldighedsintervaller, så historikken kan slås hurtigt op.
- CTE'er holder forespørgsler til genopbygning læsbare, når du skal forbinde den afledte tilstand med andre tabeller.
Disse mønstre er grundlaget for hændelsesbaserede og revisionsvenlige databasedesigns.
Lær SQL med en AI-underviser — gratis
Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.
- Kurser
- 46
- Lektioner
- 183
Ofte stillede spørgsmål
Er lektionen “Genskab tilstand ud fra hændelser” gratis?
Ja — hele teksten til “Genskab tilstand ud fra hændelser” kan læses gratis her på nettet. Hvis du vil øve dig interaktivt med en indbygget kodeeditor og en AI-vejleder døgnet rundt og få adgang til resten af SQL Academy-kurset, skal du opgradere til CoddyKit PRO. SQL Academy-kurset indeholder 4 lektioner i alt.
Hvad lærer jeg i “Genskab tilstand ud fra hændelser”?
Fold hændelser sammen til den aktuelle tilstand Du øver dig i SQL Academy med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.
Skal jeg have erfaring for at begynde på SQL Academy?
Der kræves ingen tidligere erfaring. SQL Academy på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 4 af 4.
Hvor lang tid tager lektionen “Genskab tilstand ud fra hændelser”?
De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.
Kan jeg skrive og køre kode i denne SQL Academy-lektion?
Ja. Alle SQL Academy-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.
Alle lektioner i dette kursus
- Hvorfor bevare historikken
- Hændelsestabeller, der kun tilføjes til
- Temporale og versionsstyrede rækker
- Genskab tilstand ud fra hændelser