feat(ddl): пояс назван явно — в типах колонок и в разборе строки
- Зачем: - конвенция #63 записана, а код её не достиг: колонки времени стояли без пояса, и сходилось всё лишь потому, что пояс сервера — UTC. - Что: - UTCEventTime объявлен DateTime('UTC'), служебные метки _load_ts и kafka_timestamp — DateTime64(3, 'UTC') в STG и ODS. - parseDateTimeOrNull получил третьим аргументом 'UTC': маска сверяет суффикс Z как букву, зоны из строки не берёт вовсе. - контракт схемы и описание выгрузки несут тип с поясом; имя пояса Europe/Samara встало рядом со смещением в world.py, сходимость сверяет тест. - учебный комментарий о линзе — у первой колонки с явным поясом. - Проверка: - make lint, make typecheck, make test (408 тестов) - make clean && make up && make check-clickhouse — 9 из 9 - замер тикета повторён: под session_timezone='Europe/Samara' колонка показана 2026-05-31 23:37:00, как и без настроек Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -218,7 +218,7 @@ ClickHouse 26.3.17.56. Все четыре ответили так, как жд
|
|||||||
`2026-06-01` и разбирается `JSONExtract` без оговорок.
|
`2026-06-01` и разбирается `JSONExtract` без оговорок.
|
||||||
|
|
||||||
Разбор метки времени идёт
|
Разбор метки времени идёт
|
||||||
`parseDateTimeOrNull(JSONExtractString(raw, 'UTCEventTime'), '%Y-%m-%dT%H:%i:%SZ')`
|
`parseDateTimeOrNull(JSONExtractString(raw, 'UTCEventTime'), '%Y-%m-%dT%H:%i:%SZ', 'UTC')`
|
||||||
— по буквально названному формату, а не через `parseDateTimeBestEffort`.
|
— по буквально названному формату, а не через `parseDateTimeBestEffort`.
|
||||||
Обе функции ISO-8601 понимают и обе в варианте `*OrNull` отдают NULL вместо
|
Обе функции ISO-8601 понимают и обе в варианте `*OrNull` отдают NULL вместо
|
||||||
исключения, то есть годятся в предикат. Выбран точный формат потому, что
|
исключения, то есть годятся в предикат. Выбран точный формат потому, что
|
||||||
@@ -232,10 +232,11 @@ ClickHouse 26.3.17.56. Все четыре ответили так, как жд
|
|||||||
всех трёх NULL. Источник у топика один и шлёт одну запись, так что широта не
|
всех трёх NULL. Источник у топика один и шлёт одну запись, так что широта не
|
||||||
нужна вовсе, а платится за неё отключённой проверкой.
|
нужна вовсе, а платится за неё отключённой проверкой.
|
||||||
|
|
||||||
Пояс разбору при #63 добавлен третьим аргументом — `'UTC'`; вызов выше приведён
|
Третий аргумент — имя пояса, `'UTC'` — пришёл с конвенцией #63. Маска сверяет
|
||||||
без него, каким он был до этого решения. Без имени пояса функция трактует
|
суффикс `Z` как букву и выбрасывает, зоны из строки не берёт вовсе, поэтому без
|
||||||
показания часов по поясу сессии, а тот по умолчанию серверный. Правило целиком
|
имени функция трактует показания часов по поясу сессии, а тот по умолчанию
|
||||||
и его довод — [конвенция часовых поясов](../architecture/storage.md).
|
серверный. Правило целиком и его довод — [конвенция часовых
|
||||||
|
поясов](../architecture/storage.md).
|
||||||
|
|
||||||
Цена выбора измерена на настоящих данных: по всем 101 252 строкам сырья
|
Цена выбора измерена на настоящих данных: по всем 101 252 строкам сырья
|
||||||
модельного дня (день залит дважды) точный формат разобрал метку у каждой, и
|
модельного дня (день залит дважды) точный формат разобрал метку у каждой, и
|
||||||
|
|||||||
@@ -104,11 +104,11 @@ keeper, Kafka, каркас сервисов. Этап 2 идёт: в `sql/ddl/`
|
|||||||
С `kafka_timestamp` сложнее, и форма его решена на стенде. Меток времени движок
|
С `kafka_timestamp` сложнее, и форма его решена на стенде. Меток времени движок
|
||||||
даёт две: `_timestamp` — `Nullable(DateTime)`, то есть секунды, и
|
даёт две: `_timestamp` — `Nullable(DateTime)`, то есть секунды, и
|
||||||
`_timestamp_ms` — `Nullable(DateTime64(3))`, миллисекунды. Колонка объявлена
|
`_timestamp_ms` — `Nullable(DateTime64(3))`, миллисекунды. Колонка объявлена
|
||||||
`Nullable(DateTime64(3))` и заполняется из `_timestamp_ms`: у брокера метка
|
`Nullable(DateTime64(3, 'UTC'))` и заполняется из `_timestamp_ms`: у брокера
|
||||||
миллисекундная, соседняя `_load_ts` тоже `DateTime64(3)`, а слой сырья хранит
|
метка миллисекундная, соседняя `_load_ts` тоже миллисекундная, а слой сырья
|
||||||
приехавшее, и округлять ему нечего. Обнуляемость нужна отдельно от разрядности:
|
хранит приехавшее, и округлять ему нечего. Обнуляемость нужна отдельно от
|
||||||
брокер метку заполняет не всегда, а необнуляемый тип значил бы либо падение
|
разрядности: брокер метку заполняет не всегда, а необнуляемый тип значил бы
|
||||||
приёма на первом сообщении, либо тихие нули за 1970 год.
|
либо падение приёма на первом сообщении, либо тихие нули за 1970 год.
|
||||||
|
|
||||||
Заполняются все они выражением в `SELECT` матвью приёма, а не `DEFAULT` в
|
Заполняются все они выражением в `SELECT` матвью приёма, а не `DEFAULT` в
|
||||||
таблице. Для `consumer_host` это обязательно: `DEFAULT hostName()` вычисляется
|
таблице. Для `consumer_host` это обязательно: `DEFAULT hostName()` вычисляется
|
||||||
@@ -124,9 +124,9 @@ kafka_offset)`: разбор полётов идёт от «какое сооб
|
|||||||
нет. Замену версий сюда ставить нельзя — она отменила бы свойство слоя, ради
|
нет. Замену версий сюда ставить нельзя — она отменила бы свойство слоя, ради
|
||||||
которого он заведён: повтор доставки в сырье обязан быть виден.
|
которого он заведён: повтор доставки в сырье обязан быть виден.
|
||||||
|
|
||||||
Метка времени загрузки зовётся `_load_ts`, тип `DateTime64(3)`. Ставится она
|
Метка времени загрузки зовётся `_load_ts`, тип `DateTime64(3, 'UTC')`. Ставится
|
||||||
один раз, в матвью приёма, и дальше переносится из STG в ODS как есть: колонка
|
она один раз, в матвью приёма, и дальше переносится из STG в ODS как есть:
|
||||||
отвечает на вопрос «когда строка приехала в хранилище», а не «когда её
|
колонка отвечает на вопрос «когда строка приехала в хранилище», а не «когда её
|
||||||
разобрали». В ODS она же служит колонкой версии `ReplacingMergeTree`, и работа у
|
разобрали». В ODS она же служит колонкой версии `ReplacingMergeTree`, и работа у
|
||||||
этой версии ровно одна — схлопнуть повтор доставки. Содержимое у повтора то же
|
этой версии ровно одна — схлопнуть повтор доставки. Содержимое у повтора то же
|
||||||
самое, отличается только метка, поэтому какая из двух строк переживёт мерж,
|
самое, отличается только метка, поэтому какая из двух строк переживёт мерж,
|
||||||
@@ -160,10 +160,9 @@ Greenplum, чтобы словарь был общим у двух хранил
|
|||||||
разборе, пояс сервера на данные не влияет нигде, и в этом можно убедиться,
|
разборе, пояс сервера на данные не влияет нигде, и в этом можно убедиться,
|
||||||
поменяв его. Правило стоит на источнике пояса, а не на функции:
|
поменяв его. Правило стоит на источнике пояса, а не на функции:
|
||||||
`toDate` по колонке, чей тип пояс несёт, законен и имени не требует — так и
|
`toDate` по колонке, чей тип пояс несёт, законен и имени не требует — так и
|
||||||
работают ключи партиций `toDate(_load_ts)` у сырья и у таблицы ошибок, когда
|
работают ключи партиций `toDate(_load_ts)` у сырья и у таблицы ошибок. Имя
|
||||||
`_load_ts` типизирован. Имя пишется тогда,
|
пишется тогда, когда нужна другая линза, чем у колонки:
|
||||||
когда нужна другая линза, чем у колонки: `toDate(UTCEventTime, 'Europe/Samara')`
|
`toDate(UTCEventTime, 'Europe/Samara')` — это «день по часам счётчика».
|
||||||
— это «день по часам счётчика».
|
|
||||||
|
|
||||||
**Какая линза, решает слой — по тому, кого он обслуживает.** ODS хранит снимок
|
**Какая линза, решает слой — по тому, кого он обслуживает.** ODS хранит снимок
|
||||||
выгрузки и говорит на языке выгрузки: у Метрики `UTCEventTime` абсолютна,
|
выгрузки и говорит на языке выгрузки: у Метрики `UTCEventTime` абсолютна,
|
||||||
@@ -452,7 +451,7 @@ ODS. Второе: матвью приёма создаётся последне
|
|||||||
|
|
||||||
**Проверено на стенде.** Опыты прогнаны на живом кластере: пять при исполнении
|
**Проверено на стенде.** Опыты прогнаны на живом кластере: пять при исполнении
|
||||||
#37 (четыре 5 августа 2026 года, пятый 6 августа), пять при исполнении #43
|
#37 (четыре 5 августа 2026 года, пятый 6 августа), пять при исполнении #43
|
||||||
(7 августа) и три при обсуждении #63 (8 августа). Все подтвердили то, что здесь
|
(7 августа) и четыре при #63 (8 августа). Все подтвердили то, что здесь
|
||||||
написано.
|
написано.
|
||||||
|
|
||||||
- Разбор строки берёт пояс у сессии, а не из строки. Под
|
- Разбор строки берёт пояс у сессии, а не из строки. Под
|
||||||
@@ -470,9 +469,14 @@ ODS. Второе: матвью приёма создаётся последне
|
|||||||
`2026-06-05 00:58:56`, а `toDate(UTCEventTime)` в обоих случаях
|
`2026-06-05 00:58:56`, а `toDate(UTCEventTime)` в обоих случаях
|
||||||
`2026-06-04`. То есть вывод колонки идёт по поясу сессии, а функция — по
|
`2026-06-04`. То есть вывод колонки идёт по поясу сессии, а функция — по
|
||||||
поясу типа, и тип на сессию не смотрит. Родной клиент и HTTP ведут себя
|
поясу типа, и тип на сессию не смотрит. Родной клиент и HTTP ведут себя
|
||||||
одинаково. После объявления `DateTime('UTC')` расхождение уходит: проверено
|
одинаково.
|
||||||
кастом на том же событии — колонка показывает `20:58:56` и при чужом поясе
|
- С объявленным поясом расхождение уходит. Стенд поднят с нуля уже по
|
||||||
сессии.
|
конвенции; событие `WatchID = 384218330540`, `EventDate` = `2026-06-01`:
|
||||||
|
колонка `DateTime('UTC')` показана `2026-05-31 23:37:00` и без настроек, и
|
||||||
|
под `session_timezone = 'Europe/Samara'`, по родному клиенту и по HTTP.
|
||||||
|
`toDate(UTCEventTime)` даёт `2026-05-31`, `toDate(UTCEventTime,
|
||||||
|
'Europe/Samara')` — `2026-06-01`, `EventDate` — `2026-06-01`: расхождение
|
||||||
|
`toDate(UTCEventTime)` с `EventDate` осталось, это мир, а не пояс колонки.
|
||||||
|
|
||||||
- Матвью с источником-`Distributed` срабатывает на вставку именно в эту
|
- Матвью с источником-`Distributed` срабатывает на вставку именно в эту
|
||||||
распределённую таблицу, до раскладки по шардам. Обе матвью разбора стоят над
|
распределённую таблицу, до раскладки по шардам. Обе матвью разбора стоят над
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
| 3 | `ClientID` | `UInt64` | `uint64` | `client_id` | анонимный id браузера — кука; по хешу от неё таблица шардируется |
|
| 3 | `ClientID` | `UInt64` | `uint64` | `client_id` | анонимный id браузера — кука; по хешу от неё таблица шардируется |
|
||||||
| 4 | `CounterID` | `UInt32` | `uint32` | `counter_id` | id счётчика: на стенде константа, сайт один |
|
| 4 | `CounterID` | `UInt32` | `uint32` | `counter_id` | id счётчика: на стенде константа, сайт один |
|
||||||
| 5 | `EventDate` | `Date` | `datetime64[D]` | `event_date` | дата события в часовом поясе счётчика; по ней режется партиция. Дату из `UTCEventTime` не выводить: у ночных событий она на сутки другая |
|
| 5 | `EventDate` | `Date` | `datetime64[D]` | `event_date` | дата события в часовом поясе счётчика; по ней режется партиция. Дату из `UTCEventTime` не выводить: у ночных событий она на сутки другая |
|
||||||
| 6 | `UTCEventTime` | `DateTime` | `datetime64[s]` | `utc_event_time` | время события в UTC — единственная метка времени, как у Метрики; сутки же считаются в поясе счётчика, поэтому `toDate(UTCEventTime)` ≠ `EventDate` |
|
| 6 | `UTCEventTime` | `DateTime('UTC')` | `datetime64[s]` | `utc_event_time` | время события в UTC — единственная метка времени, как у Метрики; сутки же считаются в поясе счётчика, поэтому `toDate(UTCEventTime)` ≠ `EventDate` |
|
||||||
| 7 | `ClientTimeZone` | `Int16` | `int16` | `client_timezone` | смещение часового пояса клиента от UTC, в минутах |
|
| 7 | `ClientTimeZone` | `Int16` | `int16` | `client_timezone` | смещение часового пояса клиента от UTC, в минутах |
|
||||||
| 8 | `EventType` | `LowCardinality(String)` | `object` | `event_type` | тип события: pageview, add_to_cart, purchase — добавка стенда, у Метрики такого поля нет |
|
| 8 | `EventType` | `LowCardinality(String)` | `object` | `event_type` | тип события: pageview, add_to_cart, purchase — добавка стенда, у Метрики такого поля нет |
|
||||||
| 9 | `Sign` | `Int8` | `int8` | `sign` | всегда 1: колонка формата, исправлений записей генератор не шлёт |
|
| 9 | `Sign` | `Int8` | `int8` | `sign` | всегда 1: колонка формата, исправлений записей генератор не шлёт |
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
| `ClientID` | UInt64 | анонимный id браузера (кука) — ключ шардирования |
|
| `ClientID` | UInt64 | анонимный id браузера (кука) — ключ шардирования |
|
||||||
| `CounterID` | UInt32 | константа стенда (один сайт) |
|
| `CounterID` | UInt32 | константа стенда (один сайт) |
|
||||||
| `EventDate` | Date | дата события |
|
| `EventDate` | Date | дата события |
|
||||||
| `UTCEventTime` | DateTime | единственная метка времени, как у Метрики |
|
| `UTCEventTime` | DateTime('UTC') | единственная метка времени, как у Метрики |
|
||||||
| `ClientTimeZone` | Int16 | смещение пояса клиента в минутах |
|
| `ClientTimeZone` | Int16 | смещение пояса клиента в минутах |
|
||||||
| `EventType` | LowCardinality(String) | `pageview` / `add_to_cart` / `purchase` |
|
| `EventType` | LowCardinality(String) | `pageview` / `add_to_cart` / `purchase` |
|
||||||
| `Sign` | Int8 | всегда 1: колонка формата, механика исправлений не реализована (см. 1.1) |
|
| `Sign` | Int8 | всегда 1: колонка формата, механика исправлений не реализована (см. 1.1) |
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ COLUMNS: tuple[Column, ...] = (
|
|||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
name="UTCEventTime",
|
name="UTCEventTime",
|
||||||
clickhouse_type="DateTime",
|
clickhouse_type="DateTime('UTC')",
|
||||||
numpy_dtype="datetime64[s]",
|
numpy_dtype="datetime64[s]",
|
||||||
normalized_name="utc_event_time",
|
normalized_name="utc_event_time",
|
||||||
group=ColumnGroup.IDENTIFIERS,
|
group=ColumnGroup.IDENTIFIERS,
|
||||||
|
|||||||
@@ -16,11 +16,15 @@ from datetime import date
|
|||||||
# Счётчик стенда: сайт один, номер — константа мира.
|
# Счётчик стенда: сайт один, номер — константа мира.
|
||||||
COUNTER_ID = 42150607
|
COUNTER_ID = 42150607
|
||||||
|
|
||||||
# Часовой пояс счётчика, минуты от UTC: Самара, UTC+4. Модельные сутки
|
# Часовой пояс счётчика — Самара, UTC+4 — записан двумя способами, потому что
|
||||||
# считаются в этом поясе, как в выгрузке Метрики: `EventDate` — дата в поясе
|
# стороны просят разное. Генератору нужны минуты: в этом поясе считаются
|
||||||
# счётчика, `UTCEventTime` — абсолютная метка. Отсюда следствие, о котором
|
# модельные сутки, как в выгрузке Метрики — `EventDate` дата в поясе счётчика,
|
||||||
# сторона хранилища должна знать заранее: `toDate(UTCEventTime)` ≠ `EventDate`
|
# `UTCEventTime` абсолютная метка. Отсюда следствие, о котором сторона
|
||||||
# у ночных событий (спека генератора, раздел 9).
|
# хранилища должна знать заранее: `toDate(UTCEventTime)` ≠ `EventDate` у ночных
|
||||||
|
# событий (спека генератора, раздел 9). Хранилищу нужно имя из базы поясов: его
|
||||||
|
# просят `toDate` и типы колонок DDS (docs/architecture/storage.md, «Часовые
|
||||||
|
# пояса»).
|
||||||
|
COUNTER_TIMEZONE = "Europe/Samara"
|
||||||
COUNTER_TIMEZONE_MINUTES = 240
|
COUNTER_TIMEZONE_MINUTES = 240
|
||||||
|
|
||||||
# D0 — первый день оси модельного времени, понедельник. Реальный календарь в
|
# D0 — первый день оси модельного времени, понедельник. Реальный календарь в
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ NUMPY_BY_CLICKHOUSE_TYPE = {
|
|||||||
"String": "object",
|
"String": "object",
|
||||||
"LowCardinality(String)": "object",
|
"LowCardinality(String)": "object",
|
||||||
"Date": "datetime64[D]",
|
"Date": "datetime64[D]",
|
||||||
"DateTime": "datetime64[s]",
|
"DateTime('UTC')": "datetime64[s]",
|
||||||
}
|
}
|
||||||
|
|
||||||
METRICA_NAME = re.compile(r"^[A-Za-z][A-Za-z0-9]*$")
|
METRICA_NAME = re.compile(r"^[A-Za-z][A-Za-z0-9]*$")
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
«в среднем 3–4 возврата» и «средняя кука активна ≈1,9 дня».
|
«в среднем 3–4 возврата» и «средняя кука активна ≈1,9 дня».
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime, time, timedelta
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from clickstream_generator import catalog, world
|
from clickstream_generator import catalog, world
|
||||||
|
|
||||||
|
|
||||||
@@ -27,6 +30,13 @@ def test_origin_is_a_monday():
|
|||||||
assert world.ORIGIN.weekday() == 0
|
assert world.ORIGIN.weekday() == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_counter_timezone_name_and_offset_say_the_same_thing():
|
||||||
|
"""Имя пояса просит хранилище, минуты — генератор; расходиться им нельзя."""
|
||||||
|
midnight = datetime.combine(world.ORIGIN, time())
|
||||||
|
named = ZoneInfo(world.COUNTER_TIMEZONE).utcoffset(midnight)
|
||||||
|
assert named == timedelta(minutes=world.COUNTER_TIMEZONE_MINUTES)
|
||||||
|
|
||||||
|
|
||||||
def test_weekly_profile_covers_a_week_and_averages_to_one():
|
def test_weekly_profile_covers_a_week_and_averages_to_one():
|
||||||
assert len(world.WEEKLY_PROFILE_PERCENT) == 7
|
assert len(world.WEEKLY_PROFILE_PERCENT) == 7
|
||||||
assert sum(world.WEEKLY_PROFILE_PERCENT) == 700
|
assert sum(world.WEEKLY_PROFILE_PERCENT) == 700
|
||||||
|
|||||||
@@ -45,10 +45,18 @@ SETTINGS
|
|||||||
-- колонки _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)). Взяты миллисекунды: у брокера метка миллисекундная,
|
||||||
-- _load_ts рядом тоже DateTime64(3), а слой сырья хранит то, что приехало, и
|
-- _load_ts рядом тоже миллисекундная, а слой сырья хранит то, что приехало, и
|
||||||
-- округлять ему нечего. Обнуляемость обязательна: метку брокер заполняет не
|
-- округлять ему нечего. Обнуляемость обязательна: метку брокер заполняет не
|
||||||
-- всегда, а необнуляемый тип дал бы либо падение приёма, либо тихий 1970 год.
|
-- всегда, а необнуляемый тип дал бы либо падение приёма, либо тихий 1970 год.
|
||||||
--
|
--
|
||||||
|
-- Пояс у обеих меток написан в типе — DateTime64(3, 'UTC'); в DDL стенда он
|
||||||
|
-- встречается здесь впервые. Само число от пояса не зависит, это секунды от
|
||||||
|
-- начала эпохи. Пояс — линза: по нему решают, какие часы покажут метку и в
|
||||||
|
-- какие сутки она попадёт, то есть чем окажется toDate(_load_ts) в ключе
|
||||||
|
-- партиции ниже. Не назови линзу — её выберет пояс сервера, умолчание, которого
|
||||||
|
-- в коде не видно. Правило целиком — docs/architecture/storage.md, «Часовые
|
||||||
|
-- пояса».
|
||||||
|
--
|
||||||
-- Нарезка и срок жизни — по _load_ts, то есть по реальному времени загрузки:
|
-- Нарезка и срок жизни — по _load_ts, то есть по реальному времени загрузки:
|
||||||
-- модельный день события живёт в ODS, а по нему TTL был бы просто сломан.
|
-- модельный день события живёт в ODS, а по нему TTL был бы просто сломан.
|
||||||
-- Срок — трое суток плюс хвост до суток: куски снимаются целиком
|
-- Срок — трое суток плюс хвост до суток: куски снимаются целиком
|
||||||
@@ -64,9 +72,9 @@ CREATE TABLE IF NOT EXISTS stg.hits_raw_rep ON CLUSTER clickstream_cluster
|
|||||||
kafka_topic LowCardinality(String),
|
kafka_topic LowCardinality(String),
|
||||||
kafka_partition UInt64,
|
kafka_partition UInt64,
|
||||||
kafka_offset UInt64,
|
kafka_offset UInt64,
|
||||||
kafka_timestamp Nullable(DateTime64(3)),
|
kafka_timestamp Nullable(DateTime64(3, 'UTC')),
|
||||||
consumer_host LowCardinality(String),
|
consumer_host LowCardinality(String),
|
||||||
_load_ts DateTime64(3)
|
_load_ts DateTime64(3, 'UTC')
|
||||||
)
|
)
|
||||||
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/{database}/{table}', '{replica}')
|
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/{database}/{table}', '{replica}')
|
||||||
PARTITION BY toDate(_load_ts)
|
PARTITION BY toDate(_load_ts)
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ CREATE TABLE IF NOT EXISTS ods.event_rep ON CLUSTER clickstream_cluster
|
|||||||
ClientID UInt64,
|
ClientID UInt64,
|
||||||
CounterID UInt32,
|
CounterID UInt32,
|
||||||
EventDate Date,
|
EventDate Date,
|
||||||
UTCEventTime DateTime,
|
UTCEventTime DateTime('UTC'),
|
||||||
ClientTimeZone Int16,
|
ClientTimeZone Int16,
|
||||||
EventType LowCardinality(String),
|
EventType LowCardinality(String),
|
||||||
Sign Int8,
|
Sign Int8,
|
||||||
@@ -93,7 +93,7 @@ CREATE TABLE IF NOT EXISTS ods.event_rep ON CLUSTER clickstream_cluster
|
|||||||
productQuantity Array(UInt64),
|
productQuantity Array(UInt64),
|
||||||
productEventType Array(String),
|
productEventType Array(String),
|
||||||
ecommerce String,
|
ecommerce String,
|
||||||
_load_ts DateTime64(3)
|
_load_ts DateTime64(3, 'UTC')
|
||||||
)
|
)
|
||||||
ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/{shard}/{database}/{table}', '{replica}', _load_ts)
|
ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/{shard}/{database}/{table}', '{replica}', _load_ts)
|
||||||
PARTITION BY EventDate
|
PARTITION BY EventDate
|
||||||
@@ -137,9 +137,9 @@ CREATE TABLE IF NOT EXISTS ods.event_errors_rep ON CLUSTER clickstream_cluster
|
|||||||
kafka_topic LowCardinality(String),
|
kafka_topic LowCardinality(String),
|
||||||
kafka_partition UInt64,
|
kafka_partition UInt64,
|
||||||
kafka_offset UInt64,
|
kafka_offset UInt64,
|
||||||
kafka_timestamp Nullable(DateTime64(3)),
|
kafka_timestamp Nullable(DateTime64(3, 'UTC')),
|
||||||
consumer_host LowCardinality(String),
|
consumer_host LowCardinality(String),
|
||||||
_load_ts DateTime64(3)
|
_load_ts DateTime64(3, 'UTC')
|
||||||
)
|
)
|
||||||
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/{database}/{table}', '{replica}')
|
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/{database}/{table}', '{replica}')
|
||||||
PARTITION BY toDate(_load_ts)
|
PARTITION BY toDate(_load_ts)
|
||||||
|
|||||||
@@ -52,6 +52,12 @@
|
|||||||
-- нужна вовсе, а стоит она отключённой проверкой. Замеры — ADR 0005,
|
-- нужна вовсе, а стоит она отключённой проверкой. Замеры — ADR 0005,
|
||||||
-- «Что проверено».
|
-- «Что проверено».
|
||||||
--
|
--
|
||||||
|
-- Третьим аргументом назван пояс — 'UTC'. Суффикс Z маска сверяет как букву и
|
||||||
|
-- выбрасывает, зоны из строки не берёт вовсе, поэтому без имени функция читала
|
||||||
|
-- бы показания часов по поясу сессии, а тот по умолчанию серверный. Тип
|
||||||
|
-- колонки этого не чинит: он про то, как число покажут, а не какое ляжет.
|
||||||
|
-- Правило и замер — docs/architecture/storage.md, «Часовые пояса».
|
||||||
|
--
|
||||||
-- EventDate в такой подпорке не нуждается: дата уезжает как «2026-06-01», и
|
-- EventDate в такой подпорке не нуждается: дата уезжает как «2026-06-01», и
|
||||||
-- JSONExtract её берёт.
|
-- JSONExtract её берёт.
|
||||||
|
|
||||||
@@ -79,7 +85,7 @@ WITH
|
|||||||
AND JSONExtract(raw, 'ClientID', 'Nullable(UInt64)') IS NOT NULL
|
AND JSONExtract(raw, 'ClientID', 'Nullable(UInt64)') IS NOT NULL
|
||||||
AND JSONExtract(raw, 'EventDate', 'Nullable(Date)') IS NOT NULL
|
AND JSONExtract(raw, 'EventDate', 'Nullable(Date)') IS NOT NULL
|
||||||
AND parseDateTimeOrNull(JSONExtractString(raw, 'UTCEventTime'),
|
AND parseDateTimeOrNull(JSONExtractString(raw, 'UTCEventTime'),
|
||||||
'%Y-%m-%dT%H:%i:%SZ') IS NOT NULL AS key_fields_parsed
|
'%Y-%m-%dT%H:%i:%SZ', 'UTC') IS NOT NULL AS key_fields_parsed
|
||||||
SELECT
|
SELECT
|
||||||
JSONExtract(raw, 'WatchID', 'UInt64') AS WatchID,
|
JSONExtract(raw, 'WatchID', 'UInt64') AS WatchID,
|
||||||
JSONExtract(raw, 'VisitID', 'UInt64') AS VisitID,
|
JSONExtract(raw, 'VisitID', 'UInt64') AS VisitID,
|
||||||
@@ -88,7 +94,7 @@ SELECT
|
|||||||
JSONExtract(raw, 'EventDate', 'Date') AS EventDate,
|
JSONExtract(raw, 'EventDate', 'Date') AS EventDate,
|
||||||
assumeNotNull(parseDateTimeOrNull(
|
assumeNotNull(parseDateTimeOrNull(
|
||||||
JSONExtractString(raw, 'UTCEventTime'),
|
JSONExtractString(raw, 'UTCEventTime'),
|
||||||
'%Y-%m-%dT%H:%i:%SZ')) AS UTCEventTime,
|
'%Y-%m-%dT%H:%i:%SZ', 'UTC')) AS UTCEventTime,
|
||||||
JSONExtract(raw, 'ClientTimeZone', 'Int16') AS ClientTimeZone,
|
JSONExtract(raw, 'ClientTimeZone', 'Int16') AS ClientTimeZone,
|
||||||
JSONExtract(raw, 'EventType', 'String') AS EventType,
|
JSONExtract(raw, 'EventType', 'String') AS EventType,
|
||||||
JSONExtract(raw, 'Sign', 'Int8') AS Sign,
|
JSONExtract(raw, 'Sign', 'Int8') AS Sign,
|
||||||
@@ -173,7 +179,7 @@ WITH
|
|||||||
AND JSONExtract(raw, 'ClientID', 'Nullable(UInt64)') IS NOT NULL
|
AND JSONExtract(raw, 'ClientID', 'Nullable(UInt64)') IS NOT NULL
|
||||||
AND JSONExtract(raw, 'EventDate', 'Nullable(Date)') IS NOT NULL
|
AND JSONExtract(raw, 'EventDate', 'Nullable(Date)') IS NOT NULL
|
||||||
AND parseDateTimeOrNull(JSONExtractString(raw, 'UTCEventTime'),
|
AND parseDateTimeOrNull(JSONExtractString(raw, 'UTCEventTime'),
|
||||||
'%Y-%m-%dT%H:%i:%SZ') IS NOT NULL AS key_fields_parsed
|
'%Y-%m-%dT%H:%i:%SZ', 'UTC') IS NOT NULL AS key_fields_parsed
|
||||||
SELECT
|
SELECT
|
||||||
raw,
|
raw,
|
||||||
multiIf(
|
multiIf(
|
||||||
|
|||||||
Reference in New Issue
Block a user