Повторы и поздние события
В потоке «События» шестнадцать доставок описывают пятнадцать действий. Один пакет повторил завершение посетителя a, а завершение посетителя b пришло пятого октября с временем первого числа. Добавим логический слой, который выбирает последнюю версию одного события. Результат урока — корректное определение набора для последующих отчётов.
Здесь используется ReplacingMergeTree с полем версии. Его фоновая работа не означает немедленной гарантии уникальности при обычном чтении. Сервер и запросы не запускались; число пятнадцать является ожидаемым результатом нашей фикстуры после логической дедупликации. Не переносите его на реальные события сайта.
Повтор сохраняет идентичность
Один event_id получает две строки: первоначальную с версией один и повторную с версией два. Поля действия совпадают, меняются только получение и версия. Поэтому ключ (course_id, lesson_id, occurred_at, event_id) остаётся одним и тем же. Это обязательное правило контракта, разобранное в предыдущем уроке.
Создадим логическую таблицу с теми же столбцами, используя определение исходной таблицы. Полный sql/03-logical-events.sql предназначен будущему последовательному выполнению после DDL первого слоя.
CREATE TABLE IF NOT EXISTS events_lab.events
AS events_lab.deliveries
ENGINE = ReplacingMergeTree(version)
PARTITION BY toYYYYMM(occurred_at)
ORDER BY (course_id, lesson_id, occurred_at, event_id);
INSERT INTO events_lab.events
SELECT * FROM events_lab.deliveries;
CREATE VIEW IF NOT EXISTS events_lab.logical_events AS
SELECT * FROM events_lab.events FINAL;
Вставка копирует текущий учебный набор. При повторном выполнении она добавит повторные версии в физическую таблицу; FINAL должен сохранить логическое представление. Внешний поток потребует согласованной доставки в этот слой или дополнительного представления на вставках. Архив не заявляет, что два слоя автоматически синхронизируются после любой новой строки.
Когда исчезает повтор
ReplacingMergeTree устраняет строки с одинаковым ключом в ходе объединения частей. Время этой работы не является обещанием для каждого отчёта. Обычный запрос может ещё видеть обе версии. Поэтому для правильного логического результата в учебных запросах используем events FINAL или подготовленный logical_events.
SELECT count() AS events, uniqExact(event_id) AS distinct_ids
FROM events_lab.events FINAL;
Ожидаем пятнадцать и пятнадцать после загрузки всего набора. В исходной deliveries первое число было бы шестнадцать, второе пятнадцать. Разница объясняет физические повторные доставки. Она не показывает все возможные ошибки: если отправитель дал повтору новый идентификатор, оба счётчика вырастут одинаково.
Документация ReplacingMergeTree связывает замену с ключом сортировки и описывает её фоновый характер. FINAL применяется при чтении, поэтому имеет стоимость. Мы не измеряли эту стоимость; для больших отчётов понадобятся подходящая схема и отдельные агрегаты, не отказ от корректности ради красивого счётчика.
Версия и конфликт одинаковых значений
Версия должна иметь понятный источник. В фикстуре она задана вручную. В сборщике можно выдавать монотонную версию принятой доставки, но нужно гарантировать её сравнимость для одного события. Клиентская случайная цифра или неподконтрольное время не обеспечивают правильный выбор последней версии.
Если две строки имеют одинаковый ключ и одинаковую версию, но различное содержимое, результат нельзя считать надёжной бизнес-логикой. Контракт должен запретить такое расхождение или определить более строгий способ разрешения. Наша версия не используется для исправления названия события: она выбирает свежую доставку неизменного действия.
Также нельзя рассчитывать, что фоновые объединения соединят события между разными партициями. Повтор сохраняет время и месяц. Если производитель изменяет occurred_at, он нарушает ключевую идентичность, и алгоритм заменяющего движка уже не описывает желаемое исправление. Эту ошибку следует обнаруживать раньше отчёта.
Поздняя доставка изменяет историю
У посетителя b завершение html-start произошло первого октября. До пятого числа сборщик знал только просмотр и закладку. После получения поздней строки отчёт первого октября должен увеличиться на одно завершение. Это не баг календарной группировки: новые данные уточнили прошлый период.
Поэтому отчёт нуждается в времени среза и договоре поздних событий. Например, можно объявлять последние семь дней предварительными и обновлять их при поступлении новых строк. Число семь здесь лишь пример политики, а не правило ClickHouse. Допустимую задержку владелец выбирает по реальному поведению доставки и требованиям редактора.
Нельзя закрыть месяц только на основании того, что сегодня наступил следующий. Пока контракт допускает позднее получение, старые показатели способны измениться. Если источник гарантирует финальность после срока, отчёт может закрепить её. Без такой гарантии слово «окончательный» было бы необоснованным.
Два времени для контроля качества
Разница received_at - occurred_at показывает задержку, а группировка по occurred_at — историю действий. Одна и та же строка участвует в обоих разрезах по-разному. Для позднего завершения задержка измеряется днями, для первоначальных доставок — секундами. Повторная версия заменит раннее received_at свежим, поэтому логический слой не подходит для анализа всей истории попыток доставки.
Такая история остаётся в deliveries. Логический слой отвечает на вопросы о фактах, исходный — о полученных строках. Сохранение двух слоёв полезно, пока их сроки хранения и доступы определены. Внешние пользовательские данные не должны храниться бесконечно только ради удобства диагностики.
Неизвестный результат копирования слоя
Копирование из deliveries в events может завершиться неопределённо для внешнего оператора, если связь оборвалась. Повтор такого копирования добавит те же ключи и версии. Логический запрос должен по-прежнему выбирать одно действие при соблюдённом контракте, но физическое количество строк и стоимость хранения увеличатся.
Это не повод выполнять бесконечную полную перекладку. Для рабочего потока нужна управляемая граница обработанных доставок, очередь или другая стратегия, которая понимает подтверждение и повтор. Учебный INSERT SELECT служит одному малому набору и не обещает эксплуатационную синхронизацию двух таблиц.
Проверка суммы по событиям после копирования также не доказывает, что все поля совпали. Полезно сопоставить идентификаторы, неизменяемые поля и выбранные версии. Если число пятнадцать получилось случайно после потери одной строки и добавления другой, один счётчик не обнаружит расхождение.
Полезно сохранить отдельное описание причин повторной доставки. Оно помогает отличать сетевую неопределённость от ошибочного производителя. Само наличие повторов не доказывает сбой приложения, если договор прямо допускает повтор после потери подтверждения и логический слой его обрабатывает.
Следующая глава построит счётчики по logical_events. Каждый отчёт будет указывать единицу наблюдения: доставка, событие, анонимный браузер или пара браузер–урок. Именно это определение вместе с устранением повторов делает число понятным редактору.