Este artículo está actualmente en ruso — la traducción está en curso.
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,
EXTRACT(EPOCH FROM TIMESTAMP '2026-06-17 12:00:00') AS unix_seconds;
Тип результата — 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;
Круговой обход замкнулся: 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).
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 ...), но смысл числа диаметрально разный.
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сессии.EXTRACT(EPOCH ...)нет: используйтеUNIX_TIMESTAMP(ts)для Unix-времени иTIMESTAMPDIFF(SECOND, a, b)для длительности.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 ...), но смысл числа диаметрально разный.