0Pricing
SQL Academy · Урок

Таблицы событий только для добавления

Записывайте произошедшее, не перезаписывая данные

«Таблицы событий только для добавления» — бесплатный урок 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 — локальная установка не требуется.

Все уроки этого курса

  1. Зачем хранить историю
  2. Таблицы событий только для добавления
  3. Временные строки и строки с версиями
  4. Восстановление состояния из событий
← Назад к SQL Academy