Как работать с timezone в SQL
Почему это важно
Timezone — одна из самых недооценённых тем в аналитике. Ошибки с тайм-зонами приводят к неправильным отчётам, смещённым когортам, неправильным границам дней.
Типичная ситуация: у вас есть события в таблице, вы группируете по дате, отчёт отдаёт неправильные цифры. Потому что события в UTC, а вам нужны дни по Москве. Пользователь, зашедший 23:30 UTC, для московского времени уже следующий день.
Разобраться в timezone — часовая инвестиция, которая сэкономит дни страданий.
TIMESTAMP vs TIMESTAMPTZ
В PostgreSQL есть два типа: TIMESTAMP (без timezone) и TIMESTAMPTZ (с timezone).
TIMESTAMP хранит просто дату и время, без контекста. '2026-04-15 10:00:00' — и не знаем, по какому часовому поясу.
TIMESTAMPTZ хранит абсолютный момент во времени. Внутри PostgreSQL сохраняет в UTC. При выводе конвертирует в текущую сессионную timezone.
Для production-систем используйте TIMESTAMPTZ. Она не теряет timezone информацию, корректно работает при миграциях между серверами и не требует постоянных преобразований.
Базовая работа
Создание таблицы:
CREATE TABLE events (
event_id BIGSERIAL PRIMARY KEY,
user_id BIGINT,
event_time TIMESTAMPTZ,
event_type VARCHAR(50)
);Вставка данных:
-- С явной timezone
INSERT INTO events (user_id, event_time, event_type)
VALUES (1, '2026-04-15 10:00:00 Europe/Moscow', 'login');
-- Или через now()
INSERT INTO events (user_id, event_time, event_type)
VALUES (1, now(), 'login');now() всегда возвращает UTC — это абсолютный момент времени. При выводе в TIMESTAMPTZ увидите в вашей сессионной tz.
Настройка сессионной timezone
Сессионная timezone влияет на отображение и интерпретацию timestamp:
-- Показать текущую
SHOW timezone;
-- Установить для сессии
SET timezone = 'Europe/Moscow';
-- Установить постоянно для пользователя
ALTER USER analyst SET timezone = 'Europe/Moscow';После SET timezone = 'Europe/Moscow' все TIMESTAMPTZ показываются в московском времени. Но хранятся внутренне всё равно в UTC.
Попробовать силы на подобных вопросах проще всего в тренажёре Карьерник — прямо в Telegram, без регистрации через сайт.
Конвертация timezone
Самая полезная операция — AT TIME ZONE:
-- Конвертировать UTC в московское для вывода
SELECT event_time AT TIME ZONE 'Europe/Moscow' AS msk_time
FROM events;
-- Или через pretty cast
SELECT event_time::TIMESTAMP AT TIME ZONE 'UTC' AT TIME ZONE 'Europe/Moscow'
FROM events;Первая форма — из TIMESTAMPTZ в locale TIMESTAMP. Вторая — когда данные в TIMESTAMP без timezone, но вы знаете, что они UTC, и хотите в московское время.
Группировка по дате в правильной timezone
Самая частая задача — дневная агрегация. Нужно сгруппировать events по дням в московском времени:
SELECT
(event_time AT TIME ZONE 'Europe/Moscow')::DATE AS moscow_day,
COUNT(*) AS events
FROM events
GROUP BY 1
ORDER BY 1;Без AT TIME ZONE группировка будет по UTC-дням. Event пользователя в 00:30 MSK попадёт в предыдущий UTC-день.
DAU с учётом timezone
Для multi-timezone продуктов:
-- Московский DAU
SELECT
(event_time AT TIME ZONE 'Europe/Moscow')::DATE AS day,
COUNT(DISTINCT user_id) AS dau_moscow
FROM events
WHERE event_type = 'app_open'
GROUP BY 1
ORDER BY 1;Для отчётов глобальных компаний (Яндекс, Авито) часто используют «условно пользовательское время» — приводят к timezone пользователя, если она известна. Но это сложнее и реже в classic reporting.
Daylight Saving Time
Россия не переводит часы с 2011 года, но многие страны переводят. Это может создавать проблемы:
- В марте день «короче» на час (23 часа).
- В октябре «длиннее» на час (25 часов).
Если считать DAU по этим дням без учёта DST, получите скачки. PostgreSQL корректно обрабатывает DST, если используете TIMESTAMPTZ и правильные timezone names (Europe/London, не UTC+0).
Частые ошибки
Первая — сохранять даты в TIMESTAMP без timezone. Потом при переходе на другой сервер с другой настройкой timezone результаты меняются.
Вторая — мешать naive и aware timestamps. Сравнение timestamp_col = '2026-04-15 10:00' работает только если оба типа согласованы.
Третья — ::date без AT TIME ZONE. Даёт UTC-дату, которую никто не ожидал.
Четвёртая — использовать UTC+3 вместо Europe/Moscow. В 2011 Россия могла перейти на UTC+4 (и потом обратно). Абстрактные offset-ы не учитывают исторические изменения.
Пройти 30–50 задач по теме за вечер можно в Telegram-тренажёре. Это то, что отличает «знаю» от «уверенно отвечу на собесе».
В ClickHouse
ClickHouse имеет тип DateTime и DateTime64 с опциональной timezone:
CREATE TABLE events (
event_time DateTime('Europe/Moscow'),
user_id UInt64
) ENGINE = MergeTree() ORDER BY event_time;
-- Функция для конвертации
SELECT toTimeZone(event_time, 'Asia/Tokyo') FROM events;ClickHouse с timezone работает немного иначе, чем PostgreSQL, но общие принципы те же: храните timezone явно, конвертируйте при выводе.
В pandas
Pandas хорошо работает с timezone:
import pandas as pd
df['event_time'] = pd.to_datetime(df['event_time']).dt.tz_localize('UTC')
df['msk_time'] = df['event_time'].dt.tz_convert('Europe/Moscow')
df['msk_date'] = df['msk_time'].dt.dateГруппировка по local date:
df.groupby(df['msk_date'])['user_id'].nunique()Практика — похожа на SQL. Главное — осознанно работать с tz.
Best practices
Храните в UTC, выводите в user/business timezone. Это устраняет большинство проблем.
Используйте TIMESTAMPTZ (или аналог) в production-системах. TIMESTAMP только для локальных событий, которые не имеют смысла в других timezone (дни рождения, события по местному календарю).
Явно указывайте timezone в SQL-запросах. AT TIME ZONE 'Europe/Moscow' читаемо, не зависит от настройки сервера.
Документируйте, в какой timezone хранятся данные и отображаются отчёты. В doc каждой таблицы.
Читайте также
FAQ
UTC или Moscow time для хранения?
UTC всегда. Конвертируйте при выводе.
Как решить, какой timezone показывать в отчётах?
Обычно timezone головного офиса компании. Для global reporting — UTC.
Ошибка «cannot cast aware to naive»?
Вы смешиваете TIMESTAMPTZ и TIMESTAMP. Приводите обе стороны к одному типу.
Как проверить, какая timezone у данных?
SELECT event_time FROM events LIMIT 1; — если выводится с +03, это TIMESTAMPTZ. Без — TIMESTAMP.