Таблицы событий только для добавления
Записывайте произошедшее, не перезаписывая данные
«Таблицы событий только для добавления» — бесплатный урок SQL Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения SQL Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс SQL Academy содержит 4 уроков всего.
Что такое таблица только для добавления
Таблица событий только для добавления — это таблица, в которую строки только добавляются: их никогда не обновляют и не удаляют. Каждая строка представляет то, что произошло в определённый момент времени.
Этот шаблон лежит в основе построения состояния из событий. Вместо хранения текущего состояния Вы сохраняете каждое изменение как неизменяемое событие и получаете полную историю, пригодную для аудита.
Создание таблицы событий
Хорошо спроектированная таблица событий фиксирует, кто выполнил какое действие над каким ресурсом и когда. Столбец occurred_at записывает точную отметку времени, а DEFAULT NOW() гарантирует, что это значение всегда будет заполнено автоматически.
Обратите внимание: в этой схеме нет UPDATE или DELETE — после записи строки становятся постоянными.
CREATE TABLE account_events (
id BIGSERIAL PRIMARY KEY,
account_id BIGINT NOT NULL,
event_type TEXT NOT NULL,
payload JSONB,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);Добавление событий
Каждое действие пользователя — вход в систему, внесение денег, изменение адреса электронной почты — становится новой строкой. Вы никогда не возвращаетесь к предыдущему событию и не редактируете его. Если что-то нужно исправить, вместо этого добавляется компенсирующее событие.
Так сохраняется полная последовательность произошедшего в правильном порядке.
INSERT INTO account_events (account_id, event_type, payload)
VALUES
(42, 'account_opened', '{"plan": "free"}'),
(42, 'email_verified', '{"email": "alice@example.com"}'),
(42, 'plan_upgraded', '{"from": "free", "to": "pro"}');Чтение полной истории
Поскольку каждое изменение состояния хранится в виде строки, получить полную историю счёта можно с помощью простого SELECT, упорядоченного по времени. Вы можете воспроизвести весь жизненный цикл записи — от самого первого события до последнего.
SELECT
id,
event_type,
payload,
occurred_at
FROM account_events
WHERE account_id = 42
ORDER BY occurred_at ASC;Вывод текущего состояния
В таблице только для добавления текущее состояние не хранится напрямую — Вы выводите его, читая последнее подходящее событие. Здесь текущий тариф счёта 42 определяется самым последним событием plan_upgraded или account_opened.
Конструкция ORDER BY occurred_at DESC LIMIT 1 позволяет эффективно получить самый свежий снимок состояния.
SELECT payload->>'to' AS current_plan
FROM account_events
WHERE account_id = 42
AND event_type IN ('account_opened', 'plan_upgraded')
ORDER BY occurred_at DESC
LIMIT 1;Обеспечение неизменяемости с помощью правил
Гарантию режима только для добавления можно обеспечить на уровне базы данных с помощью RULE, который молча игнорирует любые операции UPDATE или DELETE над таблицей. Это предотвращает случайные изменения со стороны любого приложения, имеющего доступ на запись.
Триггер, вызывающий исключение, — ещё более строгая альтернатива: он активно отклоняет операцию с ошибкой.
CREATE RULE no_update_events AS
ON UPDATE TO account_events
DO INSTEAD NOTHING;
CREATE RULE no_delete_events AS
ON DELETE TO account_events
DO INSTEAD NOTHING;Обеспечение неизменяемости с помощью триггера
Триггер, вызывающий исключение, строже безмолвного правила: приложение немедленно получает ошибку, если пытается изменить прошлое событие. Благодаря этому ошибки становятся заметными, а не скрываются без предупреждения.
CREATE OR REPLACE FUNCTION deny_event_mutation()
RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'Event table is append-only: % is not allowed', TG_OP;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_deny_event_mutation
BEFORE UPDATE OR DELETE ON account_events
FOR EACH ROW EXECUTE FUNCTION deny_event_mutation();Подсчёт событий во времени
Таблицы только для добавления упрощают аналитику по времени. Поскольку у каждого события есть отметка времени, Вы можете группировать события по дням, неделям или месяцам без дополнительных столбцов. В примере ниже подсчитывается, сколько событий каждого типа произошло за день.
SELECT
DATE_TRUNC('day', occurred_at) AS day,
event_type,
COUNT(*) AS total
FROM account_events
GROUP BY 1, 2
ORDER BY 1, 2;Восстановление состояния на момент времени
Одно из самых ценных свойств журнала событий — возможность восстановить состояние любой записи в том виде, в каком оно существовало в прошлом. Достаточно отобрать события до нужной отметки времени — расширение для перемещения во времени не требуется.
Это незаменимо при отладке, проведении аудита и соблюдении нормативных требований.
-- What plan was account 42 on at the end of last month?
SELECT payload->>'to' AS plan_at_snapshot
FROM account_events
WHERE account_id = 42
AND event_type IN ('account_opened', 'plan_upgraded')
AND occurred_at <= DATE_TRUNC('month', NOW()) - INTERVAL '1 second'
ORDER BY occurred_at DESC
LIMIT 1;Компенсирующие события вместо исправлений
Если обнаружена ошибка — например, неправильное списание, — Вы не удаляете ошибочное событие. Вместо этого Вы добавляете компенсирующее событие, которое отменяет или обращает его действие. Оба события остаются видимыми в журнале, точно показывая, что произошло и когда было внесено исправление.
Так журнал аудита остаётся полным, а подмену данных можно обнаружить.
-- A charge was applied by mistake; record a reversal
INSERT INTO account_events (account_id, event_type, payload)
VALUES (
42,
'charge_reversed',
'{"reason": "billing_error", "reverses_event_id": 17}'
);Секционирование больших таблиц событий
Таблицы событий быстро растут. Секционирование по диапазону времени сохраняет небольшие размеры отдельных секций, ускоряет запросы по диапазонам и позволяет архивировать или удалять старые секции, не затрагивая свежие данные.
Декларативное секционирование в PostgreSQL упрощает эту задачу: определите секционирование RANGE по occurred_at, а база данных автоматически направит добавляемые строки в нужные секции.
CREATE TABLE account_events_2025
PARTITION OF account_events
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
CREATE TABLE account_events_2026
PARTITION OF account_events
FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');Проверка знаний о таблицах только для добавления
Проверьте, насколько хорошо Вы понимаете проектирование таблиц событий только для добавления.
Итоги: таблицы событий только для добавления
В этом уроке Вы узнали, как проектировать и использовать таблицы событий только для добавления:
- Неизменяемость — строки добавляются один раз и никогда не изменяются. Прошлые события являются фактами.
- Полная история — каждое изменение состояния сохраняется, обеспечивая полные журналы аудита и запросы по состоянию на момент времени.
- Компенсирующие события — ошибки исправляются добавлением нового события отмены, а не удалением старого.
- Обеспечение правил — правила или триггеры на уровне базы данных предотвращают случайные изменения.
- Масштабируемость — секционирование по диапазонам сохраняет высокую производительность больших журналов событий с течением времени.
Таблицы только для добавления — основа построения состояния из событий, архитектур CQRS и любых систем, где критически важны возможность аудита и точность исторических данных.
Часто задаваемые вопросы
Урок «Таблицы событий только для добавления» бесплатный?
Да — полный текст урока «Таблицы событий только для добавления» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс SQL Academy, подпишись на CoddyKit PRO. Курс SQL Academy содержит 4 уроков всего.
Чему я научусь в уроке «Таблицы событий только для добавления»?
Записывайте произошедшее, не перезаписывая данные Ты практикуешь SQL Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать SQL Academy?
Предыдущий опыт не требуется. SQL Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «Таблицы событий только для добавления»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке SQL Academy?
Да. Каждый урок SQL Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Зачем хранить историю
- Таблицы событий только для добавления
- Временные строки и строки с версиями
- Восстановление состояния из событий