docs(storage): правки конвенции по двум холодным ревью
- Зачем:
- раздел «Часовые пояса» прошёл два холодных ревью — по дефектам и по
уместности. Первое поймало ложный замер и три расхождения с живым
стендом, второе — материал не своей зоны и дубли (#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 к конвенции по-прежнему не приведён: документы описывают цель
This commit is contained in:
@@ -232,6 +232,11 @@ ClickHouse 26.3.17.56. Все четыре ответили так, как жд
|
|||||||
всех трёх NULL. Источник у топика один и шлёт одну запись, так что широта не
|
всех трёх NULL. Источник у топика один и шлёт одну запись, так что широта не
|
||||||
нужна вовсе, а платится за неё отключённой проверкой.
|
нужна вовсе, а платится за неё отключённой проверкой.
|
||||||
|
|
||||||
|
Пояс разбору при #63 добавлен третьим аргументом — `'UTC'`; вызов выше приведён
|
||||||
|
без него, каким он был до этого решения. Без имени пояса функция трактует
|
||||||
|
показания часов по поясу сессии, а тот по умолчанию серверный. Правило целиком
|
||||||
|
и его довод — [конвенция часовых поясов](../architecture/storage.md).
|
||||||
|
|
||||||
Цена выбора измерена на настоящих данных: по всем 101 252 строкам сырья
|
Цена выбора измерена на настоящих данных: по всем 101 252 строкам сырья
|
||||||
модельного дня (день залит дважды) точный формат разобрал метку у каждой, и
|
модельного дня (день залит дважды) точный формат разобрал метку у каждой, и
|
||||||
ни на одной не разошёлся с `parseDateTimeBestEffort`. Различаются они только
|
ни на одной не разошёлся с `parseDateTimeBestEffort`. Различаются они только
|
||||||
|
|||||||
@@ -154,9 +154,14 @@ Greenplum, чтобы словарь был общим у двух хранил
|
|||||||
|
|
||||||
**Линза называется явно — в типе колонки либо в вызове функции.** Третий
|
**Линза называется явно — в типе колонки либо в вызове функции.** Третий
|
||||||
источник, умолчание сервера, в коде не виден и меняется снаружи, поэтому в DDL
|
источник, умолчание сервера, в коде не виден и меняется снаружи, поэтому в DDL
|
||||||
и запросах его не остаётся. Правило стоит на источнике пояса, а не на функции:
|
и запросах его не остаётся. Прописать этот пояс своей рукой — `<timezone>` в
|
||||||
`toDate` по колонке, чей тип пояс несёт, законен и имени не требует — так стоит
|
конфигурации ноды или `TZ` контейнеру — было бы той же болезнью с другим
|
||||||
`PARTITION BY toDate(_load_ts)` у сырья и у таблицы ошибок. Имя пишется тогда,
|
умолчанием, и вдобавок отняло бы проверку: когда линза названа в типах и в
|
||||||
|
разборе, пояс сервера на данные не влияет нигде, и в этом можно убедиться,
|
||||||
|
поменяв его. Правило стоит на источнике пояса, а не на функции:
|
||||||
|
`toDate` по колонке, чей тип пояс несёт, законен и имени не требует — так и
|
||||||
|
работают ключи партиций `toDate(_load_ts)` у сырья и у таблицы ошибок, когда
|
||||||
|
`_load_ts` типизирован. Имя пишется тогда,
|
||||||
когда нужна другая линза, чем у колонки: `toDate(UTCEventTime, 'Europe/Samara')`
|
когда нужна другая линза, чем у колонки: `toDate(UTCEventTime, 'Europe/Samara')`
|
||||||
— это «день по часам счётчика».
|
— это «день по часам счётчика».
|
||||||
|
|
||||||
@@ -164,41 +169,29 @@ Greenplum, чтобы словарь был общим у двух хранил
|
|||||||
выгрузки и говорит на языке выгрузки: у Метрики `UTCEventTime` абсолютна,
|
выгрузки и говорит на языке выгрузки: у Метрики `UTCEventTime` абсолютна,
|
||||||
значит `DateTime('UTC')`; служебные метки `_load_ts` и `kafka_timestamp`
|
значит `DateTime('UTC')`; служебные метки `_load_ts` и `kafka_timestamp`
|
||||||
абсолютны тоже — `DateTime64(3, 'UTC')`. DDS и витрины обслуживают человека с
|
абсолютны тоже — `DateTime64(3, 'UTC')`. DDS и витрины обслуживают человека с
|
||||||
дашбордом, поэтому время там лежит местным, в поясе счётчика, и пересчёт идёт
|
дашбордом, поэтому время там лежит местным, в поясе счётчика (`Europe/Samara`,
|
||||||
один раз при наполнении слоя: автор отчёта пояса не пишет, он берёт готовую
|
UTC+4), и пересчёт идёт один раз при наполнении слоя: автор отчёта пояса не
|
||||||
колонку. `Date` не участвует вовсе — у типа пояса нет.
|
пишет, он берёт готовую колонку. `Date` не участвует вовсе — у типа пояса нет.
|
||||||
|
|
||||||
|
Объявить местным и ODS — `DateTime('Europe/Samara')` — соблазнительно: байты те
|
||||||
|
же, меняется одна линза, и `toDate(UTCEventTime)` начинает совпадать с
|
||||||
|
`EventDate` всегда. Отвергнуто потому, что колонка зовётся `UTCEventTime` и в
|
||||||
|
настоящей выгрузке Метрики она в UTC, а стёртое расхождение — тот самый урок,
|
||||||
|
ради которого мир сделан с поясом счётчика.
|
||||||
|
|
||||||
**Линза выбирается дважды, и второй раз упустить легко.** На чтении — показать
|
**Линза выбирается дважды, и второй раз упустить легко.** На чтении — показать
|
||||||
метку или свести её к дате. На записи — превратить строку в число: на проводе
|
метку или свести её к дате. На записи — превратить строку в число: суффикс `Z`
|
||||||
`UTCEventTime` приезжает как `2026-06-04T20:58:56Z`, но суффикс `Z` в маске
|
на проводе зоны не даёт, маска разбора съедает его буквой, и
|
||||||
разбора не признак зоны, а буква, которую парсер сверяет и выбрасывает. Значит
|
`parseDateTimeOrNull` без третьего аргумента трактует показания часов по поясу
|
||||||
`parseDateTimeOrNull` получает показания часов и решает, чьи они; без третьего
|
сессии, а тот по умолчанию серверный. Тип колонки тут не помогает: он про то,
|
||||||
аргумента — по поясу сервера. Тип колонки тут не помогает: он про то, как число
|
как число читают, а не про то, какое ляжет. Механика и выбор функции — [ADR
|
||||||
читают, а не про то, какое число ляжет. Поэтому имя пояса стоит и в разборе;
|
0005](../adr/0005-event-ingestion.md).
|
||||||
сам разбор и выбор функции решены в [ADR 0005](../adr/0005-event-ingestion.md).
|
|
||||||
|
|
||||||
**День берётся из `EventDate`.** Дата в поясе счётчика уже посчитана
|
**День берётся из `EventDate`.** Дата в поясе счётчика уже посчитана
|
||||||
генератором и лежит колонкой, так что суточные срезы группируются по ней и
|
генератором и лежит колонкой, так что суточные срезы группируются по ней и
|
||||||
пояса не упоминают вовсе. Почему у ночных событий `toDate(UTCEventTime)` с ней
|
пояса не упоминают вовсе. Почему у ночных событий `toDate(UTCEventTime)` с ней
|
||||||
расходится — [спека генератора](../specs/2026-08-01-generator.md), раздел 9.
|
расходится — [спека генератора](../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
|
## Путь реплицированных таблиц в keeper
|
||||||
|
|
||||||
Шаблон — `/clickhouse/tables/{shard}/{database}/{table}`. База в пути
|
Шаблон — `/clickhouse/tables/{shard}/{database}/{table}`. База в пути
|
||||||
@@ -452,9 +445,10 @@ ODS. Второе: матвью приёма создаётся последне
|
|||||||
завязан на движок базы `Atomic`. `ON CLUSTER` ждёт все хосты и бросает по
|
завязан на движок базы `Atomic`. `ON CLUSTER` ждёт все хосты и бросает по
|
||||||
таймауту; `CREATE ... IF NOT EXISTS` на существующем объекте не бросает.
|
таймауту; `CREATE ... IF NOT EXISTS` на существующем объекте не бросает.
|
||||||
`parseDateTime` и его родня принимают пояс необязательным последним аргументом,
|
`parseDateTime` и его родня принимают пояс необязательным последним аргументом,
|
||||||
а без него берут пояс сессии или сервера. У колонки с объявленным поясом
|
а без него берут пояс сессии — он же по умолчанию серверный. У колонки с
|
||||||
значения приводятся к нему, у колонки без объявленного действует пояс сервера
|
объявленным поясом значения приводятся к нему; у колонки без объявленного
|
||||||
(сверка 8 августа 2026 года).
|
`session_timezone` перекрывает серверную настройку на выводе, но тип, с которым
|
||||||
|
считают функции, остаётся прежним (сверка 8 августа 2026 года).
|
||||||
|
|
||||||
**Проверено на стенде.** Опыты прогнаны на живом кластере: пять при исполнении
|
**Проверено на стенде.** Опыты прогнаны на живом кластере: пять при исполнении
|
||||||
#37 (четыре 5 августа 2026 года, пятый 6 августа), пять при исполнении #43
|
#37 (четыре 5 августа 2026 года, пятый 6 августа), пять при исполнении #43
|
||||||
@@ -470,15 +464,15 @@ 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` живёт по HTTP и только там. Событие
|
- У колонки без объявленного пояса глаз и `GROUP BY` расходятся. Событие
|
||||||
`WatchID = 113504893317`, `EventDate` = `2026-06-05`: по HTTP без настроек
|
`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)` в обоих случаях
|
||||||
`2026-06-04`. Родной `clickhouse-client` `session_timezone` к отображению
|
`2026-06-04`. То есть вывод колонки идёт по поясу сессии, а функция — по
|
||||||
колонки не применяет и показывает `20:58:56` в обоих случаях. То есть сцену
|
поясу типа, и тип на сессию не смотрит. Родной клиент и HTTP ведут себя
|
||||||
видно из клиентов поверх HTTP — Superset в стенде ходит именно так. После объявления
|
одинаково. После объявления `DateTime('UTC')` расхождение уходит: проверено
|
||||||
`DateTime('UTC')` она уходит: проверено кастом на том же событии — колонка
|
кастом на том же событии — колонка показывает `20:58:56` и при чужом поясе
|
||||||
показывает `20:58:56` и при чужом поясе сессии.
|
сессии.
|
||||||
|
|
||||||
- Матвью с источником-`Distributed` срабатывает на вставку именно в эту
|
- Матвью с источником-`Distributed` срабатывает на вставку именно в эту
|
||||||
распределённую таблицу, до раскладки по шардам. Обе матвью разбора стоят над
|
распределённую таблицу, до раскладки по шардам. Обе матвью разбора стоят над
|
||||||
|
|||||||
Reference in New Issue
Block a user