0Pricing
SQL Academy · Aula

Reconstruindo o Estado a Partir de Eventos

Combine eventos para obter o estado atual.

Reconstruindo o Estado a Partir de Eventos é uma aula grátis de SQL Academy no CoddyKit. Esta é a aula 4 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de SQL Academy, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de SQL Academy inclui 4 aulas no total.

O que significa reconstruir o estado?

Na origem de eventos, os dados são armazenados como um registro imutável de eventos, e não como linhas mutáveis. Para conhecer o estado atual de qualquer elemento, você precisa reproduzir esses eventos e agregá-los em um único resultado.

Isso é chamado de reconstrução do estado a partir de eventos. Pense em uma conta bancária: em vez de armazenar o saldo, você armazena cada depósito e saque. O saldo é sempre a soma de todos esses eventos.

Uma tabela simples de eventos

Vamos começar criando um registro mínimo de eventos para um sistema de contas bancárias. Cada linha representa algo que aconteceu — um depósito ou um saque — com o valor e o carimbo de data e hora.

Esta tabela nunca é atualizada nem excluída. Novos fatos são sempre acrescentados como novas linhas.

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');

Incorporando eventos a um saldo

Para reconstruir o saldo atual, agregamos todos os eventos. Os depósitos aumentam o saldo, e os saques o reduzem. Uma expressão CASE permite tratar cada tipo de evento com o sinal correto antes da soma.

Essa única consulta fornece o estado atual, derivado inteiramente do registro histórico de eventos.

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;

Estado em um instante específico

Um dos aspectos mais poderosos da origem de eventos é a capacidade de reconstruir o estado em qualquer instante. Basta adicionar um filtro WHERE created_at <= :target_time antes da agregação.

Isso fornece uma consulta que permite viajar no tempo, sem exigir alterações adicionais no esquema — o histórico já está no registro de eventos.

-- 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 acumulado com funções de janela

Em vez de um único total, podemos calcular um saldo acumulado — o saldo depois de cada evento. A função de janela SUM(...) OVER (ORDER BY ...) calcula a soma acumulada à medida que os eventos se acumulam em ordem cronológica.

Isso é extremamente útil para trilhas de auditoria e para depurar transições de estado.

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;

Materializando o estado em uma tabela de instantâneos

Reproduzir todos os eventos a cada consulta pode ficar caro à medida que o registro cresce. Uma otimização comum é materializar o estado atual em uma tabela de instantâneos e reconstruí-la periodicamente ou sob demanda.

O instantâneo armazena o resultado agregado; as consultas leem o instantâneo em vez de reproduzir todo o registro sempre.

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;

Atualizações incrementais de instantâneos

Quando chegam novos eventos, você não precisa reproduzir todo o histórico. Se tiver registrado o último event_id processado no instantâneo, poderá aplicar apenas a diferença — os eventos que chegaram depois da criação do instantâneo.

Esse padrão incremental mantém as atualizações dos instantâneos rápidas, mesmo em registros grandes.

-- 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;

Tabelas temporais e versionamento do sistema

O SQL:2011 introduziu tabelas temporais com versionamento do sistema, que são mantidas pelo próprio banco de dados. Cada linha recebe automaticamente as colunas valid_from e valid_to, gerenciadas pelo mecanismo.

O PostgreSQL não oferece suporte nativo a isso, mas você pode simulá-lo. Outros bancos de dados, como MariaDB e SQL Server, oferecem suporte direto a 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');

Consultando o histórico temporal

Com a tabela temporal simulada pronta, você pode perguntar qual era o saldo em qualquer momento do passado filtrando pelo intervalo de validade. A linha cujo intervalo contém o carimbo de data e hora desejado representa o estado naquele momento.

Esse padrão separa a lógica da consulta da reprodução de eventos — a tabela de histórico de estados já contém o resultado agregado.

-- 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';

Origem de eventos com várias entidades

Sistemas reais acompanham eventos de muitas entidades ao mesmo tempo. Um registro de eventos compartilhado, com as colunas entity_id e entity_type, permite reconstruir o estado de qualquer objeto a partir de uma única tabela.

Aqui acompanhamos as movimentações de estoque de vários produtos. Reconstruir o estoque atual de cada produto é novamente apenas uma agregação agrupada.

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;

Usando CTEs para ganhar clareza

As consultas para reconstruir o estado podem ficar complexas. Envolver a etapa de agregação em uma CTE melhora a legibilidade e permite combinar o estado reconstruído com outras tabelas de forma organizada.

Aqui reconstruímos os saldos das contas e depois os combinamos com uma tabela de referência de contas para incluir os nomes dos proprietários no resultado.

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ção de conhecimento

Avalie sua compreensão sobre como reconstruir estados a partir de eventos em SQL.

Recapitulação da lição

Nesta lição, você aprendeu a reconstruir estados atuais e históricos a partir de um registro imutável de eventos usando SQL.

Principais aprendizados:

  • O estado é derivado ao combinar (agregar) eventos com uma expressão CASE com sinais dentro de SUM.
  • Adicionar um filtro de carimbo de data e hora fornece consultas para qualquer instante sem esforço adicional.
  • As funções de janela produzem um estado acumulado depois de cada evento.
  • As tabelas de instantâneos materializam o resultado agregado para melhorar o desempenho; as atualizações incrementais aplicam apenas os eventos novos.
  • As tabelas temporais simuladas armazenam linhas de estado já agregadas, com intervalos de validade para consultas históricas rápidas.
  • As CTEs mantêm legíveis as consultas de reconstrução quando você precisa combinar o estado derivado com outras tabelas.

Esses padrões são a base de projetos de bancos de dados orientados a eventos e preparados para auditoria.

Perguntas Frequentes

A aula “Reconstruindo o Estado a Partir de Eventos” é grátis?

Sim — o texto completo de “Reconstruindo o Estado a Partir de Eventos” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de SQL Academy, atualize para CoddyKit PRO. O curso de SQL Academy inclui 4 aulas no total.

O que vou aprender em “Reconstruindo o Estado a Partir de Eventos”?

Combine eventos para obter o estado atual. Você pratica SQL Academy com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar SQL Academy?

Nenhuma experiência prévia é necessária. SQL Academy no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 4 de 4.

Quanto tempo leva a aula “Reconstruindo o Estado a Partir de Eventos”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de SQL Academy?

Sim. Cada aula de SQL Academy inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. Por Que Manter o Histórico
  2. Tabelas de Eventos Somente para Acréscimo
  3. Linhas Temporais e Versionadas
  4. Reconstruindo o Estado a Partir de Eventos
← Voltar para SQL Academy