From e6f300a66ec78d96f87d5547f0477760244872f4 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sat, 8 Aug 2026 19:05:39 +0300 Subject: [PATCH] =?UTF-8?q?docs(storage):=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BA=D0=BE=D0=BD=D0=B2=D0=B5=D0=BD=D1=86=D0=B8=D0=B8?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D0=B4=D0=B2=D1=83=D0=BC=20=D1=85=D0=BE=D0=BB?= =?UTF-8?q?=D0=BE=D0=B4=D0=BD=D1=8B=D0=BC=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - раздел «Часовые пояса» прошёл два холодных ревью — по дефектам и по уместности. Первое поймало ложный замер и три расхождения с живым стендом, второе — материал не своей зоны и дубли (#63). - Что: - замер «расхождение живёт по HTTP» отозван: мерил toString(UTCEventTime) в родном клиенте против голой колонки по HTTP, а это разные вещи. Перемерено — клиенты ведут себя одинаково; записан верный факт: вывод колонки идёт по поясу сессии, функция — по поясу типа. - «по поясу сервера» заменено на «по поясу сессии, а тот по умолчанию серверный» — в разделе и в ADR 0005; утверждение в ледгере переписано под измеренный раскол вывода и типа. - PARTITION BY toDate(_load_ts) больше не выдаётся за уже соблюдённое правило: _load_ts сегодня DateTime64(3) без пояса. - абзац ADR 0005 больше не спорит с цитатой вызова строкой выше. - вырезано: веер отклонённых вариантов под заголовком (живые отказы разложены прозой по своим абзацам, как принято в этом документе), ссылка на несуществующую связку в world.py, осиротевшая строка про Grafana, абзац про пояс показа — он уехал комментарием в #63. - Проверка: - make lint - замеры повторены на живом стенде 8 августа 2026 года - DDL к конвенции по-прежнему не приведён: документы описывают цель --- docs/adr/0005-event-ingestion.md | 5 ++ docs/architecture/storage.md | 78 +++++++++++++++----------------- 2 files changed, 41 insertions(+), 42 deletions(-) diff --git a/docs/adr/0005-event-ingestion.md b/docs/adr/0005-event-ingestion.md index a6733a8..d933e94 100644 --- a/docs/adr/0005-event-ingestion.md +++ b/docs/adr/0005-event-ingestion.md @@ -232,6 +232,11 @@ ClickHouse 26.3.17.56. Все четыре ответили так, как жд всех трёх NULL. Источник у топика один и шлёт одну запись, так что широта не нужна вовсе, а платится за неё отключённой проверкой. +Пояс разбору при #63 добавлен третьим аргументом — `'UTC'`; вызов выше приведён +без него, каким он был до этого решения. Без имени пояса функция трактует +показания часов по поясу сессии, а тот по умолчанию серверный. Правило целиком +и его довод — [конвенция часовых поясов](../architecture/storage.md). + Цена выбора измерена на настоящих данных: по всем 101 252 строкам сырья модельного дня (день залит дважды) точный формат разобрал метку у каждой, и ни на одной не разошёлся с `parseDateTimeBestEffort`. Различаются они только diff --git a/docs/architecture/storage.md b/docs/architecture/storage.md index 4ec3b94..796c068 100644 --- a/docs/architecture/storage.md +++ b/docs/architecture/storage.md @@ -154,9 +154,14 @@ Greenplum, чтобы словарь был общим у двух хранил **Линза называется явно — в типе колонки либо в вызове функции.** Третий источник, умолчание сервера, в коде не виден и меняется снаружи, поэтому в DDL -и запросах его не остаётся. Правило стоит на источнике пояса, а не на функции: -`toDate` по колонке, чей тип пояс несёт, законен и имени не требует — так стоит -`PARTITION BY toDate(_load_ts)` у сырья и у таблицы ошибок. Имя пишется тогда, +и запросах его не остаётся. Прописать этот пояс своей рукой — `` в +конфигурации ноды или `TZ` контейнеру — было бы той же болезнью с другим +умолчанием, и вдобавок отняло бы проверку: когда линза названа в типах и в +разборе, пояс сервера на данные не влияет нигде, и в этом можно убедиться, +поменяв его. Правило стоит на источнике пояса, а не на функции: +`toDate` по колонке, чей тип пояс несёт, законен и имени не требует — так и +работают ключи партиций `toDate(_load_ts)` у сырья и у таблицы ошибок, когда +`_load_ts` типизирован. Имя пишется тогда, когда нужна другая линза, чем у колонки: `toDate(UTCEventTime, 'Europe/Samara')` — это «день по часам счётчика». @@ -164,41 +169,29 @@ Greenplum, чтобы словарь был общим у двух хранил выгрузки и говорит на языке выгрузки: у Метрики `UTCEventTime` абсолютна, значит `DateTime('UTC')`; служебные метки `_load_ts` и `kafka_timestamp` абсолютны тоже — `DateTime64(3, 'UTC')`. DDS и витрины обслуживают человека с -дашбордом, поэтому время там лежит местным, в поясе счётчика, и пересчёт идёт -один раз при наполнении слоя: автор отчёта пояса не пишет, он берёт готовую -колонку. `Date` не участвует вовсе — у типа пояса нет. +дашбордом, поэтому время там лежит местным, в поясе счётчика (`Europe/Samara`, +UTC+4), и пересчёт идёт один раз при наполнении слоя: автор отчёта пояса не +пишет, он берёт готовую колонку. `Date` не участвует вовсе — у типа пояса нет. + +Объявить местным и ODS — `DateTime('Europe/Samara')` — соблазнительно: байты те +же, меняется одна линза, и `toDate(UTCEventTime)` начинает совпадать с +`EventDate` всегда. Отвергнуто потому, что колонка зовётся `UTCEventTime` и в +настоящей выгрузке Метрики она в UTC, а стёртое расхождение — тот самый урок, +ради которого мир сделан с поясом счётчика. **Линза выбирается дважды, и второй раз упустить легко.** На чтении — показать -метку или свести её к дате. На записи — превратить строку в число: на проводе -`UTCEventTime` приезжает как `2026-06-04T20:58:56Z`, но суффикс `Z` в маске -разбора не признак зоны, а буква, которую парсер сверяет и выбрасывает. Значит -`parseDateTimeOrNull` получает показания часов и решает, чьи они; без третьего -аргумента — по поясу сервера. Тип колонки тут не помогает: он про то, как число -читают, а не про то, какое число ляжет. Поэтому имя пояса стоит и в разборе; -сам разбор и выбор функции решены в [ADR 0005](../adr/0005-event-ingestion.md). +метку или свести её к дате. На записи — превратить строку в число: суффикс `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}`. База в пути @@ -452,9 +445,10 @@ ODS. Второе: матвью приёма создаётся последне завязан на движок базы `Atomic`. `ON CLUSTER` ждёт все хосты и бросает по таймауту; `CREATE ... IF NOT EXISTS` на существующем объекте не бросает. `parseDateTime` и его родня принимают пояс необязательным последним аргументом, -а без него берут пояс сессии или сервера. У колонки с объявленным поясом -значения приводятся к нему, у колонки без объявленного действует пояс сервера -(сверка 8 августа 2026 года). +а без него берут пояс сессии — он же по умолчанию серверный. У колонки с +объявленным поясом значения приводятся к нему; у колонки без объявленного +`session_timezone` перекрывает серверную настройку на выводе, но тип, с которым +считают функции, остаётся прежним (сверка 8 августа 2026 года). **Проверено на стенде.** Опыты прогнаны на живом кластере: пять при исполнении #37 (четыре 5 августа 2026 года, пятый 6 августа), пять при исполнении #43 @@ -470,15 +464,15 @@ ODS. Второе: матвью приёма создаётся последне - `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` и при чужом поясе сессии. +- У колонки без объявленного пояса глаз и `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)` в обоих случаях + `2026-06-04`. То есть вывод колонки идёт по поясу сессии, а функция — по + поясу типа, и тип на сессию не смотрит. Родной клиент и HTTP ведут себя + одинаково. После объявления `DateTime('UTC')` расхождение уходит: проверено + кастом на том же событии — колонка показывает `20:58:56` и при чужом поясе + сессии. - Матвью с источником-`Distributed` срабатывает на вставку именно в эту распределённую таблицу, до раскладки по шардам. Обе матвью разбора стоят над