Odtwarzanie stanu ze zdarzeń
Składaj zdarzenia w bieżący stan
Odtwarzanie stanu ze zdarzeń to bezpłatna lekcja SQL Academy na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej SQL Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs SQL Academy zawiera 4 lekcji w sumie.
Co oznacza odbudowywanie stanu?
W event sourcingu dane są przechowywane jako niezmienny dziennik zdarzeń, a nie jako modyfikowalne wiersze. Aby poznać bieżący stan dowolnego elementu, trzeba ponownie odtworzyć te zdarzenia i złożyć je w jeden wynik.
Nazywa się to odbudowywaniem stanu na podstawie zdarzeń. Proszę pomyśleć o koncie bankowym: zamiast przechowywać saldo, przechowuje się każdą wpłatę i wypłatę. Saldo jest zawsze sumą wszystkich tych zdarzeń.
Prosta tabela zdarzeń
Zacznijmy od utworzenia minimalnego dziennika zdarzeń dla systemu kont bankowych. Każdy wiersz reprezentuje coś, co się wydarzyło — wpłatę lub wypłatę — wraz z kwotą i znacznikiem czasu.
Ta tabela nigdy nie jest aktualizowana ani usuwana. Nowe fakty są zawsze dopisywane jako nowe wiersze.
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');Agregowanie zdarzeń do salda
Aby odtworzyć bieżące saldo, agregujemy wszystkie zdarzenia. Wpłaty zwiększają saldo, a wypłaty je zmniejszają. Wyrażenie CASE pozwala nadać każdemu typowi zdarzenia właściwy znak przed zsumowaniem.
To pojedyncze zapytanie pokazuje bieżący stan wyprowadzony wyłącznie z historycznego dziennika zdarzeń.
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;Stan w konkretnym momencie
Jedną z najpotężniejszych zalet event sourcingu jest możliwość odtworzenia stanu w dowolnym momencie. Wystarczy dodać filtr WHERE created_at <= :target_time przed agregowaniem.
Dzięki temu można wykonywać zapytania umożliwiające podróż w czasie bez konieczności wprowadzania dodatkowych zmian w schemacie — historia już znajduje się w dzienniku zdarzeń.
-- 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 narastające z funkcjami okna
Zamiast jednej sumy możemy obliczyć saldo narastające — saldo po każdym zdarzeniu. Funkcja okna SUM(...) OVER (ORDER BY ...) oblicza sumę skumulowaną w miarę dodawania zdarzeń w kolejności chronologicznej.
Jest to niezwykle przydatne w ścieżkach audytu i podczas debugowania zmian stanu.
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;Materializowanie stanu w tabeli migawek
Odtwarzanie wszystkich zdarzeń przy każdym zapytaniu może stać się kosztowne wraz ze wzrostem dziennika. Częstą optymalizacją jest materializowanie bieżącego stanu w tabeli migawek oraz jej okresowe lub wykonywane na żądanie odtwarzanie.
Snapshot przechowuje zagregowany wynik, a zapytania odczytują go zamiast za każdym razem odtwarzać cały dziennik.
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;Przyrostowe aktualizacje migawki
Gdy pojawiają się nowe zdarzenia, nie trzeba odtwarzać całej historii. Jeśli w migawce zapisano ostatni przetworzony event_id, można zastosować tylko deltę — zdarzenia, które pojawiły się po utworzeniu migawki.
Ten przyrostowy wzorzec zapewnia szybkie odświeżanie migawek nawet w przypadku dużych dzienników.
-- 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;Tabele temporalne i wersjonowanie systemowe
Standard SQL:2011 wprowadził systemowo wersjonowane tabele temporalne, które są utrzymywane przez samą bazę danych. Każdy wiersz automatycznie otrzymuje kolumny valid_from i valid_to zarządzane przez silnik bazy danych.
PostgreSQL nie obsługuje tego natywnie, ale można to emulować. Inne bazy danych, takie jak MariaDB i SQL Server, obsługują bezpośrednio konstrukcję 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');Odpytywanie historii temporalnej
Po utworzeniu emulowanej tabeli temporalnej można sprawdzić, jakie było saldo w dowolnym momencie w przeszłości, filtrując dane według zakresu obowiązywania. Wiersz, którego zakres zawiera docelowy znacznik czasu, przedstawia stan z tego momentu.
Ten wzorzec oddziela logikę zapytań od odtwarzania zdarzeń — tabela historii stanu jest już wstępnie zagregowana.
-- 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 z wieloma encjami
Rzeczywiste systemy śledzą jednocześnie zdarzenia dotyczące wielu encji. Wspólny dziennik zdarzeń z kolumnami entity_id i entity_type pozwala odtworzyć stan dowolnego obiektu z jednej tabeli.
W tym przykładzie śledzimy ruchy magazynowe dotyczące wielu produktów. Odtworzenie bieżącego stanu zapasów dla każdego produktu ponownie sprowadza się do agregacji według grup.
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;Używanie CTE dla przejrzystości
Zapytania odtwarzające stan mogą stać się złożone. Umieszczenie kroku agregowania w obiekcie CTE poprawia czytelność i pozwala przejrzyście połączyć odtworzony stan z innymi tabelami.
W tym przykładzie odtwarzamy salda kont, a następnie łączymy je z tabelą referencyjną kont, aby uwzględnić nazwiska właścicieli w wynikach.
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;Sprawdzenie wiedzy
Sprawdźmy, jak dobrze rozumieją Państwo odtwarzanie stanu ze zdarzeń w SQL.
Podsumowanie lekcji
W tej lekcji nauczyli się Państwo odtwarzać bieżący i historyczny stan z niezmiennego dziennika zdarzeń za pomocą SQL.
Najważniejsze informacje:
- Stan powstaje przez agregowanie (składanie) zdarzeń za pomocą wyrażenia
CASEokreślającego znak wewnątrzSUM. - Dodanie filtra czasowego zapewnia bezpłatnie zapytania dotyczące konkretnego momentu.
- Funkcje okna tworzą stan narastający po każdym zdarzeniu.
- Tabele migawek materializują zagregowany wynik na potrzeby wydajności, a przyrostowe aktualizacje uwzględniają tylko nowe zdarzenia.
- Emulowane tabele temporalne przechowują wstępnie zagregowane wiersze stanu z zakresami obowiązywania, co umożliwia szybkie wyszukiwanie danych historycznych.
- CTE zapewniają czytelność zapytań odtwarzających stan, gdy trzeba połączyć wyprowadzony stan z innymi tabelami.
Wzorce te stanowią podstawę projektowania baz danych opartych na event sourcingu i ułatwiających audyt.
Ucz się SQL dzięki korepetycjom AI — za darmo
Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.
- Kursy
- 46
- Lekcje
- 183
Często zadawane pytania
Czy lekcja „Odtwarzanie stanu ze zdarzeń” jest bezpłatna?
Tak — pełny tekst „Odtwarzanie stanu ze zdarzeń” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu SQL Academy, przejdź na CoddyKit PRO. Kurs SQL Academy zawiera 4 lekcji w sumie.
Co nauczysz się w „Odtwarzanie stanu ze zdarzeń”?
Składaj zdarzenia w bieżący stan Ćwiczysz SQL Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć SQL Academy?
Nie wymagamy żadnego doświadczenia. SQL Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.
Ile czasu zajmuje lekcja „Odtwarzanie stanu ze zdarzeń”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji SQL Academy?
Tak. Każda lekcja SQL Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Dlaczego warto przechowywać historię
- Tabele zdarzeń tylko do dopisywania
- Wiersze czasowe i wersjonowane
- Odtwarzanie stanu ze zdarzeń