docs(storage): конвенция часовых поясов принята и записана
- Зачем:
- пояс в стенде нигде не назван: числа верны только потому, что сервер
ClickHouse стоит в UTC, а правило понадобится в dds и витринах —
воронки, удержание, «покупки по дням» (#63).
- Что:
- раздел «Часовые пояса»: пояс — линза, называется в типе колонки либо в
вызове; какая именно — решает слой (ODS на языке выгрузки, DDS и витрины
на языке бизнеса); день берётся из EventDate.
- названы оба перехода, где пояс выбирается, включая разбор строки в
матвью — он берёт пояс у сервера и тип колонки этого не чинит.
- отвергнутые варианты прозой: умолчание сервера, TZ серверу, ODS в поясе
счётчика, хранение местного времени.
- три замера ушли в «Что проверено», сверка с документацией — от 8 августа.
- Проверка:
- make lint
- DDL к конвенции ещё не приведён: документ описывает цель, код идёт
следом тем же тикетом.
This commit is contained in:
@@ -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` срабатывает на вставку именно в эту
|
||||
распределённую таблицу, до раскладки по шардам. Обе матвью разбора стоят над
|
||||
|
||||
Reference in New Issue
Block a user