sqlpostgresqlepochdate-time

SQL EXTRACT(EPOCH FROM ...): Durations in Seconds and Unix Time

How EXTRACT(EPOCH FROM ...) turns an interval into total seconds and a timestamp into Unix time, why those two cases differ, and how to round-trip back with to_timestamp.

4 мин четенеReferencesql · postgresql · epoch · date-time · interval · unix-time
Тази статия в момента е на руски — английският превод е в процес на изготвяне.

EXTRACT(EPOCH FROM ...) в PostgreSQL извлекает из значения времени количество секунд: от interval это его полная длительность в секундах, а от метки времени — Unix-время, то есть число секунд, прошедших с 1970-01-01 00:00:00 UTC. К этой функции обращаются, когда нужно измерить, сколько длился процесс, посчитать SLA в секундах или минутах, либо превратить дату в число для хранения, сортировки и обмена с очередями, логами и внешними API.

Удобство EPOCH именно в том, что время становится обычным числом double precision: его можно делить, усреднять, складывать и сравнивать так же, как любую метрику. Дальше в статье разберём четыре практических случая — длительность между двумя метками, перевод в минуты и часы, агрегаты по группам и обратное преобразование через to_timestamp — и закончим ловушкой с timestamptz и часовыми поясами.

Два режима: интервал против метки времени

Поведение EXTRACT(EPOCH FROM ...) зависит от типа аргумента, и это ключевой момент: одно и то же выражение возвращает либо длительность, либо абсолютный момент.

  • Над interval результат — полная длительность интервала в секундах. interval '1 day' даёт 86400, потому что в сутках столько секунд.
  • Над timestamp результат — Unix-время, секунды от эпохи 1970 года. Это большое число, а не длительность.
SELECT
  EXTRACT(EPOCH FROM INTERVAL '1 day')                 AS interval_seconds, -- 86400
  EXTRACT(EPOCH FROM TIMESTAMP '2026-06-17 12:00:00')  AS unix_seconds;     -- 1781697600

Тип результата — double precision, поэтому дробные секунды (миллисекунды) не теряются. Не путайте два режима: 86400 — это длина суток в секундах, а 1781697600 — это конкретный момент 2026-06-17 12:00 UTC на оси Unix-времени. Если в одном выражении смешать длительность интервала и Unix-время метки, легко получить бессмысленное число вроде «86400 секунд плюс полтора миллиарда» и долго искать причину. Поэтому перед каждым EPOCH полезно спросить себя: я измеряю длительность или фиксирую точку во времени?

Длительность между двумя метками времени

Самый частый сценарий EXTRACT(EPOCH FROM ...) — измерить, сколько прошло между двумя событиями. Вычитание двух timestamp в PostgreSQL даёт interval, а EPOCH превращает этот интервал в секунды.

SELECT
  id,
  EXTRACT(EPOCH FROM (shipped_at - created_at)) AS seconds_to_ship
FROM orders
WHERE status = 'shipped';

Чтобы получить минуты или часы, поделите результат на 60 или 3600:

SELECT
  id,
  EXTRACT(EPOCH FROM (shipped_at - created_at)) / 3600.0 AS hours_to_ship
FROM orders
WHERE status = 'shipped';

Делите именно на 3600.0 (с точкой), а не на 3600. Сам EPOCH возвращает double precision, так что секунды до часов поделятся дробно в любом случае, но привычка писать .0 спасает в выражениях, где числитель вдруг оказался целым типом и целочисленное деление отбросило бы остаток.

Агрегаты: среднее время выполнения

Результат EPOCH — обычное число, поэтому он отлично ложится в агрегатные функции. Посчитаем среднее время доставки в часах по странам, соединив orders с users:

SELECT
  u.country,
  AVG(EXTRACT(EPOCH FROM (o.shipped_at - o.created_at))) / 3600.0 AS avg_hours
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.status = 'shipped'
GROUP BY u.country
ORDER BY avg_hours DESC;

Сначала EPOCH переводит каждую разницу в секунды, затем AVG усредняет эти числа, и только потом делим на 3600. Это надёжнее, чем усреднять сами интервалы. Тот же приём работает для медианы и процентилей через percentile_cont(0.5): считаем длительности в секундах, а перевод в часы или минуты оставляем на самый конец.

Обратно: из секунд в метку времени

Если в столбце лежит Unix-время как число, вернуть его в человекочитаемую дату поможет to_timestamp — операция, обратная EXTRACT(EPOCH FROM timestamp). Подставим то самое число 1781697600, что получили выше, и должны увидеть исходный момент:

SELECT
  to_timestamp(1781697600) AS as_timestamp; -- 2026-06-17 12:00:00+00

Круговой обход замкнулся: EXTRACT(EPOCH FROM ...) дал из 2026-06-17 12:00:00 число 1781697600, а to_timestamp вернул из него тот же момент в UTC. Полезно это и для арифметики: добавим к Unix-времени фиксированное число секунд и снова получим метку времени.

SELECT
  to_timestamp(
    EXTRACT(EPOCH FROM created_at) + 3600
  ) AS one_hour_later
FROM users
LIMIT 5;

to_timestamp принимает Unix-время и возвращает timestamptz в UTC, поэтому отображение момента дальше зависит от часового пояса сессии.

Ловушка: EPOCH над timestamptz и часовые пояса

Главная путаница с EXTRACT(EPOCH FROM ...) возникает из-за типа аргумента. От timestamptz функция всегда считает секунды от эпохи в UTC — это однозначно. А вот от «голого» timestamp (без зоны) PostgreSQL тоже трактует значение как UTC, что может разойтись с ожиданиями, если данные на самом деле хранились в локальном поясе: одна и та же запись даст разное Unix-время в зависимости от того, какой тип вы выбрали.

  • Для timestamptz результат стабилен и не зависит от TimeZone сессии.
  • В MySQL прямого EXTRACT(EPOCH ...) нет: используйте UNIX_TIMESTAMP(ts) для Unix-времени и TIMESTAMPDIFF(SECOND, a, b) для длительности.
  • В ClickHouse для Unix-времени берут toUnixTimestamp(ts), а разницу считают через dateDiff('second', a, b).
-- MySQL: duration in seconds
SELECT TIMESTAMPDIFF(SECOND, created_at, shipped_at) AS seconds_to_ship
FROM orders
WHERE status = 'shipped';

Проблемы с EPOCH почти всегда живут не в самой функции, а в граничных данных: timestamp без зоны против timestamptz с зоной, отрицательные интервалы при shipped_at раньше created_at и потеря точности, если потом округлять секунды до часов. Перед переносом запроса между PostgreSQL, MySQL и ClickHouse стоит прогнать его на маленькой таблице с NULL, нулевой длительностью и пограничной датой: на «счастливом пути» движки совпадают, а расходятся именно на таких значениях. Полезно проверить и саму функцию EXTRACT(EPOCH FROM ...), а не только итоговую цифру SLA.

Практический вывод: если число из EPOCH влияет на деньги, SLA или внешний идентификатор, не оставляйте трактовку неявной. Зафиксируйте рядом с запросом, считаете ли вы длительность интервала или Unix-время метки и от какого типа (timestamp или timestamptz) берёте секунды. И помните про производительность: EXTRACT(EPOCH FROM ts) в условии WHERE оборачивает колонку функцией и часто закрывает путь к обычному индексу по ts, поэтому для фильтров по диапазону дат сравнивайте саму колонку с границами, а EPOCH оставляйте для вычислений в SELECT.

Запомните разделение: над interval функция даёт длительность, над меткой времени — момент. Один и тот же EXTRACT(EPOCH FROM ...), но смысл числа диаметрально разный.

Упражнявай се на реални задачи

Решавай задачи в SQL тренажора с незабавно оценяване и подсказки.

Отвори тренажора