sqlpostgresqldate-partextract

DATE_PART в PostgreSQL: как достать час, день недели, неделю или секунды из даты

DATE_PART достает год, месяц, день недели, epoch и другие поля из timestamp или interval, но требует внимания к типам и нумерации.

10 мин чтенияСправочникsql · postgresql · date-part · extract · date-functions · mysql

DATE_PART в PostgreSQL достаёт из даты или времени одну конкретную часть и возвращает её числом.

Например, у вас есть дата заказа: 2026-06-17 14:32:09. Из неё можно отдельно получить:

  • час — 14;
  • день недели;
  • день года;
  • номер недели;
  • секунды от начала Unix-эпохи;
  • месяц, год, минуту, секунду и другие части.

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

Простая идея: дата как коробка с деталями

Представьте дату как коробку, внутри которой лежат разные детали:

2026-06-17 14:32:09

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

SELECT DATE_PART('hour', TIMESTAMP '2026-06-17 14:32:09') AS hour_of_day;

Результат:

14

Синтаксис такой:

DATE_PART('field', date_or_timestamp)

Где:

  • 'field' — какую часть даты нужно достать;
  • date_or_timestamp — значение типа date, timestamp, timestamptz или interval.

Например:

SELECT
  DATE_PART('hour', TIMESTAMP '2026-06-17 14:32:09') AS hour_of_day,
  DATE_PART('dow', TIMESTAMP '2026-06-17 14:32:09') AS day_of_week,
  DATE_PART('doy', TIMESTAMP '2026-06-17 14:32:09') AS day_of_year,
  DATE_PART('week', TIMESTAMP '2026-06-17 14:32:09') AS iso_week;

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

hour_of_day | day_of_week | day_of_year | iso_week
------------+-------------+-------------+---------
14          | 3           | 168         | 25

То есть PostgreSQL говорит:

  • событие произошло в 14-й час;
  • это третий день недели в нумерации dow;
  • это 168-й день года;
  • это 25-я ISO-неделя.

Что возвращает DATE_PART

Важно запомнить: DATE_PART всегда возвращает число типа double precision.

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

SELECT
  DATE_PART('year', DATE '2026-06-17') AS year_num,
  DATE_PART('month', DATE '2026-06-17') AS month_num,
  DATE_PART('day', DATE '2026-06-17') AS day_num;

Результат:

year_num | month_num | day_num
---------+-----------+--------
2026     | 6         | 17

На экране это выглядит как обычные числа, но тип результата — double precision. В большинстве отчётов это не мешает, но иногда важно при сравнении типов, округлении или передаче результата в другие выражения.

Полезные поля для DATE_PART

У DATE_PART есть много полей. Новичку чаще всего нужны такие:

Поле Что достаёт
'year' год
'month' месяц
'day' день месяца
'hour' час
'minute' минуту
'second' секунду, включая дробную часть
'dow' день недели, где воскресенье — 0
'isodow' день недели по ISO, где понедельник — 1, воскресенье — 7
'doy' день года от 1 до 366
'week' номер недели по ISO
'epoch' секунды Unix-эпохи или длительность интервала в секундах

Пример с разными частями даты:

SELECT
  DATE_PART('year', created_at) AS year_num,
  DATE_PART('month', created_at) AS month_num,
  DATE_PART('hour', created_at) AS hour_num
FROM orders;

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

Пример: заказы по часам

Один из самых понятных сценариев — почасовой отчёт.

Допустим, в таблице orders есть колонка created_at, где хранится время создания заказа. Нужно понять, в какие часы покупатели чаще всего платят.

SELECT
  DATE_PART('hour', created_at) AS hour_of_day,
  COUNT(*) AS orders_count
FROM orders
WHERE status = 'paid'
GROUP BY DATE_PART('hour', created_at)
ORDER BY hour_of_day;

Запрос делает три вещи:

  1. Берёт из created_at только час.
  2. Считает количество оплаченных заказов в каждом часе.
  3. Сортирует результат от 0 до 23.

Результат может выглядеть так:

hour_of_day | orders_count
------------+-------------
9           | 18
10          | 25
11          | 31
12          | 44

Такой отчёт помогает увидеть привычки пользователей: например, что больше всего заказов приходит в обед или вечером после работы.

DATE_PART и EXTRACT: в чём разница

В PostgreSQL есть ещё один похожий инструмент — EXTRACT.

Эти два выражения делают одно и то же:

SELECT EXTRACT(hour FROM created_at) AS hour_of_day
FROM orders;
SELECT DATE_PART('hour', created_at) AS hour_of_day
FROM orders;

Главная разница — в синтаксисе.

EXTRACT выглядит как специальная SQL-конструкция со словом FROM, а DATE_PART выглядит как обычная функция.

У DATE_PART есть удобный практический плюс: имя поля передаётся строкой. Поэтому его проще подставлять динамически в приложении, если пользователь выбирает группировку сам: по часу, по дню, по месяцу или по неделе.

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

SELECT
  DATE_PART('hour', created_at) AS period_num,
  COUNT(*) AS orders_count
FROM orders
GROUP BY DATE_PART('hour', created_at)
ORDER BY period_num;

А завтра можно заменить 'hour' на 'month' и получить группировку по месяцам.

Есть и тонкость с типами: в PostgreSQL 14 и новее EXTRACT возвращает numeric, а DATE_PART по-прежнему возвращает double precision. В обычных отчётах это редко заметно, но при строгих проверках типов или работе с дробными секундами разница может всплыть.

День недели: dow и isodow

Самая частая ловушка в DATE_PART — день недели.

В PostgreSQL есть два похожих поля:

  • 'dow';
  • 'isodow'.

Они оба возвращают день недели, но нумеруют дни по-разному.

Для 'dow' неделя начинается с воскресенья:

День Значение
Воскресенье 0
Понедельник 1
Вторник 2
Среда 3
Четверг 4
Пятница 5
Суббота 6

Для 'isodow' используется ISO-нумерация:

День Значение
Понедельник 1
Вторник 2
Среда 3
Четверг 4
Пятница 5
Суббота 6
Воскресенье 7

Проверим на воскресенье:

SELECT
  DATE_PART('dow', DATE '2026-06-21') AS dow_num,
  DATE_PART('isodow', DATE '2026-06-21') AS isodow_num;

Результат:

dow_num | isodow_num
--------+-----------
0       | 7

Дата одна и та же, но числа разные.

Для отчётов на русском и в европейской логике чаще удобнее isodow, потому что неделя начинается с понедельника.

Пример: регистрации по дням недели

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

SELECT
  DATE_PART('isodow', created_at) AS weekday_num,
  TO_CHAR(created_at, 'Dy') AS weekday_name,
  COUNT(*) AS signups_count
FROM users
GROUP BY
  DATE_PART('isodow', created_at),
  TO_CHAR(created_at, 'Dy')
ORDER BY weekday_num;

Здесь мы достаём номер дня недели через DATE_PART, а короткое название дня получаем через TO_CHAR.

Результат может быть таким:

weekday_num | weekday_name | signups_count
------------+--------------+--------------
1           | Mon          | 120
2           | Tue          | 135
3           | Wed          | 128
4           | Thu          | 141
5           | Fri          | 160
6           | Sat          | 98
7           | Sun          | 87

Почему мы сортируем именно по weekday_num, а не по weekday_name?

Потому что названия дней — это текст. Если сортировать по тексту, порядок может стать алфавитным, а не календарным. Номер дня недели даёт правильный порядок: понедельник, вторник, среда и так далее.

Как правильно фильтровать выходные

Если вы используете 'dow', выходные — это воскресенье и суббота, то есть значения 0 и 6:

SELECT *
FROM orders
WHERE DATE_PART('dow', created_at) IN (0, 6);

Если вы используете 'isodow', выходные — это суббота и воскресенье, то есть значения 6 и 7:

SELECT *
FROM orders
WHERE DATE_PART('isodow', created_at) IN (6, 7);

Опасность в том, что эти два варианта легко перепутать.

Например, если заменить 'dow' на 'isodow', но оставить условие IN (0, 6), запрос начнёт работать неправильно. Он будет находить субботу, но потеряет воскресенье, потому что в isodow воскресенье — это 7, а не 0.

Поэтому хорошее правило такое: если пишете условие по дням недели, сразу проверьте, какую именно нумерацию вы используете.

Номер недели: поле week

Поле 'week' возвращает номер недели по ISO.

SELECT
  DATE_PART('week', DATE '2026-06-17') AS week_num;

Результат:

week_num
--------
25

Это удобно для отчётов вида «продажи по неделям»:

SELECT
  DATE_PART('year', created_at) AS year_num,
  DATE_PART('week', created_at) AS week_num,
  COUNT(*) AS orders_count
FROM orders
WHERE status = 'paid'
GROUP BY
  DATE_PART('year', created_at),
  DATE_PART('week', created_at)
ORDER BY year_num, week_num;

Но с неделями есть важная тонкость: ISO-неделя не всегда совпадает с обычным календарным годом.

Например, первые дни января иногда относятся к последней ISO-неделе прошлого года, а последние дни декабря — к первой ISO-неделе следующего ISO-года. Для простых учебных отчётов это обычно не критично, но в настоящей финансовой аналитике лучше внимательно проверять правила календаря.

Если нужен именно аккуратный недельный период, часто удобнее группировать не по номеру недели, а по началу недели через DATE_TRUNC.

SELECT
  DATE_TRUNC('week', created_at) AS week_start,
  COUNT(*) AS orders_count
FROM orders
WHERE status = 'paid'
GROUP BY DATE_TRUNC('week', created_at)
ORDER BY week_start;

DATE_PART('week', created_at) отвечает на вопрос «какой номер недели?», а DATE_TRUNC('week', created_at) отвечает на вопрос «к какой неделе относится дата?».

Epoch: секунды от Unix-эпохи

Поле 'epoch' возвращает количество секунд.

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

SELECT
  DATE_PART('epoch', TIMESTAMP '2026-06-17 14:32:09') AS epoch_seconds;

Результат будет большим числом:

epoch_seconds
-------------
1781706729

Чаще всего 'epoch' используют не ради красивого вывода, а ради расчётов.

Например, можно посчитать возраст аккаунта в днях:

SELECT
  id,
  DATE_PART('epoch', NOW() - created_at) / 86400 AS account_age_days
FROM users
ORDER BY account_age_days DESC
LIMIT 10;

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

  1. NOW() - created_at даёт интервал между текущим моментом и датой регистрации.
  2. DATE_PART('epoch', ...) превращает этот интервал в секунды.
  3. Деление на 86400 переводит секунды в дни.

Почему 86400? Потому что в сутках 24 часа, в часе 3600 секунд, значит 24 × 3600 = 86400.

DATE_PART с interval

DATE_PART можно применять не только к датам, но и к интервалам.

Например:

SELECT
  DATE_PART('hour', INTERVAL '2 days 5 hours') AS hour_part,
  DATE_PART('epoch', INTERVAL '2 days 5 hours') AS total_seconds;

Результат:

hour_part | total_seconds
----------+--------------
5         | 190800

Вот здесь очень важный момент.

DATE_PART('hour', interval) возвращает только часовую часть интервала. В примере это 5 часов.

А DATE_PART('epoch', interval) возвращает всю длительность интервала в секундах. Два дня и пять часов превращаются в полное количество секунд.

Поэтому для расчёта длительности обычно лучше использовать 'epoch'.

Например, среднее время между созданием и оплатой заказа в минутах:

SELECT
  AVG(DATE_PART('epoch', paid_at - created_at) / 60) AS avg_payment_minutes
FROM orders
WHERE paid_at IS NOT NULL;

Такой запрос считает полную длительность, а не только минутную или часовую часть.

Пример: средний возраст заказа по отделам

Представим, что есть таблица заказов orders, таблица пользователей users и таблица сотрудников employees. Нужно понять, в каких отделах находятся самые старые необработанные заказы.

SELECT
  e.dept,
  AVG(DATE_PART('epoch', NOW() - o.created_at) / 3600) AS avg_order_age_hours
FROM orders o
JOIN users u ON u.id = o.user_id
JOIN employees e ON e.name = u.name
WHERE o.status = 'pending'
GROUP BY e.dept
ORDER BY avg_order_age_hours DESC;

Запрос делает следующее:

  1. Берёт только заказы в статусе pending.
  2. Считает возраст каждого заказа в часах.
  3. Группирует заказы по отделам.
  4. Показывает средний возраст заказа в каждом отделе.

Деление на 3600 переводит секунды в часы.

Такой приём часто встречается в аналитике: сначала превращаем дату в число, потом спокойно считаем среднее, максимум, минимум или медиану.

Важная ловушка: DATE_PART в WHERE и индексы

Допустим, в таблице orders миллионы строк, а на колонке created_at есть обычный индекс.

Хочется написать так:

SELECT *
FROM orders
WHERE DATE_PART('year', created_at) = 2026;

Запрос выглядит понятно: найти заказы за 2026 год.

Но для большой таблицы это может быть плохой вариант. Почему?

Потому что колонка created_at обёрнута в функцию. PostgreSQL уже не может просто взять обычный индекс по created_at и быстро найти диапазон дат. Ему приходится вычислять DATE_PART для большого количества строк.

Чаще лучше писать фильтр диапазоном:

SELECT *
FROM orders
WHERE created_at >= DATE '2026-01-01'
  AND created_at < DATE '2027-01-01';

Этот вариант обычно лучше дружит с индексом по created_at.

Идея простая:

  • для группировки в отчётах DATE_PART часто нормален;
  • для фильтрации больших таблиц по дате лучше использовать диапазоны;
  • если фильтр через DATE_PART действительно нужен постоянно, можно подумать об индексе по выражению.

Например:

CREATE INDEX orders_created_year_idx
ON orders ((DATE_PART('year', created_at)));

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

MySQL: чем заменить DATE_PART

DATE_PART — это не универсальное имя для всех баз данных. В MySQL такой функции нет.

В MySQL можно использовать EXTRACT:

SELECT
  EXTRACT(HOUR FROM created_at) AS hour_of_day,
  COUNT(*) AS orders_count
FROM orders
GROUP BY EXTRACT(HOUR FROM created_at);

Или отдельные функции:

SELECT
  HOUR(created_at) AS hour_of_day,
  DAYOFWEEK(created_at) AS dow_sun_is_1,
  DAYOFYEAR(created_at) AS day_of_year,
  COUNT(*) AS orders_count
FROM orders
GROUP BY
  HOUR(created_at),
  DAYOFWEEK(created_at),
  DAYOFYEAR(created_at);

Здесь тоже есть ловушка с днями недели.

В MySQL DAYOFWEEK() возвращает 1 для воскресенья, 2 для понедельника и так далее.

То есть это не то же самое, что PostgreSQL DATE_PART('dow', ...), где воскресенье — 0.

ClickHouse: похожие функции

В ClickHouse обычно используют отдельные функции:

SELECT
  toHour(created_at) AS hour_of_day,
  toDayOfWeek(created_at) AS dow_mon_is_1,
  toDayOfYear(created_at) AS day_of_year,
  toUnixTimestamp(created_at) AS epoch_seconds,
  count() AS orders_count
FROM orders
GROUP BY
  hour_of_day,
  dow_mon_is_1,
  day_of_year,
  epoch_seconds;

В ClickHouse toDayOfWeek обычно начинает неделю с понедельника, где понедельник — 1.

Поэтому при переносе запросов между PostgreSQL, MySQL и ClickHouse нельзя просто заменить функцию и надеяться, что всё совпадёт.

Особенно внимательно проверяйте:

  • как нумеруются дни недели;
  • какой тип возвращает функция;
  • как считается неделя;
  • в какой временной зоне интерпретируется значение.

Сравнение PostgreSQL, MySQL и ClickHouse

Задача PostgreSQL MySQL ClickHouse
Достать час DATE_PART('hour', created_at) HOUR(created_at) toHour(created_at)
Достать день года DATE_PART('doy', created_at) DAYOFYEAR(created_at) toDayOfYear(created_at)
Достать день недели DATE_PART('dow', created_at) DAYOFWEEK(created_at) toDayOfWeek(created_at)
Получить секунды эпохи DATE_PART('epoch', created_at) UNIX_TIMESTAMP(created_at) toUnixTimestamp(created_at)

Главное отличие — день недели.

Для одной и той же даты воскресенья разные СУБД могут вернуть разные числа:

СУБД и функция Воскресенье
PostgreSQL DATE_PART('dow', d) 0
PostgreSQL DATE_PART('isodow', d) 7
MySQL DAYOFWEEK(d) 1
ClickHouse toDayOfWeek(d) 7

Поэтому любой фильтр выходных, отчёт по дням недели или CASE с номерами дней при переносе нужно проверять вручную.

Когда использовать DATE_PART

DATE_PART хорошо подходит, когда нужно получить числовую часть даты:

  • час события;
  • номер месяца;
  • день недели;
  • день года;
  • номер недели;
  • возраст записи в секундах, минутах, часах или днях;
  • длительность интервала как число.

Хорошие примеры:

SELECT
  DATE_PART('hour', created_at) AS hour_of_day,
  COUNT(*) AS events_count
FROM events
GROUP BY DATE_PART('hour', created_at)
ORDER BY hour_of_day;
SELECT
  DATE_PART('isodow', created_at) AS weekday_num,
  COUNT(*) AS signups_count
FROM users
GROUP BY DATE_PART('isodow', created_at)
ORDER BY weekday_num;
SELECT
  id,
  DATE_PART('epoch', finished_at - started_at) AS duration_seconds
FROM jobs
WHERE finished_at IS NOT NULL;

Когда лучше выбрать другой инструмент

DATE_PART не всегда лучший выбор.

Если нужно округлить дату до начала месяца, дня или недели, чаще нужен DATE_TRUNC.

SELECT
  DATE_TRUNC('month', created_at) AS month_start,
  COUNT(*) AS orders_count
FROM orders
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY month_start;

Если нужно отфильтровать данные по году, месяцу или дню на большой таблице, часто лучше использовать диапазон дат:

SELECT *
FROM orders
WHERE created_at >= TIMESTAMP '2026-06-01 00:00:00'
  AND created_at < TIMESTAMP '2026-07-01 00:00:00';

Если нужно красиво вывести дату текстом, нужен TO_CHAR:

SELECT
  TO_CHAR(created_at, 'YYYY-MM-DD') AS created_day
FROM orders;

То есть DATE_PART — это инструмент именно для извлечения числа из даты, а не универсальная замена всем функциям работы с датами.

Главное из статьи

DATE_PART достаёт одну числовую часть из даты, времени или интервала.

Базовый синтаксис:

DATE_PART('field', date_or_timestamp)

Например:

SELECT DATE_PART('hour', created_at) AS hour_of_day
FROM orders;

Самые полезные поля:

  • 'hour' — час;
  • 'minute' — минута;
  • 'second' — секунда;
  • 'year' — год;
  • 'month' — месяц;
  • 'day' — день месяца;
  • 'dow' — день недели, где воскресенье равно 0;
  • 'isodow' — день недели по ISO, где понедельник равен 1, а воскресенье равно 7;
  • 'doy' — день года;
  • 'week' — номер ISO-недели;
  • 'epoch' — секунды эпохи или полная длительность интервала в секундах.

Главные тонкости:

  • DATE_PART возвращает double precision;
  • dow и isodow по-разному нумеруют дни недели;
  • для длительности интервалов часто удобнее использовать 'epoch';
  • в WHERE на больших таблицах лучше не оборачивать индексированную дату в DATE_PART, если можно написать фильтр диапазоном;
  • в MySQL и ClickHouse используются другие функции, а нумерация дней недели отличается.

Если коротко по смыслу: DATE_PART нужен тогда, когда дата хранится целиком, а для отчёта или расчёта вам нужна только одна её числовая часть.

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

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

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