SQL Academy · Lektion

Genskab tilstand ud fra hændelser

Fold hændelser sammen til den aktuelle tilstand

Lektion 4 af 413 trin

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.

Gratis at komme i gang

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

  1. Hvorfor bevare historikken
  2. Hændelsestabeller, der kun tilføjes til
  3. Temporale og versionsstyrede rækker
  4. Genskab tilstand ud fra hændelser
← Tilbage til SQL Academy