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';
Логика такая:
shipped_at - created_at считает промежуток времени.
EXTRACT(EPOCH FROM ...) переводит этот промежуток в секунды.
- В результате получаем обычное число.
Например, если заказ создали в 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;
Что здесь происходит:
JOIN соединяет заказы с пользователями.
WHERE o.status = 'shipped' оставляет только отправленные заказы.
o.shipped_at - o.created_at считает длительность доставки для каждого заказа.
EXTRACT(EPOCH FROM ...) переводит длительность в секунды.
AVG(...) считает среднее значение в секундах.
- Деление на
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;
Смысл такой:
- Берём дату.
- Превращаем её в Unix-время.
- Превращаем 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, потому что часовые пояса — это то место, где хорошие запросы часто начинают вести себя неожиданно.
EXTRACT(EPOCH FROM ...)в PostgreSQL превращает время в секунды.На первый взгляд звучит сухо: «достать количество секунд». Но на практике это одна из тех функций, которые быстро становятся любимыми, когда вы начинаете считать реальные метрики: сколько заказ ехал до клиента, сколько пользователь ждал ответа, сколько минут занял импорт файла, уложилась ли команда в SLA.
Главная идея простая:
interval, PostgreSQL вернёт длительность в секундах;timestampилиtimestamptz, PostgreSQL вернёт Unix-время — количество секунд от1970-01-01 00:00:00 UTC.Результат будет числом типа
double precision. Значит, его можно делить, усреднять, сравнивать, сортировать и спокойно использовать как обычную числовую метрику.Например, можно посчитать:
to_timestamp.Разберём всё постепенно.
Что такое
EPOCHВ PostgreSQL
EPOCHвнутриEXTRACTозначает «верни значение времени в секундах».Общий синтаксис такой:
EXTRACT(EPOCH FROM value)Где
value— это дата, время, timestamp или interval.Например:
SELECT EXTRACT(EPOCH FROM INTERVAL '1 day') AS seconds;Результат:
В одних сутках 24 часа, в одном часе 3600 секунд, значит:
Пока всё очевидно. Но дальше начинается важное различие.
Два разных смысла: длительность и момент времени
EXTRACT(EPOCH FROM ...)работает по-разному в зависимости от того, что вы передали внутрь.Если передать
intervalPostgreSQL вернёт длительность интервала в секундах.
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;Результат будет примерно таким:
Здесь число отвечает на вопрос:
Если передать
timestampPostgreSQL вернёт Unix-время.
Unix-время — это количество секунд, прошедших с полуночи
1970-01-01 00:00:00 UTC.SELECT EXTRACT(EPOCH FROM TIMESTAMP '2026-06-17 12:00:00') AS unix_seconds;Результат:
Это уже не длительность. Это конкретная точка на временной шкале.
Такое число отвечает на другой вопрос:
Почему это важно не путать
Посмотрите на два числа:
SELECT EXTRACT(EPOCH FROM INTERVAL '1 day') AS interval_seconds, EXTRACT(EPOCH FROM TIMESTAMP '2026-06-17 12:00:00') AS unix_seconds;Результат:
Оба значения — секунды. Но смысл у них совершенно разный.
86400— это длина промежутка.1781697600— это номер момента на временной оси.Это как с деньгами: «100 рублей сдачи» и «100 рублей на банковском счёте» выглядят похожими числами, но контекст разный. С
EPOCHточно так же: перед использованием всегда полезно спросить себя:Если вы отвечаете «длительность», обычно внутри
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';Логика такая:
shipped_at - created_atсчитает промежуток времени.EXTRACT(EPOCH FROM ...)переводит этот промежуток в секунды.Например, если заказ создали в
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;Что здесь происходит:
JOINсоединяет заказы с пользователями.WHERE o.status = 'shipped'оставляет только отправленные заказы.o.shipped_at - o.created_atсчитает длительность доставки для каждого заказа.EXTRACT(EPOCH FROM ...)переводит длительность в секунды.AVG(...)считает среднее значение в секундах.3600.0переводит секунды в часы.Такой запрос можно читать почти как фразу:
Почему удобно агрегировать секунды, а не интервалы
PostgreSQL умеет работать с
interval, но для аналитики часто удобнее переводить длительности в секунды.С числами проще:
Например, среднее время ответа в минутах:
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.
Например:
Это намного полезнее, чем просто «среднее время ответа — 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-время уже хранится в таблице как число.
Например, внешний сервис прислал событие:
Чтобы превратить это число обратно в дату и время, в PostgreSQL есть функция
to_timestamp.SELECT to_timestamp(1781697600) AS event_time;Результат будет меткой времени с часовым поясом:
То есть
to_timestampделает обратную операцию:SELECT to_timestamp( EXTRACT(EPOCH FROM TIMESTAMP '2026-06-17 12:00:00') ) AS restored_time;Смысл такой:
Добавить час к 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Для
timestamptzEXTRACT(EPOCH FROM ...)считает секунды от Unix-эпохи в UTC. Это стабильный и однозначный вариант.Например, если два пользователя смотрят на один и тот же момент из разных часовых поясов, отображение может отличаться, но Unix-время будет описывать один и тот же момент.
SELECT EXTRACT(EPOCH FROM TIMESTAMPTZ '2026-06-17 12:00:00+00') AS unix_seconds;timestamptimestampбез часового пояса — это просто дата и время «как написано».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 — и появится сдвиг на несколько часов.Поэтому правило простое:
А
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;Результат:
Это нормальное поведение 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;Результат:
Это значит, что первая дата раньше второй на 2 часа.
В бизнес-данных отрицательная длительность часто говорит о проблеме:
Например, заказ не должен быть отправлен раньше создания:
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.Хорошее практическое правило:
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;Результат:
EXTRACT(EPOCH FROM timestamp)возвращает Unix-время:SELECT EXTRACT(EPOCH FROM TIMESTAMP '2026-06-17 12:00:00') AS unix_seconds;Результат:
Разница между двумя
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, потому что часовые пояса — это то место, где хорошие запросы часто начинают вести себя неожиданно.