Skip to content
Назад к блогу
Руководства

Что на самом деле хранится в колонке timestamp PostgreSQL

Простое руководство по тому, как PostgreSQL хранит timestamp и timestamptz, почему часовые пояса кусаются и как выбрать правильный тип для задачи.

6 мин чтения

PostgreSQL timestamp против timestamptz: что реально лежит под капотом?

PostgreSQL хранит и timestamp, и timestamptz как одно 64-битное целое: количество микросекунд с 1970-01-01 00:00:00 UTC. Различие проявляется только при форматировании данных для человека.

Почему здесь все спотыкаются?

  • Две колонки, одна дата… два разных результата запроса
  • Ваше приложение вставляет 2025-07-29 10:00, а другая команда видит 02:00
  • Фронтенд рендерит ISO-строку, которая не совпадает с серверным логом

Две банки персиков: одна без этикетки, другая помечена

Тип данныхПолное имяХранимое значениеЧто происходит при SELECT
timestamptimestamp без часового поясасырое число микросекундВозвращается без изменений — Postgres не угадывает часовой пояс
timestamptztimestamp с часовым поясомто же число микросекунд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'; затем select2025-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' эффективно работает с индексом для любого из типов. Для timestamptz PostgreSQL приводит литерал к 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 — счётчики микросекунд; вся разница только в метке
  • Неправильный выбор — это путаница со временем и сломанная арифметика
  • Тестируйте, конвертируйте и проверяйте здравый смысл правильными инструментами, чтобы сэкономить часы отладки

Похожие статьи

Все статьи