Reconstrucción del estado a partir de eventos
Integre los eventos para obtener el estado actual.
Reconstrucción del estado a partir de eventos es una lección gratuita de SQL Academy en CoddyKit. Esta es la lección 4 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de SQL Academy, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de SQL Academy incluye 4 lecciones en total.
¿Qué significa reconstruir el estado?
En event sourcing, los datos se almacenan como un registro inmutable de eventos, en lugar de como filas mutables. Para conocer el estado actual de cualquier elemento, debe reproducir esos eventos y combinarlos para obtener un único resultado.
Esto se denomina reconstruir el estado a partir de eventos. Piense en una cuenta bancaria: en lugar de almacenar el saldo, se guardan todos los depósitos y las retiradas. El saldo siempre es la suma de todos esos eventos.
Una tabla de eventos sencilla
Comencemos creando un registro de eventos mínimo para un sistema de cuentas bancarias. Cada fila representa algo que ocurrió —un depósito o una retirada—, junto con el importe y la marca de tiempo.
Esta tabla nunca se actualiza ni se elimina. Los hechos nuevos siempre se añaden como filas nuevas.
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');Integrar eventos en un saldo
Para reconstruir el saldo actual, agregamos todos los eventos. Los depósitos aumentan el saldo y los retiros lo disminuyen. Una expresión CASE permite tratar cada tipo de evento con el signo adecuado antes de sumarlo.
Esta única consulta nos proporciona el estado actual, derivado completamente del 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 en un momento dado
Uno de los aspectos más potentes del event sourcing es la posibilidad de reconstruir el estado en cualquier momento. Basta con añadir un filtro WHERE created_at <= :target_time antes de agregar los datos.
Esto le proporciona una consulta de viaje temporal sin necesidad de cambios adicionales en el esquema: el historial ya está en el 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 con funciones de ventana
En lugar de un único total, podemos calcular un saldo acumulado: el saldo después de cada evento. La función de ventana SUM(...) OVER (ORDER BY ...) calcula la suma acumulada a medida que los eventos se acumulan en orden cronológico.
Esto resulta extremadamente útil para los registros de auditoría y la depuración de las transiciones 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;Materializar el estado en una tabla de instantáneas
Reproducir todos los eventos en cada consulta puede volverse costoso a medida que crece el registro. Una optimización habitual consiste en materializar el estado actual en una tabla de instantáneas y reconstruirla periódicamente o cuando se necesite.
La instantánea almacena el resultado de la agregación; las consultas leen de ella en lugar de reproducir todo el registro cada vez.
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;Actualizaciones incrementales de la instantánea
Cuando llegan eventos nuevos, no tiene que reproducir todo el historial. Si registró el último event_id procesado en la instantánea, puede aplicar solo el delta: los eventos que llegaron después de tomar la instantánea.
Este patrón incremental mantiene rápidas las actualizaciones de la instantánea incluso con 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;Tablas temporales y versionado del sistema
SQL:2011 introdujo las tablas temporales con versionado del sistema, que la propia base de datos mantiene. Cada fila recibe automáticamente las columnas valid_from y valid_to, gestionadas por el motor.
PostgreSQL no admite esta función de forma nativa, pero puede emularla. Otras bases de datos, como MariaDB y SQL Server, admiten WITH SYSTEM VERSIONING directamente.
-- 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');Consultar el historial temporal
Con la tabla temporal emulada ya disponible, puede consultar cuál era el saldo en cualquier momento pasado filtrando por el intervalo de validez. La fila cuyo intervalo contiene la marca de tiempo objetivo representa el estado en ese momento.
Este patrón desacopla la lógica de consulta de la reproducción de eventos: la tabla del historial de estados ya contiene el resultado preagregado.
-- 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 varias entidades
Los sistemas reales registran eventos de muchas entidades a la vez. Un registro de eventos compartido con una columna entity_id y otra entity_type permite reconstruir el estado de cualquier objeto desde una sola tabla.
En este caso registramos movimientos de inventario de varios productos. Reconstruir las existencias actuales de cada producto vuelve a ser simplemente una agregación 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;Uso de CTE para mayor claridad
Las consultas para reconstruir el estado pueden volverse complejas. Envolver el paso de agregación en una CTE mejora la legibilidad y permite combinar limpiamente el estado reconstruido con otras tablas.
Aquí reconstruimos los saldos de las cuentas y luego los combinamos con una tabla de referencia de cuentas para incluir los nombres de los titulares en el 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;Comprobación de conocimientos
Ponga a prueba su comprensión de la reconstrucción del estado a partir de eventos en SQL.
Resumen de la lección
En esta lección ha aprendido a reconstruir el estado actual e histórico a partir de un registro de eventos inmutable mediante SQL.
Conclusiones clave:
- El estado se deriva al plegar (agregar) eventos con una expresión
CASEcon signo dentro deSUM. - Añadir un filtro de marca de tiempo le proporciona consultas en un momento dado sin esfuerzo adicional.
- Las funciones de ventana producen un estado acumulado después de cada evento.
- Las tablas de instantáneas materializan el resultado agregado para mejorar el rendimiento; las actualizaciones incrementales solo aplican los eventos nuevos.
- Las tablas temporales emuladas almacenan filas de estado preagregadas con intervalos de validez para realizar consultas históricas rápidas.
- Las CTE mantienen legibles las consultas de reconstrucción cuando necesita combinar el estado derivado con otras tablas.
Estos patrones son la base de los diseños de bases de datos basados en event sourcing y orientados a la auditoría.
Preguntas frecuentes
¿La lección «Reconstrucción del estado a partir de eventos» es gratis?
Sí — el texto completo de «Reconstrucción del estado a partir de eventos» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de SQL Academy, actualiza a CoddyKit PRO. El curso de SQL Academy incluye 4 lecciones en total.
¿Qué aprenderé en «Reconstrucción del estado a partir de eventos»?
Integre los eventos para obtener el estado actual. Practicas SQL Academy con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.
¿Necesito experiencia previa para empezar SQL Academy?
No se requiere experiencia previa. SQL Academy en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 4 de 4.
¿Cuánto tiempo toma la lección «Reconstrucción del estado a partir de eventos»?
La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.
¿Puedo escribir y ejecutar código en esta lección de SQL Academy?
Sí. Cada lección de SQL Academy incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.
Todas las lecciones de este curso
- Por qué conservar el historial
- Tablas de eventos de solo anexado
- Filas temporales y versionadas
- Reconstrucción del estado a partir de eventos