sqlpostgresqlepochdate-time

EXTRACT(EPOCH FROM ...) в PostgreSQL: как получить секунды из даты, времени и интервала

EXTRACT(EPOCH FROM ...) возвращает длительность интервала в секундах, а от метки времени — Unix-время; разбираем оба режима, деление на 60 и 3600 и обратный to_timestamp.

10 мин чтенияСправочникsql · postgresql · epoch · date-time · interval · unix-time

EXTRACT(EPOCH FROM ...) в PostgreSQL превращает время в секунды.

На первый взгляд звучит сухо: «достать количество секунд». Но на практике это одна из тех функций, которые быстро становятся любимыми, когда вы начинаете считать реальные метрики: сколько заказ ехал до клиента, сколько пользователь ждал ответа, сколько минут занял импорт файла, уложилась ли команда в SLA.

Главная идея простая:

  • если передать interval, PostgreSQL вернёт длительность в секундах;
  • если передать timestamp или timestamptz, PostgreSQL вернёт Unix-время — количество секунд от 1970-01-01 00:00:00 UTC.

Результат будет числом типа double precision. Значит, его можно делить, усреднять, сравнивать, сортировать и спокойно использовать как обычную числовую метрику.

Например, можно посчитать:

  • сколько секунд прошло между созданием и доставкой заказа;
  • сколько минут пользователь был в онлайне;
  • среднее время обработки заявки;
  • Unix-время для передачи во внешний API;
  • момент времени обратно из Unix timestamp через to_timestamp.

Разберём всё постепенно.

Что такое EPOCH

В PostgreSQL EPOCH внутри EXTRACT означает «верни значение времени в секундах».

Общий синтаксис такой:

EXTRACT(EPOCH FROM value)

Где value — это дата, время, timestamp или interval.

Например:

SELECT EXTRACT(EPOCH FROM INTERVAL '1 day') AS seconds;

Результат:

86400

В одних сутках 24 часа, в одном часе 3600 секунд, значит:

24 * 3600 = 86400

Пока всё очевидно. Но дальше начинается важное различие.

Два разных смысла: длительность и момент времени

EXTRACT(EPOCH FROM ...) работает по-разному в зависимости от того, что вы передали внутрь.

Если передать interval

PostgreSQL вернёт длительность интервала в секундах.

SELECT
  EXTRACT(EPOCH FROM INTERVAL '1 hour') AS one_hour,
  EXTRACT(EPOCH FROM INTERVAL '1 day') AS one_day,
  EXTRACT(EPOCH FROM INTERVAL '2 days 3 hours') AS two_days_three_hours;

Результат будет примерно таким:

one_hour | one_day | two_days_three_hours
---------+---------+----------------------
3600     | 86400   | 183600

Здесь число отвечает на вопрос:

«Сколько секунд длится этот промежуток?»

Если передать timestamp

PostgreSQL вернёт Unix-время.

Unix-время — это количество секунд, прошедших с полуночи 1970-01-01 00:00:00 UTC.

SELECT
  EXTRACT(EPOCH FROM TIMESTAMP '2026-06-17 12:00:00') AS unix_seconds;

Результат:

1781697600

Это уже не длительность. Это конкретная точка на временной шкале.

Такое число отвечает на другой вопрос:

«Сколько секунд прошло от начала Unix-эпохи до этого момента?»

Почему это важно не путать

Посмотрите на два числа:

SELECT
  EXTRACT(EPOCH FROM INTERVAL '1 day') AS interval_seconds,
  EXTRACT(EPOCH FROM TIMESTAMP '2026-06-17 12:00:00') AS unix_seconds;

Результат:

interval_seconds | unix_seconds
-----------------+-------------
86400            | 1781697600

Оба значения — секунды. Но смысл у них совершенно разный.

86400 — это длина промежутка.

1781697600 — это номер момента на временной оси.

Это как с деньгами: «100 рублей сдачи» и «100 рублей на банковском счёте» выглядят похожими числами, но контекст разный. С EPOCH точно так же: перед использованием всегда полезно спросить себя:

Я сейчас считаю длительность или превращаю дату в Unix timestamp?

Если вы отвечаете «длительность», обычно внутри EPOCH должен быть interval.

Если отвечаете «момент времени», внутри будет timestamp или timestamptz.

Как посчитать длительность между двумя датами

Самый частый сценарий — посчитать, сколько времени прошло между двумя событиями.

Допустим, есть таблица заказов:

CREATE TABLE orders (
  id bigint,
  user_id bigint,
  status text,
  created_at timestamp,
  shipped_at timestamp
);

В ней:

  • created_at — когда заказ создали;
  • shipped_at — когда заказ отправили;
  • status — статус заказа.

Чтобы узнать, сколько секунд прошло от создания до отправки, нужно вычесть одну дату из другой:

shipped_at - created_at

В PostgreSQL разница между двумя timestamp даёт interval.

А дальше этот interval можно превратить в секунды:

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

Логика такая:

  1. shipped_at - created_at считает промежуток времени.
  2. EXTRACT(EPOCH FROM ...) переводит этот промежуток в секунды.
  3. В результате получаем обычное число.

Например, если заказ создали в 10:00, а отправили в 12:30, разница будет 2 hours 30 minutes, а EPOCH вернёт 9000 секунд.

Как получить минуты и часы

Секунды удобны для хранения и точных расчётов, но человеку чаще приятнее видеть минуты или часы.

Чтобы получить минуты, делим секунды на 60.

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

Чтобы получить часы, делим на 3600.

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

Почему в примерах написано 60.0 и 3600.0, а не просто 60 и 3600?

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

Например, 1.5 часа гораздо информативнее, чем округлённое 1.

Пример: время обработки заявки

Возьмём более жизненный пример. Допустим, у нас есть обращения в поддержку:

CREATE TABLE support_tickets (
  id bigint,
  created_at timestamp,
  first_reply_at timestamp,
  status text
);

Нужно понять, сколько минут пользователь ждал первого ответа.

SELECT
  id,
  EXTRACT(EPOCH FROM (first_reply_at - created_at)) / 60.0 AS minutes_to_first_reply
FROM support_tickets
WHERE first_reply_at IS NOT NULL;

Здесь важно добавить условие:

WHERE first_reply_at IS NOT NULL

Если первого ответа ещё не было, разницу посчитать нельзя: NULL в арифметике даст NULL.

Можно дополнительно найти обращения, где ответ занял больше 15 минут:

SELECT
  id,
  EXTRACT(EPOCH FROM (first_reply_at - created_at)) / 60.0 AS minutes_to_first_reply
FROM support_tickets
WHERE first_reply_at IS NOT NULL
  AND EXTRACT(EPOCH FROM (first_reply_at - created_at)) > 15 * 60;

Но для больших таблиц такой фильтр стоит писать аккуратно. В конце статьи отдельно поговорим про индексы и производительность.

Средняя длительность по группам

Раз EPOCH возвращает число, его можно передавать в агрегатные функции: AVG, MIN, MAX, SUM.

Например, посчитаем среднее время доставки по странам.

Пусть есть таблицы:

CREATE TABLE users (
  id bigint,
  country text
);

CREATE TABLE orders (
  id bigint,
  user_id bigint,
  status text,
  created_at timestamp,
  shipped_at timestamp
);

Запрос:

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

Что здесь происходит:

  1. JOIN соединяет заказы с пользователями.
  2. WHERE o.status = 'shipped' оставляет только отправленные заказы.
  3. o.shipped_at - o.created_at считает длительность доставки для каждого заказа.
  4. EXTRACT(EPOCH FROM ...) переводит длительность в секунды.
  5. AVG(...) считает среднее значение в секундах.
  6. Деление на 3600.0 переводит секунды в часы.

Такой запрос можно читать почти как фразу:

«Покажи среднее время от создания до отправки заказа в часах по каждой стране».

Почему удобно агрегировать секунды, а не интервалы

PostgreSQL умеет работать с interval, но для аналитики часто удобнее переводить длительности в секунды.

С числами проще:

  • сортировать;
  • округлять;
  • строить графики;
  • считать процентили;
  • передавать данные в BI-инструменты;
  • сравнивать с лимитами SLA.

Например, среднее время ответа в минутах:

SELECT
  AVG(EXTRACT(EPOCH FROM (first_reply_at - created_at))) / 60.0 AS avg_reply_minutes
FROM support_tickets
WHERE first_reply_at IS NOT NULL;

Максимальное время ответа:

SELECT
  MAX(EXTRACT(EPOCH FROM (first_reply_at - created_at))) / 60.0 AS max_reply_minutes
FROM support_tickets
WHERE first_reply_at IS NOT NULL;

Минимальное время ответа:

SELECT
  MIN(EXTRACT(EPOCH FROM (first_reply_at - created_at))) / 60.0 AS min_reply_minutes
FROM support_tickets
WHERE first_reply_at IS NOT NULL;

Для отчёта это обычно понятнее, чем показывать пользователю interval.

Медиана и процентили

Среднее значение иногда обманывает.

Представьте, что у поддержки обычно ответ за 5 минут, но один тикет случайно провисел 2 дня. Среднее резко вырастет и создаст впечатление, будто всё плохо у всех.

Для таких случаев полезны медиана и процентили.

Медиана — это значение посередине: половина обращений быстрее, половина медленнее.

В PostgreSQL её можно посчитать через percentile_cont(0.5):

SELECT
  percentile_cont(0.5) WITHIN GROUP (
    ORDER BY EXTRACT(EPOCH FROM (first_reply_at - created_at))
  ) / 60.0 AS median_reply_minutes
FROM support_tickets
WHERE first_reply_at IS NOT NULL;

А 95-й процентиль покажет время, в которое уложились 95% обращений:

SELECT
  percentile_cont(0.95) WITHIN GROUP (
    ORDER BY EXTRACT(EPOCH FROM (first_reply_at - created_at))
  ) / 60.0 AS p95_reply_minutes
FROM support_tickets
WHERE first_reply_at IS NOT NULL;

Такой показатель часто используют для SLA.

Например:

95% пользователей получили первый ответ быстрее чем за 12 минут.

Это намного полезнее, чем просто «среднее время ответа — 8 минут».

Как округлить результат

EXTRACT(EPOCH FROM ...) возвращает дробное число. Иногда это хорошо: можно видеть миллисекунды. Но в отчётах чаще хочется округлить результат.

Например, округлим часы доставки до двух знаков:

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

Почему здесь есть ::numeric?

В PostgreSQL ROUND с двумя аргументами удобнее всего работает с numeric. Поэтому мы сначала делим секунды на часы, затем приводим результат к numeric, а потом округляем до двух знаков.

Если нужна целая минута, можно использовать ROUND без второго аргумента:

SELECT
  id,
  ROUND(
    (EXTRACT(EPOCH FROM (shipped_at - created_at)) / 60.0)::numeric
  ) AS minutes_to_ship
FROM orders
WHERE status = 'shipped';

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

Иногда Unix-время уже хранится в таблице как число.

Например, внешний сервис прислал событие:

1781697600

Чтобы превратить это число обратно в дату и время, в PostgreSQL есть функция to_timestamp.

SELECT
  to_timestamp(1781697600) AS event_time;

Результат будет меткой времени с часовым поясом:

2026-06-17 12:00:00+00

То есть to_timestamp делает обратную операцию:

SELECT
  to_timestamp(
    EXTRACT(EPOCH FROM TIMESTAMP '2026-06-17 12:00:00')
  ) AS restored_time;

Смысл такой:

  1. Берём дату.
  2. Превращаем её в Unix-время.
  3. Превращаем Unix-время обратно в дату.

Добавить час к Unix-времени

Так как Unix-время — обычное число секунд, к нему можно прибавлять секунды.

Например, добавим один час к created_at:

SELECT
  id,
  created_at,
  to_timestamp(
    EXTRACT(EPOCH FROM created_at) + 3600
  ) AS one_hour_later
FROM orders
LIMIT 5;

3600 секунд — это один час.

Но в реальных запросах чаще проще писать так:

SELECT
  id,
  created_at,
  created_at + INTERVAL '1 hour' AS one_hour_later
FROM orders
LIMIT 5;

Первый вариант полезен, когда вы работаете именно с Unix-временем как с числом. Второй — когда вы просто хотите прибавить интервал к дате внутри PostgreSQL.

timestamp и timestamptz: важная разница

Самая частая путаница с EPOCH начинается с типов времени.

В PostgreSQL есть два похожих, но разных типа:

  • timestamp — дата и время без часового пояса;
  • timestamptz — дата и время с часовым поясом.

Название timestamptz иногда сбивает с толку. Он не хранит «часовой пояс пользователя» в каждой строке. Он хранит конкретный момент времени, а PostgreSQL показывает его с учётом TimeZone текущей сессии.

timestamptz

Для timestamptz EXTRACT(EPOCH FROM ...) считает секунды от Unix-эпохи в UTC. Это стабильный и однозначный вариант.

Например, если два пользователя смотрят на один и тот же момент из разных часовых поясов, отображение может отличаться, но Unix-время будет описывать один и тот же момент.

SELECT
  EXTRACT(EPOCH FROM TIMESTAMPTZ '2026-06-17 12:00:00+00') AS unix_seconds;

timestamp

timestamp без часового пояса — это просто дата и время «как написано».

SELECT
  EXTRACT(EPOCH FROM TIMESTAMP '2026-06-17 12:00:00') AS unix_seconds;

PostgreSQL посчитает для этого значения номинальное количество секунд от 1970-01-01 00:00:00, не применяя правила часового пояса как к timestamptz.

Проблема возникает, когда в timestamp без зоны на самом деле хранили локальное время.

Например, команда договорилась: «все даты в базе лежат по Москве», но тип выбрала timestamp, а не timestamptz. Через полгода кто-то начнёт считать Unix-время, передавать его в API, сравнивать с логами в UTC — и появится сдвиг на несколько часов.

Поэтому правило простое:

Если значение описывает реальный момент времени, чаще всего лучше использовать timestamptz.

А timestamp без зоны хорош там, где часовой пояс действительно не нужен: например, «магазин открывается каждый день в 10:00 по местному времени».

Как to_timestamp зависит от часового пояса сессии

to_timestamp принимает Unix-время и возвращает timestamptz.

То есть результат — это конкретный момент времени. Но отображаться он будет в часовом поясе текущей сессии.

Посмотреть текущий часовой пояс можно так:

SHOW TimeZone;

Поменять для сессии:

SET TimeZone = 'UTC';

Или, например:

SET TimeZone = 'Europe/Moscow';

Один и тот же Unix timestamp может отображаться разными строками времени, но момент останется тем же.

Это похоже на авиабилет: вылет один и тот же, но пассажир в Москве, Лондоне и Токио может видеть время относительно своего часового пояса.

NULL: что будет, если даты нет

Если внутри выражения есть NULL, результат тоже будет NULL.

SELECT
  EXTRACT(EPOCH FROM (NULL::timestamp - TIMESTAMP '2026-06-17 12:00:00')) AS seconds_diff;

Результат:

NULL

Это нормальное поведение SQL.

Например, если заказ ещё не доставлен, shipped_at может быть пустым. Тогда длительность доставки пока неизвестна.

SELECT
  id,
  EXTRACT(EPOCH FROM (shipped_at - created_at)) AS seconds_to_ship
FROM orders;

Для недоставленных заказов seconds_to_ship будет NULL.

Если нужны только завершённые заказы, добавьте фильтр:

SELECT
  id,
  EXTRACT(EPOCH FROM (shipped_at - created_at)) AS seconds_to_ship
FROM orders
WHERE shipped_at IS NOT NULL;

Отрицательные интервалы

Иногда результат может быть отрицательным.

SELECT
  EXTRACT(
    EPOCH FROM (
      TIMESTAMP '2026-06-17 10:00:00' - TIMESTAMP '2026-06-17 12:00:00'
    )
  ) AS diff_seconds;

Результат:

-7200

Это значит, что первая дата раньше второй на 2 часа.

В бизнес-данных отрицательная длительность часто говорит о проблеме:

  • даты перепутали местами;
  • событие записалось раньше создания сущности;
  • данные приехали из разных систем с разными часовыми поясами;
  • в тестовых данных случайно поставили неправильный timestamp.

Например, заказ не должен быть отправлен раньше создания:

SELECT
  id,
  created_at,
  shipped_at,
  EXTRACT(EPOCH FROM (shipped_at - created_at)) AS seconds_to_ship
FROM orders
WHERE shipped_at < created_at;

Такой запрос полезен как проверка качества данных.

Фильтры и индексы: не оборачивайте колонку без необходимости

Важный момент для производительности: если в WHERE обернуть колонку функцией, обычный индекс по этой колонке может не помочь.

Например, такой фильтр выглядит удобно:

SELECT
  id,
  created_at
FROM orders
WHERE EXTRACT(EPOCH FROM created_at) >= 1781697600;

Но для большой таблицы это может быть плохой идеей. PostgreSQL придётся вычислять EXTRACT(EPOCH FROM created_at) для строк, а обычный индекс по created_at может оказаться бесполезным.

Лучше сравнивать саму колонку с датой:

SELECT
  id,
  created_at
FROM orders
WHERE created_at >= TIMESTAMP '2026-06-17 12:00:00';

Так запрос понятнее и дружелюбнее к индексу по created_at.

Хорошее практическое правило:

В WHERE фильтруйте по исходной дате, а EXTRACT(EPOCH FROM ...) используйте в SELECT, когда нужно показать или посчитать число секунд.

PostgreSQL, MySQL и ClickHouse: как написать похожие запросы

В PostgreSQL для секунд удобно использовать EXTRACT(EPOCH FROM ...).

Но в других базах синтаксис другой.

MySQL

В MySQL нет такого же EXTRACT(EPOCH FROM ...).

Для Unix-времени используется UNIX_TIMESTAMP:

SELECT
  UNIX_TIMESTAMP(created_at) AS unix_seconds
FROM orders;

Для разницы между двумя датами в секундах используется TIMESTAMPDIFF:

SELECT
  TIMESTAMPDIFF(SECOND, created_at, shipped_at) AS seconds_to_ship
FROM orders
WHERE status = 'shipped';

ClickHouse

В ClickHouse Unix-время можно получить через toUnixTimestamp:

SELECT
  toUnixTimestamp(created_at) AS unix_seconds
FROM orders;

А разницу в секундах — через dateDiff:

SELECT
  dateDiff('second', created_at, shipped_at) AS seconds_to_ship
FROM orders
WHERE status = 'shipped';

Идея везде одинаковая: либо получаем Unix timestamp, либо считаем длительность между двумя моментами. Отличается только синтаксис.

Мини-памятка

EXTRACT(EPOCH FROM interval) возвращает длительность:

SELECT EXTRACT(EPOCH FROM INTERVAL '2 hours') AS seconds;

Результат:

7200

EXTRACT(EPOCH FROM timestamp) возвращает Unix-время:

SELECT EXTRACT(EPOCH FROM TIMESTAMP '2026-06-17 12:00:00') AS unix_seconds;

Результат:

1781697600

Разница между двумя timestamp даёт interval:

SELECT
  EXTRACT(EPOCH FROM (shipped_at - created_at)) AS seconds_to_ship
FROM orders;

Минуты:

SELECT
  EXTRACT(EPOCH FROM (shipped_at - created_at)) / 60.0 AS minutes_to_ship
FROM orders;

Часы:

SELECT
  EXTRACT(EPOCH FROM (shipped_at - created_at)) / 3600.0 AS hours_to_ship
FROM orders;

Обратно из Unix timestamp в дату:

SELECT
  to_timestamp(1781697600) AS event_time;

Главное

EXTRACT(EPOCH FROM ...) — это удобный способ превратить время в секунды.

Но важно помнить главный водораздел:

  • от interval вы получаете длительность;
  • от timestamp или timestamptz вы получаете Unix-время, то есть конкретный момент на временной шкале.

Для аналитики чаще всего используется первый сценарий: вычитаете две даты, получаете interval, переводите его в секунды и дальше считаете минуты, часы, средние значения, медианы и процентили.

Для интеграций и логов часто нужен второй сценарий: превратить дату в Unix timestamp или восстановить дату из числа через to_timestamp.

Если результат влияет на SLA, деньги, отчёты или обмен с внешней системой, не оставляйте смысл числа «на догадку». Сразу фиксируйте, что именно вы считаете: длительность или момент времени. И отдельно следите за типами timestamp и timestamptz, потому что часовые пояса — это то место, где хорошие запросы часто начинают вести себя неожиданно.

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр