From a93a3bf3a303536744bff5bb0d81403562bce82f Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sat, 8 Aug 2026 18:36:29 +0300 Subject: [PATCH] =?UTF-8?q?docs(storage):=20=D0=BA=D0=BE=D0=BD=D0=B2=D0=B5?= =?UTF-8?q?=D0=BD=D1=86=D0=B8=D1=8F=20=D1=87=D0=B0=D1=81=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D1=85=20=D0=BF=D0=BE=D1=8F=D1=81=D0=BE=D0=B2=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=D0=BD=D1=8F=D1=82=D0=B0=20=D0=B8=20=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=B0=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - пояс в стенде нигде не назван: числа верны только потому, что сервер ClickHouse стоит в UTC, а правило понадобится в dds и витринах — воронки, удержание, «покупки по дням» (#63). - Что: - раздел «Часовые пояса»: пояс — линза, называется в типе колонки либо в вызове; какая именно — решает слой (ODS на языке выгрузки, DDS и витрины на языке бизнеса); день берётся из EventDate. - названы оба перехода, где пояс выбирается, включая разбор строки в матвью — он берёт пояс у сервера и тип колонки этого не чинит. - отвергнутые варианты прозой: умолчание сервера, TZ серверу, ODS в поясе счётчика, хранение местного времени. - три замера ушли в «Что проверено», сверка с документацией — от 8 августа. - Проверка: - make lint - DDL к конвенции ещё не приведён: документ описывает цель, код идёт следом тем же тикетом. --- docs/architecture/storage.md | 81 +++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/docs/architecture/storage.md b/docs/architecture/storage.md index 80138c4..4ec3b94 100644 --- a/docs/architecture/storage.md +++ b/docs/architecture/storage.md @@ -146,6 +146,59 @@ Greenplum, чтобы словарь был общим у двух хранил пустой формальностью. В слоях, которые наполняет Airflow, `run_id` появится по-настоящему — тогда и заведём, тем же стилем имени. +## Часовые пояса + +Пояс — линза, а не свойство значения. `DateTime` хранит одно число, секунды от +начала эпохи; пояс решает лишь, какие часы по этому числу покажут время и в +какие сутки оно попадёт. + +**Линза называется явно — в типе колонки либо в вызове функции.** Третий +источник, умолчание сервера, в коде не виден и меняется снаружи, поэтому в DDL +и запросах его не остаётся. Правило стоит на источнике пояса, а не на функции: +`toDate` по колонке, чей тип пояс несёт, законен и имени не требует — так стоит +`PARTITION BY toDate(_load_ts)` у сырья и у таблицы ошибок. Имя пишется тогда, +когда нужна другая линза, чем у колонки: `toDate(UTCEventTime, 'Europe/Samara')` +— это «день по часам счётчика». + +**Какая линза, решает слой — по тому, кого он обслуживает.** ODS хранит снимок +выгрузки и говорит на языке выгрузки: у Метрики `UTCEventTime` абсолютна, +значит `DateTime('UTC')`; служебные метки `_load_ts` и `kafka_timestamp` +абсолютны тоже — `DateTime64(3, 'UTC')`. DDS и витрины обслуживают человека с +дашбордом, поэтому время там лежит местным, в поясе счётчика, и пересчёт идёт +один раз при наполнении слоя: автор отчёта пояса не пишет, он берёт готовую +колонку. `Date` не участвует вовсе — у типа пояса нет. + +**Линза выбирается дважды, и второй раз упустить легко.** На чтении — показать +метку или свести её к дате. На записи — превратить строку в число: на проводе +`UTCEventTime` приезжает как `2026-06-04T20:58:56Z`, но суффикс `Z` в маске +разбора не признак зоны, а буква, которую парсер сверяет и выбрасывает. Значит +`parseDateTimeOrNull` получает показания часов и решает, чьи они; без третьего +аргумента — по поясу сервера. Тип колонки тут не помогает: он про то, как число +читают, а не про то, какое число ляжет. Поэтому имя пояса стоит и в разборе; +сам разбор и выбор функции решены в [ADR 0005](../adr/0005-event-ingestion.md). + +**День берётся из `EventDate`.** Дата в поясе счётчика уже посчитана +генератором и лежит колонкой, так что суточные срезы группируются по ней и +пояса не упоминают вовсе. Почему у ночных событий `toDate(UTCEventTime)` с ней +расходится — [спека генератора](../specs/2026-08-01-generator.md), раздел 9. + +Функции ClickHouse требуют имя пояса, а генератор считает смещением; связаны +они в [`world.py`](../../generator/src/clickstream_generator/world.py) — там же +довод, почему смещение не выводится из имени. + +Отвергнуто по дороге четыре варианта. **Оставить умолчание сервера**: числа +сегодня верные, но держатся они на настройке окружения, которой нет ни в одном +файле репозитория. **Прописать пояс серверу** — та же болезнь с другим +умолчанием, и вдобавок отнимает проверку: когда линза названа в типах и в +разборе, пояс сервера на данные не влияет нигде, и менти может в этом +убедиться, поменяв его. **Объявить и ODS в поясе счётчика** +(`DateTime('Europe/Samara')`) — байты те же, меняется линза, и +`toDate(UTCEventTime)` начинает всегда совпадать с `EventDate`; отвергнуто +потому, что колонка зовётся `UTCEventTime` и в настоящей выгрузке Метрики она в +UTC, а стёртое расхождение — тот самый урок, ради которого мир сделан с поясом ++4. **Хранить местное время вместо абсолютного** ломает формат выгрузки и +названо было для полноты веера. + ## Путь реплицированных таблиц в keeper Шаблон — `/clickhouse/tables/{shard}/{database}/{table}`. База в пути @@ -398,10 +451,34 @@ ODS. Второе: матвью приёма создаётся последне одинаковым путём в keeper становятся репликами друг друга, а макрос `{uuid}` завязан на движок базы `Atomic`. `ON CLUSTER` ждёт все хосты и бросает по таймауту; `CREATE ... IF NOT EXISTS` на существующем объекте не бросает. +`parseDateTime` и его родня принимают пояс необязательным последним аргументом, +а без него берут пояс сессии или сервера. У колонки с объявленным поясом +значения приводятся к нему, у колонки без объявленного действует пояс сервера +(сверка 8 августа 2026 года). **Проверено на стенде.** Опыты прогнаны на живом кластере: пять при исполнении -#37 (четыре 5 августа 2026 года, пятый 6 августа) и пять при исполнении #43 -(7 августа). Все подтвердили то, что здесь написано. +#37 (четыре 5 августа 2026 года, пятый 6 августа), пять при исполнении #43 +(7 августа) и три при обсуждении #63 (8 августа). Все подтвердили то, что здесь +написано. + +- Разбор строки берёт пояс у сессии, а не из строки. Под + `session_timezone = 'Europe/Samara'` одна и та же строка + `2026-06-01T01:24:09Z` по маске `%Y-%m-%dT%H:%i:%SZ` дала 1780262649 без + третьего аргумента и 1780277049 с аргументом `'UTC'` — ровно четыре часа + разницы. Тип результата без аргумента — `Nullable(DateTime('Europe/Samara'))`. + Отсюда имя пояса в разборе: суффикс `Z` маска съедает и выбрасывает. +- `toDate` берёт пояс у типа своего аргумента. Из одного момента: + по `DateTime('UTC')` — `2026-05-31`, по `DateTime('Europe/Samara')` — + `2026-06-01`. Отсюда форма правила: имя пояса нужно там, где его не несёт тип. +- Расхождение глаза и `GROUP BY` живёт по HTTP и только там. Событие + `WatchID = 113504893317`, `EventDate` = `2026-06-05`: по HTTP без настроек + колонка показана `2026-06-04 20:58:56`, с `session_timezone = 'Europe/Samara'` + — `2026-06-05 00:58:56`, а `toDate(UTCEventTime)` в обоих случаях + `2026-06-04`. Родной `clickhouse-client` `session_timezone` к отображению + колонки не применяет и показывает `20:58:56` в обоих случаях. То есть сцену + видно из клиентов поверх HTTP — Superset в стенде ходит именно так. После объявления + `DateTime('UTC')` она уходит: проверено кастом на том же событии — колонка + показывает `20:58:56` и при чужом поясе сессии. - Матвью с источником-`Distributed` срабатывает на вставку именно в эту распределённую таблицу, до раскладки по шардам. Обе матвью разбора стоят над