Ricostruire lo stato dagli eventi
Integri gli eventi nello stato corrente.
Ricostruire lo stato dagli eventi è una lezione SQL Academy gratuita su CoddyKit. Questa è la lezione 4 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento SQL Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso SQL Academy include 4 lezioni in totale.
Che cosa significa ricostruire lo stato
Nell'event sourcing, i dati vengono memorizzati come un registro immutabile di eventi anziché come righe modificabili. Per conoscere lo stato attuale di qualsiasi elemento, è necessario riprodurre quegli eventi e combinarli in un unico risultato.
Questo processo viene chiamato ricostruzione dello stato dagli eventi. Pensate a un conto bancario: invece di memorizzare il saldo, si conservano tutti i depositi e i prelievi. Il saldo è sempre la somma di tutti questi eventi.
Una semplice tabella di eventi
Iniziamo creando un registro eventi minimale per un sistema di conti bancari. Ogni riga rappresenta qualcosa che è accaduto, un deposito o un prelievo, insieme all'importo e al timestamp.
Questa tabella non viene mai aggiornata né eliminata. I nuovi fatti vengono sempre aggiunti come nuove righe.
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');Aggregare gli eventi in un saldo
Per ricostruire il saldo corrente, si aggregano tutti gli eventi. I depositi aumentano il saldo e i prelievi lo diminuiscono. Un'espressione CASE permette di applicare il segno corretto a ogni tipo di evento prima della somma.
Questa singola query restituisce lo stato attuale, ricavato interamente dal log storico degli eventi.
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;Stato in un momento specifico
Uno degli aspetti più potenti dell'event sourcing è la possibilità di ricostruire lo stato in qualsiasi momento. È sufficiente aggiungere il filtro WHERE created_at <= :target_time prima dell'aggregazione.
Questo consente di eseguire una query con viaggio nel tempo senza dover modificare lo schema: la cronologia è già nel log degli eventi.
-- 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;Saldo progressivo con le funzioni finestra
Anziché un unico totale, è possibile calcolare un saldo progressivo, cioè il saldo dopo ogni evento. La funzione finestra SUM(...) OVER (ORDER BY ...) calcola la somma cumulativa man mano che gli eventi si accumulano in ordine cronologico.
È estremamente utile per le tracce di audit e per il debug delle transizioni di stato.
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;Materializzare lo stato in una tabella snapshot
Riprodurre tutti gli eventi a ogni query può diventare costoso man mano che il log cresce. Un'ottimizzazione comune consiste nel materializzare lo stato corrente in una tabella snapshot e ricostruirlo periodicamente o su richiesta.
Lo snapshot memorizza il risultato aggregato; le query leggono dallo snapshot anziché riprodurre ogni volta l'intero log.
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;Aggiornamenti incrementali dello snapshot
Quando arrivano nuovi eventi, non è necessario riprodurre l'intera cronologia. Se nello snapshot è stato registrato l'ultimo event_id elaborato, si può applicare solo il delta: gli eventi arrivati dopo la creazione dello snapshot.
Questo approccio incrementale mantiene rapidi gli aggiornamenti dello snapshot anche con log di grandi dimensioni.
-- 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;Tabelle temporali e versionamento di sistema
SQL:2011 ha introdotto le tabelle temporali con versionamento di sistema, gestite direttamente dal database. Ogni riga riceve automaticamente le colonne valid_from e valid_to, gestite dal motore.
PostgreSQL non le supporta nativamente, ma è possibile emularle. Altri database, come MariaDB e SQL Server, supportano direttamente WITH SYSTEM VERSIONING.
-- 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');Interrogare la cronologia temporale
Con la tabella temporale emulata, si può chiedere quale fosse il saldo in qualsiasi momento passato filtrando in base all'intervallo di validità. La riga il cui intervallo contiene il timestamp di destinazione rappresenta lo stato in quel momento.
Questo approccio separa la logica delle query dalla riproduzione degli eventi: la tabella della cronologia dello stato contiene già i dati aggregati.
-- 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 con più entità
I sistemi reali tengono traccia degli eventi di molte entità contemporaneamente. Un log condiviso con una colonna entity_id e una entity_type consente di ricostruire lo stato di qualsiasi oggetto da un'unica tabella.
Qui si tracciano i movimenti di inventario di più prodotti. Ricostruire la giacenza corrente per ogni prodotto consiste ancora una volta in un'aggregazione raggruppata.
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;Usare le CTE per maggiore chiarezza
Le query per ricostruire lo stato possono diventare complesse. Racchiudere il passaggio di aggregazione in una CTE ne migliora la leggibilità e permette di collegare facilmente lo stato ricostruito ad altre tabelle.
Qui si ricostruiscono i saldi dei conti e poi li si collega a una tabella di riferimento dei conti per includere nell'output i nomi dei titolari.
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;Verifica delle conoscenze
Verifichi di aver compreso come ricostruire lo stato dagli eventi in SQL.
Riepilogo della lezione
In questa lezione ha imparato a ricostruire lo stato corrente e storico da un log di eventi immutabile usando SQL.
Punti chiave:
- Lo stato deriva dal folding (aggregazione) degli eventi con un'espressione
CASEche assegna il segno all'interno diSUM. - L'aggiunta di un filtro temporale offre gratuitamente query relative a un momento specifico.
- Le funzioni finestra producono uno stato progressivo dopo ogni evento.
- Le tabelle snapshot materializzano il risultato aggregato per migliorare le prestazioni; gli aggiornamenti incrementali applicano solo i nuovi eventi.
- Le tabelle temporali emulate memorizzano righe di stato già aggregate con intervalli di validità per consultazioni storiche rapide.
- Le CTE mantengono leggibili le query di ricostruzione quando è necessario collegare lo stato derivato ad altre tabelle.
Questi approcci sono alla base dei progetti di database basati sull'event sourcing e adatti agli audit.
Domande Frequenti
La lezione «Ricostruire lo stato dagli eventi» è gratuita?
Sì — il testo completo di «Ricostruire lo stato dagli eventi» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso SQL Academy, passa a CoddyKit PRO. Il corso SQL Academy include 4 lezioni in totale.
Cosa imparerò in «Ricostruire lo stato dagli eventi»?
Integri gli eventi nello stato corrente. Eserciti SQL Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare SQL Academy?
Non è richiesta alcuna esperienza precedente. SQL Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 4 di 4.
Quanto tempo richiede la lezione «Ricostruire lo stato dagli eventi»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione SQL Academy?
Sì. Ogni lezione SQL Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Perché conservare la cronologia
- Tabelle di eventi append-only
- Righe temporali e versionate
- Ricostruire lo stato dagli eventi