From 359570ec6538f2185f6b911de0f3b12f257f2070 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sat, 8 Aug 2026 19:44:53 +0300 Subject: [PATCH] =?UTF-8?q?docs(63):=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BF=D0=BE=20=D0=B4=D0=B2=D1=83=D0=BC=20=D1=85=D0=BE?= =?UTF-8?q?=D0=BB=D0=BE=D0=B4=D0=BD=D1=8B=D0=BC=20=D1=80=D0=B5=D0=B2=D1=8C?= =?UTF-8?q?=D1=8E=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - линия дефектов нашла три неверных утверждения и мёртвый замер, линия уместности — три пересказа уже сказанного. - Что: - «тип колонки не решает, какое число ляжет» сужено до правды: разбор отдаёт готовое число, а пояс приёмника решал бы судьбу строки. - замер до правки типов помечен как неповторяемый на нынешнем стенде. - правило о поясе сервера привязано к местам, где линза что-то решает: матвью приёма пояс не называет, и это не нарушение. - убраны: пересказ механики в ADR 0005, четыре строки учебного комментария, утверждение о порядке файлов и «секунды от начала эпохи» у миллисекундной метки. - Проверка: - make lint, make typecheck, make test (408 тестов) - make clean && make up && make check-clickhouse — 9 из 9 Co-Authored-By: Claude Opus 5 --- docs/adr/0005-event-ingestion.md | 7 ++----- docs/architecture/storage.md | 20 ++++++++++++-------- generator/src/clickstream_generator/world.py | 7 ++++--- sql/ddl/10-stg-tables.sql | 12 +++++------- sql/ddl/30-ods-views.sql | 3 ++- 5 files changed, 25 insertions(+), 24 deletions(-) diff --git a/docs/adr/0005-event-ingestion.md b/docs/adr/0005-event-ingestion.md index 8c64f81..8f1ec33 100644 --- a/docs/adr/0005-event-ingestion.md +++ b/docs/adr/0005-event-ingestion.md @@ -232,11 +232,8 @@ ClickHouse 26.3.17.56. Все четыре ответили так, как жд всех трёх NULL. Источник у топика один и шлёт одну запись, так что широта не нужна вовсе, а платится за неё отключённой проверкой. -Третий аргумент — имя пояса, `'UTC'` — пришёл с конвенцией #63. Маска сверяет -суффикс `Z` как букву и выбрасывает, зоны из строки не берёт вовсе, поэтому без -имени функция трактует показания часов по поясу сессии, а тот по умолчанию -серверный. Правило целиком и его довод — [конвенция часовых -поясов](../architecture/storage.md). +Третий аргумент — имя пояса, `'UTC'` — пришёл с конвенцией #63; правило и его +довод — [конвенция часовых поясов](../architecture/storage.md). Цена выбора измерена на настоящих данных: по всем 101 252 строкам сырья модельного дня (день залит дважды) точный формат разобрал метку у каждой, и diff --git a/docs/architecture/storage.md b/docs/architecture/storage.md index ead70b9..551ff41 100644 --- a/docs/architecture/storage.md +++ b/docs/architecture/storage.md @@ -153,11 +153,12 @@ Greenplum, чтобы словарь был общим у двух хранил какие сутки оно попадёт. **Линза называется явно — в типе колонки либо в вызове функции.** Третий -источник, умолчание сервера, в коде не виден и меняется снаружи, поэтому в DDL -и запросах его не остаётся. Прописать этот пояс своей рукой — `` в -конфигурации ноды или `TZ` контейнеру — было бы той же болезнью с другим -умолчанием, и вдобавок отняло бы проверку: когда линза названа в типах и в -разборе, пояс сервера на данные не влияет нигде, и в этом можно убедиться, +источник, умолчание сервера, в коде не виден и меняется снаружи, поэтому там, +где линза что-то решает — в объявлении хранимой колонки и в выражении, +считающем дату, — его не остаётся. Прописать этот пояс своей рукой — +`` в конфигурации ноды или `TZ` контейнеру — было бы той же болезнью +с другим умолчанием, и вдобавок отняло бы проверку: когда линза названа в типах +и в разборе, пояс сервера на данные не влияет нигде, и в этом можно убедиться, поменяв его. Правило стоит на источнике пояса, а не на функции: `toDate` по колонке, чей тип пояс несёт, законен и имени не требует — так и работают ключи партиций `toDate(_load_ts)` у сырья и у таблицы ошибок. Имя @@ -182,8 +183,9 @@ UTC+4), и пересчёт идёт один раз при наполнении метку или свести её к дате. На записи — превратить строку в число: суффикс `Z` на проводе зоны не даёт, маска разбора съедает его буквой, и `parseDateTimeOrNull` без третьего аргумента трактует показания часов по поясу -сессии, а тот по умолчанию серверный. Тип колонки тут не помогает: он про то, -как число читают, а не про то, какое ляжет. Механика и выбор функции — [ADR +сессии, а тот по умолчанию серверный. Тип колонки тут не помогает: разбор +отдаёт готовое число, и колонка кладёт его как есть — пояс приёмника решал бы +судьбу строки, а не числа. Механика и выбор функции — [ADR 0005](../adr/0005-event-ingestion.md). **День берётся из `EventDate`.** Дата в поясе счётчика уже посчитана @@ -463,7 +465,9 @@ ODS. Второе: матвью приёма создаётся последне - `toDate` берёт пояс у типа своего аргумента. Из одного момента: по `DateTime('UTC')` — `2026-05-31`, по `DateTime('Europe/Samara')` — `2026-06-01`. Отсюда форма правила: имя пояса нужно там, где его не несёт тип. -- У колонки без объявленного пояса глаз и `GROUP BY` расходятся. Событие +- У колонки без объявленного пояса глаз и `GROUP BY` расходятся. Замер снят до + правки типов и на нынешнем стенде не повторяется — колонка уже с поясом. + Событие `WatchID = 113504893317`, `EventDate` = `2026-06-05`: без настроек колонка показана `2026-06-04 20:58:56`, под `session_timezone = 'Europe/Samara'` — `2026-06-05 00:58:56`, а `toDate(UTCEventTime)` в обоих случаях diff --git a/generator/src/clickstream_generator/world.py b/generator/src/clickstream_generator/world.py index dfa0c96..3f99b83 100644 --- a/generator/src/clickstream_generator/world.py +++ b/generator/src/clickstream_generator/world.py @@ -21,9 +21,10 @@ COUNTER_ID = 42150607 # модельные сутки, как в выгрузке Метрики — `EventDate` дата в поясе счётчика, # `UTCEventTime` абсолютная метка. Отсюда следствие, о котором сторона # хранилища должна знать заранее: `toDate(UTCEventTime)` ≠ `EventDate` у ночных -# событий (спека генератора, раздел 9). Хранилищу нужно имя из базы поясов: его -# просят `toDate` и типы колонок DDS (docs/architecture/storage.md, «Часовые -# пояса»). +# событий (спека генератора, раздел 9). Хранилищу тот же пояс нужен именем из +# базы поясов — так пишутся `toDate` и типы колонок DDS +# (docs/architecture/storage.md, «Часовые пояса»). Пути отсюда в SQL нет, имя +# переносят руками — но берут его здесь. COUNTER_TIMEZONE = "Europe/Samara" COUNTER_TIMEZONE_MINUTES = 240 diff --git a/sql/ddl/10-stg-tables.sql b/sql/ddl/10-stg-tables.sql index da3f01d..9a163a1 100644 --- a/sql/ddl/10-stg-tables.sql +++ b/sql/ddl/10-stg-tables.sql @@ -41,7 +41,7 @@ SETTINGS -- виртуальные колонки его не несут, а после записи в Distributed он уже -- невосстановим. -- --- kafka_timestamp — Nullable(DateTime64(3)), и заполняется из виртуальной +-- kafka_timestamp — Nullable(DateTime64(3, 'UTC')), и заполняется из виртуальной -- колонки _timestamp_ms, а не из _timestamp. Измерено на стенде 5 августа -- 2026 года: _timestamp — Nullable(DateTime), то есть секунды; _timestamp_ms — -- Nullable(DateTime64(3)). Взяты миллисекунды: у брокера метка миллисекундная, @@ -49,12 +49,10 @@ SETTINGS -- округлять ему нечего. Обнуляемость обязательна: метку брокер заполняет не -- всегда, а необнуляемый тип дал бы либо падение приёма, либо тихий 1970 год. -- --- Пояс у обеих меток написан в типе — DateTime64(3, 'UTC'); в DDL стенда он --- встречается здесь впервые. Само число от пояса не зависит, это секунды от --- начала эпохи. Пояс — линза: по нему решают, какие часы покажут метку и в --- какие сутки она попадёт, то есть чем окажется toDate(_load_ts) в ключе --- партиции ниже. Не назови линзу — её выберет пояс сервера, умолчание, которого --- в коде не видно. Правило целиком — docs/architecture/storage.md, «Часовые +-- Пояс у обеих меток написан в типе. Хранимого числа он не меняет, а решает, +-- в какие сутки метка попадёт, — то есть чем окажется toDate(_load_ts) в ключе +-- партиции ниже. Не напиши его — пояс возьмётся у сервера, а это умолчание в +-- коде не видно. Правило целиком — docs/architecture/storage.md, «Часовые -- пояса». -- -- Нарезка и срок жизни — по _load_ts, то есть по реальному времени загрузки: diff --git a/sql/ddl/30-ods-views.sql b/sql/ddl/30-ods-views.sql index 553d2d7..d51a1b2 100644 --- a/sql/ddl/30-ods-views.sql +++ b/sql/ddl/30-ods-views.sql @@ -55,7 +55,8 @@ -- Третьим аргументом назван пояс — 'UTC'. Суффикс Z маска сверяет как букву и -- выбрасывает, зоны из строки не берёт вовсе, поэтому без имени функция читала -- бы показания часов по поясу сессии, а тот по умолчанию серверный. Тип --- колонки этого не чинит: он про то, как число покажут, а не какое ляжет. +-- колонки этого не чинит: разбор отдаёт готовое число, и колонка кладёт его +-- как есть — пояс приёмника решал бы судьбу строки, а не числа. -- Правило и замер — docs/architecture/storage.md, «Часовые пояса». -- -- EventDate в такой подпорке не нуждается: дата уезжает как «2026-06-01», и