docs(63): правки по двум холодным ревью реализации

- Зачем:
  - линия дефектов нашла три неверных утверждения и мёртвый замер, линия
    уместности — три пересказа уже сказанного.
- Что:
  - «тип колонки не решает, какое число ляжет» сужено до правды: разбор
    отдаёт готовое число, а пояс приёмника решал бы судьбу строки.
  - замер до правки типов помечен как неповторяемый на нынешнем стенде.
  - правило о поясе сервера привязано к местам, где линза что-то решает:
    матвью приёма пояс не называет, и это не нарушение.
  - убраны: пересказ механики в ADR 0005, четыре строки учебного
    комментария, утверждение о порядке файлов и «секунды от начала эпохи»
    у миллисекундной метки.
- Проверка:
  - make lint, make typecheck, make test (408 тестов)
  - make clean && make up && make check-clickhouse — 9 из 9

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 19:44:53 +03:00
co-authored by Claude Opus 5
parent d67e697821
commit 359570ec65
5 changed files with 25 additions and 24 deletions
+2 -5
View File
@@ -232,11 +232,8 @@ ClickHouse 26.3.17.56. Все четыре ответили так, как жд
всех трёх NULL. Источник у топика один и шлёт одну запись, так что широта не всех трёх NULL. Источник у топика один и шлёт одну запись, так что широта не
нужна вовсе, а платится за неё отключённой проверкой. нужна вовсе, а платится за неё отключённой проверкой.
Третий аргумент — имя пояса, `'UTC'` — пришёл с конвенцией #63. Маска сверяет Третий аргумент — имя пояса, `'UTC'` — пришёл с конвенцией #63; правило и его
суффикс `Z` как букву и выбрасывает, зоны из строки не берёт вовсе, поэтому без довод — [конвенция часовых поясов](../architecture/storage.md).
имени функция трактует показания часов по поясу сессии, а тот по умолчанию
серверный. Правило целиком и его довод — [конвенция часовых
поясов](../architecture/storage.md).
Цена выбора измерена на настоящих данных: по всем 101 252 строкам сырья Цена выбора измерена на настоящих данных: по всем 101 252 строкам сырья
модельного дня (день залит дважды) точный формат разобрал метку у каждой, и модельного дня (день залит дважды) точный формат разобрал метку у каждой, и
+12 -8
View File
@@ -153,11 +153,12 @@ Greenplum, чтобы словарь был общим у двух хранил
какие сутки оно попадёт. какие сутки оно попадёт.
**Линза называется явно — в типе колонки либо в вызове функции.** Третий **Линза называется явно — в типе колонки либо в вызове функции.** Третий
источник, умолчание сервера, в коде не виден и меняется снаружи, поэтому в DDL источник, умолчание сервера, в коде не виден и меняется снаружи, поэтому там,
и запросах его не остаётся. Прописать этот пояс своей рукой — `<timezone>` в где линза что-то решает — в объявлении хранимой колонки и в выражении,
конфигурации ноды или `TZ` контейнеру — было бы той же болезнью с другим считающем дату, — его не остаётся. Прописать этот пояс своей рукой —
умолчанием, и вдобавок отняло бы проверку: когда линза названа в типах и в `<timezone>` в конфигурации ноды или `TZ` контейнеру — было бы той же болезнью
разборе, пояс сервера на данные не влияет нигде, и в этом можно убедиться, с другим умолчанием, и вдобавок отняло бы проверку: когда линза названа в типах
и в разборе, пояс сервера на данные не влияет нигде, и в этом можно убедиться,
поменяв его. Правило стоит на источнике пояса, а не на функции: поменяв его. Правило стоит на источнике пояса, а не на функции:
`toDate` по колонке, чей тип пояс несёт, законен и имени не требует — так и `toDate` по колонке, чей тип пояс несёт, законен и имени не требует — так и
работают ключи партиций `toDate(_load_ts)` у сырья и у таблицы ошибок. Имя работают ключи партиций `toDate(_load_ts)` у сырья и у таблицы ошибок. Имя
@@ -182,8 +183,9 @@ UTC+4), и пересчёт идёт один раз при наполнении
метку или свести её к дате. На записи — превратить строку в число: суффикс `Z` метку или свести её к дате. На записи — превратить строку в число: суффикс `Z`
на проводе зоны не даёт, маска разбора съедает его буквой, и на проводе зоны не даёт, маска разбора съедает его буквой, и
`parseDateTimeOrNull` без третьего аргумента трактует показания часов по поясу `parseDateTimeOrNull` без третьего аргумента трактует показания часов по поясу
сессии, а тот по умолчанию серверный. Тип колонки тут не помогает: он про то, сессии, а тот по умолчанию серверный. Тип колонки тут не помогает: разбор
как число читают, а не про то, какое ляжет. Механика и выбор функции — [ADR отдаёт готовое число, и колонка кладёт его как есть — пояс приёмника решал бы
судьбу строки, а не числа. Механика и выбор функции — [ADR
0005](../adr/0005-event-ingestion.md). 0005](../adr/0005-event-ingestion.md).
**День берётся из `EventDate`.** Дата в поясе счётчика уже посчитана **День берётся из `EventDate`.** Дата в поясе счётчика уже посчитана
@@ -463,7 +465,9 @@ ODS. Второе: матвью приёма создаётся последне
- `toDate` берёт пояс у типа своего аргумента. Из одного момента: - `toDate` берёт пояс у типа своего аргумента. Из одного момента:
по `DateTime('UTC')``2026-05-31`, по `DateTime('Europe/Samara')` по `DateTime('UTC')``2026-05-31`, по `DateTime('Europe/Samara')`
`2026-06-01`. Отсюда форма правила: имя пояса нужно там, где его не несёт тип. `2026-06-01`. Отсюда форма правила: имя пояса нужно там, где его не несёт тип.
- У колонки без объявленного пояса глаз и `GROUP BY` расходятся. Событие - У колонки без объявленного пояса глаз и `GROUP BY` расходятся. Замер снят до
правки типов и на нынешнем стенде не повторяется — колонка уже с поясом.
Событие
`WatchID = 113504893317`, `EventDate` = `2026-06-05`: без настроек колонка `WatchID = 113504893317`, `EventDate` = `2026-06-05`: без настроек колонка
показана `2026-06-04 20:58:56`, под `session_timezone = 'Europe/Samara'` показана `2026-06-04 20:58:56`, под `session_timezone = 'Europe/Samara'`
`2026-06-05 00:58:56`, а `toDate(UTCEventTime)` в обоих случаях `2026-06-05 00:58:56`, а `toDate(UTCEventTime)` в обоих случаях
+4 -3
View File
@@ -21,9 +21,10 @@ COUNTER_ID = 42150607
# модельные сутки, как в выгрузке Метрики — `EventDate` дата в поясе счётчика, # модельные сутки, как в выгрузке Метрики — `EventDate` дата в поясе счётчика,
# `UTCEventTime` абсолютная метка. Отсюда следствие, о котором сторона # `UTCEventTime` абсолютная метка. Отсюда следствие, о котором сторона
# хранилища должна знать заранее: `toDate(UTCEventTime)` ≠ `EventDate` у ночных # хранилища должна знать заранее: `toDate(UTCEventTime)` ≠ `EventDate` у ночных
# событий (спека генератора, раздел 9). Хранилищу нужно имя из базы поясов: его # событий (спека генератора, раздел 9). Хранилищу тот же пояс нужен именем из
# просят `toDate` и типы колонок DDS (docs/architecture/storage.md, «Часовые # базы поясов — так пишутся `toDate` и типы колонок DDS
# пояса»). # (docs/architecture/storage.md, «Часовые пояса»). Пути отсюда в SQL нет, имя
# переносят руками — но берут его здесь.
COUNTER_TIMEZONE = "Europe/Samara" COUNTER_TIMEZONE = "Europe/Samara"
COUNTER_TIMEZONE_MINUTES = 240 COUNTER_TIMEZONE_MINUTES = 240
+5 -7
View File
@@ -41,7 +41,7 @@ SETTINGS
-- виртуальные колонки его не несут, а после записи в Distributed он уже -- виртуальные колонки его не несут, а после записи в Distributed он уже
-- невосстановим. -- невосстановим.
-- --
-- kafka_timestamp — Nullable(DateTime64(3)), и заполняется из виртуальной -- kafka_timestamp — Nullable(DateTime64(3, 'UTC')), и заполняется из виртуальной
-- колонки _timestamp_ms, а не из _timestamp. Измерено на стенде 5 августа -- колонки _timestamp_ms, а не из _timestamp. Измерено на стенде 5 августа
-- 2026 года: _timestamp — Nullable(DateTime), то есть секунды; _timestamp_ms — -- 2026 года: _timestamp — Nullable(DateTime), то есть секунды; _timestamp_ms —
-- Nullable(DateTime64(3)). Взяты миллисекунды: у брокера метка миллисекундная, -- Nullable(DateTime64(3)). Взяты миллисекунды: у брокера метка миллисекундная,
@@ -49,12 +49,10 @@ SETTINGS
-- округлять ему нечего. Обнуляемость обязательна: метку брокер заполняет не -- округлять ему нечего. Обнуляемость обязательна: метку брокер заполняет не
-- всегда, а необнуляемый тип дал бы либо падение приёма, либо тихий 1970 год. -- всегда, а необнуляемый тип дал бы либо падение приёма, либо тихий 1970 год.
-- --
-- Пояс у обеих меток написан в типе — DateTime64(3, 'UTC'); в DDL стенда он -- Пояс у обеих меток написан в типе. Хранимого числа он не меняет, а решает,
-- встречается здесь впервые. Само число от пояса не зависит, это секунды от -- в какие сутки метка попадёт, то есть чем окажется toDate(_load_ts) в ключе
-- начала эпохи. Пояс — линза: по нему решают, какие часы покажут метку и в -- партиции ниже. Не напиши его — пояс возьмётся у сервера, а это умолчание в
-- какие сутки она попадёт, то есть чем окажется toDate(_load_ts) в ключе -- коде не видно. Правило целиком — docs/architecture/storage.md, «Часовые
-- партиции ниже. Не назови линзу — её выберет пояс сервера, умолчание, которого
-- в коде не видно. Правило целиком — docs/architecture/storage.md, «Часовые
-- пояса». -- пояса».
-- --
-- Нарезка и срок жизни — по _load_ts, то есть по реальному времени загрузки: -- Нарезка и срок жизни — по _load_ts, то есть по реальному времени загрузки:
+2 -1
View File
@@ -55,7 +55,8 @@
-- Третьим аргументом назван пояс — 'UTC'. Суффикс Z маска сверяет как букву и -- Третьим аргументом назван пояс — 'UTC'. Суффикс Z маска сверяет как букву и
-- выбрасывает, зоны из строки не берёт вовсе, поэтому без имени функция читала -- выбрасывает, зоны из строки не берёт вовсе, поэтому без имени функция читала
-- бы показания часов по поясу сессии, а тот по умолчанию серверный. Тип -- бы показания часов по поясу сессии, а тот по умолчанию серверный. Тип
-- колонки этого не чинит: он про то, как число покажут, а не какое ляжет. -- колонки этого не чинит: разбор отдаёт готовое число, и колонка кладёт его
-- как есть — пояс приёмника решал бы судьбу строки, а не числа.
-- Правило и замер — docs/architecture/storage.md, «Часовые пояса». -- Правило и замер — docs/architecture/storage.md, «Часовые пояса».
-- --
-- EventDate в такой подпорке не нуждается: дата уезжает как «2026-06-01», и -- EventDate в такой подпорке не нуждается: дата уезжает как «2026-06-01», и