PostgreSQL timestamp против timestamptz: что реально лежит под капотом?
PostgreSQL хранит и timestamp, и timestamptz как одно 64-битное целое: количество микросекунд с 1970-01-01 00:00:00 UTC. Различие проявляется только при форматировании данных для человека.
Почему здесь все спотыкаются?
- Две колонки, одна дата… два разных результата запроса
- Ваше приложение вставляет
2025-07-29 10:00, а другая команда видит02:00 - Фронтенд рендерит ISO-строку, которая не совпадает с серверным логом
Две банки персиков: одна без этикетки, другая помечена
| Тип данных | Полное имя | Хранимое значение | Что происходит при SELECT |
|---|---|---|---|
timestamp | timestamp без часового пояса | сырое число микросекунд | Возвращается без изменений — Postgres не угадывает часовой пояс |
timestamptz | timestamp с часовым поясом | то же число микросекунд | Postgres применяет настройку сессии TimeZone прямо перед отдачей текста |
Аналогия
timestamp= банка персиков без этикетки происхождения. Вы знаете, что это фрукты, но не знаете, где их закатали.timestamptz= банка с гордой надписью «Произведено в UTC+8». Любой, кто её открывает, может решить, переводить ли пищевую ценность.
Под капотом: это просто большое число
2000-01-01 00:00:00 UTC → 0
2000-01-01 00:00:01 UTC → 1 000 000
- Единица: микросекунды (одна миллионная доля секунды)
- Диапазон: 4713 до н. э. – 294276 н. э. — одобрено Индианой Джонсом
- Хранилище для
timestampиtimestamptzидентично; различается только интерпретация
Демонстрация на 15 секунд
-- Клиент мыслит в шанхайском времени
SET TimeZone = 'Asia/Shanghai';
CREATE TABLE demo (
created_ts timestamp,
created_tz timestamptz
);
INSERT INTO demo VALUES ('2025-07-29 10:00', '2025-07-29 10:00');
| Запрос | Результат | Почему |
|---|---|---|
SELECT created_ts FROM demo; | 2025-07-29 10:00:00 | Сырое значение, без TZ-арифметики |
SELECT created_tz FROM demo; | 2025-07-29 10:00:00+08 | Метка применяется на выходе |
SET TimeZone = 'UTC'; затем select | 2025-07-29 02:00:00+00 | Тот же момент, новая линза |
Арифметика timestamp и интервалы
Один из самых полезных аспектов PostgreSQL timestamp — арифметика интервалов. Поскольку оба типа хранят микросекунды, можно прямо складывать и вычитать интервалы:
-- Прибавим 3 часа 30 минут
SELECT '2025-07-29 10:00'::timestamptz + INTERVAL '3 hours 30 minutes';
-- → 2025-07-29 13:30:00+08
-- Найдём разницу между двумя timestamp
SELECT '2025-07-30 09:00'::timestamptz - '2025-07-29 10:00'::timestamptz;
-- → 23:00:00 (интервал)
-- Извлечём конкретное поле
SELECT EXTRACT(EPOCH FROM '2025-07-29 10:00:00+08'::timestamptz);
-- → 1753768800 (Unix timestamp в секундах)
-- Округлим до начала дня (полезно для дневных агрегаций)
SELECT date_trunc('day', '2025-07-29 15:42:19+08'::timestamptz);
-- → 2025-07-29 00:00:00+08
Функция EXTRACT(EPOCH FROM ...) особенно полезна, когда нужно передать timestamp внешним системам, ожидающим Unix epoch в секундах. Обратное преобразование тоже простое:
SELECT to_timestamp(1753768800);
-- → 2025-07-29 10:00:00+08 (в сессии Asia/Shanghai)
Тонкий, но важный момент: арифметика интервалов с timestamp (без часового пояса) полностью игнорирует переходы DST, тогда как timestamptz их учитывает. Это значит, что прибавление INTERVAL '1 day' к значению timestamptz, пересекающему границу DST, корректно вернёт то же время по настенным часам — а не ровно через 24 часа. Подробнее о DST и работе с эпохой см. в руководстве по Unix timestamp.
Индексы и производительность
И timestamp, и timestamptz хранятся как 8-байтовые целые, поэтому разницы в производительности хранения или индексирования нет. B-tree индексы работают одинаково для обоих типов, потому что под капотом сравнение — это просто целочисленное сравнение.
Однако есть несколько практических соображений:
- Range-запросы:
WHERE created_at > '2025-07-01'эффективно работает с индексом для любого из типов. ДляtimestamptzPostgreSQL приводит литерал к UTC до сравнения, поэтому индекс по-прежнему используется. - Ключи партиционирования: при range-партиционировании по timestamp
timestamptzобычно безопаснее, так как границы партиций однозначны (всегда UTC). Дляtimestampграница вроде'2025-07-01 00:00'может означать разное в разных сессиях. - Функциональные индексы: если вы часто запрашиваете по дате (без времени), рассмотрите индекс по
date_trunc('day', created_at)для ускорения дневных агрегаций. Сами индексные страницы могут разбиваться при росте таблицы — про разбиения страниц индекса (page splits) и их влияние на запись лучше думать заранее, а не после первого инцидента.
Распространённые ловушки и быстрые решения
1. Разные пользователи — разные часы
- Причина: клиенты используют разные настройки
TimeZoneсtimestamptz - Решение: либо хранить всё как
timestampи согласовать единый пояс, либо принудительноSET TimeZone = 'UTC'при инициализации соединения
Распространённая практика — задать часовой пояс один раз при инициализации пула соединений:
-- В настройке соединения (например, в конфиге pg pool)
SET timezone = 'UTC';
Это гарантирует, что все сессии видят единое UTC-представление, а слой приложения берёт на себя перевод в локальное время для отображения.
2. Хранится «настенное время», но выбран не тот тип
- Бизнес-календарь (часы работы магазина, сроки) — берите
timestamp - Кросс-границные потоки (заказы, логи) — храните UTC в
timestamptz
Тест простой: если вопрос звучит как «в какой момент времени это произошло?» — берите timestamptz. Если «что показывают настенные часы?» — берите timestamp.
3. API, которые дрейфуют
- Всегда отправляйте
timestamptzкак ISO-8601 строки со смещением (Zили+08:00) - Дайте интерфейсу форматировать локально
4. Сравнение timestamp между типами
Смешивание timestamp и timestamptz в сравнениях или join — частый источник тонких багов:
-- Опасно: неявное приведение применяет часовой пояс сессии
SELECT * FROM orders o
JOIN schedules s ON o.created_tz = s.start_ts;
-- PostgreSQL приводит s.start_ts к timestamptz через часовой пояс сессии
-- Разные сессии могут получать разные результаты join!
Решение: всегда явно приводите типы при сравнении между ними — либо стандартизируйте один тип на домен.
5. Ловушки ORM по умолчанию
Многие ORM (Django, SQLAlchemy, ActiveRecord) по умолчанию применяют timestamp без часового пояса. Проверьте миграционные файлы — если приложение обслуживает пользователей в разных часовых поясах, переопределите умолчание на timestamptz. В Django выставьте USE_TZ = True в settings. В SQLAlchemy — DateTime(timezone=True).
Шпаргалка: что выбрать?
Только локальный календарь → timestamp
Что-то глобальное → timestamptz (хранить UTC)
- Финансовые отчёты, расписания занятий →
timestamp - Аудит-логи, e-commerce заказы →
timestamptz
Проверьте за секунды с помощью Go Tools
| Что нужно | Инструмент | Как |
|---|---|---|
| Изучить значение epoch из SQL | Конвертер epoch | Вставьте 1690622400, нажмите «Конвертировать» |
| Привести в порядок массовый JSON с временными полями | Форматировщик JSON | Скиньте payload, отформатируйте, изучите |
Все утилиты работают полностью в браузере — данные никогда не покидают машину.
Часто задаваемые вопросы
В чём разница между timestamp и timestamptz в PostgreSQL?
timestamp (без часового пояса) хранит дату-время «как есть», без контекста часового пояса. timestamptz (с часовым поясом) приводит вход к UTC при хранении и переводит обратно в часовой пояс сессии при чтении. Берите timestamptz почти всегда — это предотвращает связанные с поясами баги в распределённых системах.
Действительно ли PostgreSQL хранит часовой пояс в timestamptz?
Нет — несмотря на название, PostgreSQL не сохраняет сам часовой пояс. Он приводит вход к UTC и хранит только UTC-значение (число микросекунд от 2000-01-01). При чтении он переводит из UTC в тот часовой пояс, который задан настройкой сессии timezone. Информация об исходном часовом поясе теряется.
Как сменить часовой пояс для сессии PostgreSQL?
Запустите SET timezone = 'America/New_York';, чтобы сменить часовой пояс сессии. Это влияет на отображение и интерпретацию timestamptz значений. Для серверного дефолта пропишите timezone в postgresql.conf. Всегда применяйте имена IANA (например, Asia/Shanghai), а не аббревиатуры (вроде CST), чтобы избежать неоднозначности.
Что выбрать, timestamp или timestamptz, для хранения времени событий?
Берите timestamptz почти везде — пользовательские действия, вызовы API, аудит-логи, плановые события. Применяйте timestamp (без часового пояса) только для абстрактного времени, не привязанного к конкретному моменту, например «магазин открывается в 09:00» — это значит 9 утра в локальном часовом поясе, а не конкретный момент UTC.
Как PostgreSQL обрабатывает летнее время с timestamptz?
PostgreSQL корректно обрабатывает DST с timestamptz, потому что внутри хранит всё в UTC. При чтении значения PostgreSQL переводит из UTC по текущим правилам DST для часового пояса сессии. Это значит, что один и тот же UTC-момент корректно показывает разное локальное время до и после перехода DST.
Полное руководство по Unix timestamp — обработка точности, лучшие практики работы с часовыми поясами, примеры на JavaScript, Python и Go — см. в руководстве по Unix timestamp.
Итог
- Оба типа времени Postgres — счётчики микросекунд; вся разница только в метке
- Неправильный выбор — это путаница со временем и сломанная арифметика
- Тестируйте, конвертируйте и проверяйте здравый смысл правильными инструментами, чтобы сэкономить часы отладки