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;
Запрос делает три вещи:
- Берёт из
created_at только час.
- Считает количество оплаченных заказов в каждом часе.
- Сортирует результат от 0 до 23.
Результат может выглядеть так:
hour_of_day | orders_count
------------+-------------
9 | 18
10 | 25
11 | 31
12 | 44
Такой отчёт помогает увидеть привычки пользователей: например, что больше всего заказов приходит в обед или вечером после работы.
В 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' неделя начинается с воскресенья:
| День |
Значение |
| Воскресенье |
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;
Что здесь происходит:
NOW() - created_at даёт интервал между текущим моментом и датой регистрации.
DATE_PART('epoch', ...) превращает этот интервал в секунды.
- Деление на 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;
Запрос делает следующее:
- Берёт только заказы в статусе
pending.
- Считает возраст каждого заказа в часах.
- Группирует заказы по отделам.
- Показывает средний возраст заказа в каждом отделе.
Деление на 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 нужен тогда, когда дата хранится целиком, а для отчёта или расчёта вам нужна только одна её числовая часть.
DATE_PARTв PostgreSQL достаёт из даты или времени одну конкретную часть и возвращает её числом.Например, у вас есть дата заказа:
2026-06-17 14:32:09. Из неё можно отдельно получить:Это удобно, когда нужна не вся дата целиком, а одно её свойство: построить отчёт по часам, посчитать регистрации по дням недели, сгруппировать продажи по неделям или вычислить длительность в секундах.
Простая идея: дата как коробка с деталями
Представьте дату как коробку, внутри которой лежат разные детали:
В этой коробке есть год, месяц, день, час, минута, секунда.
DATE_PARTпозволяет аккуратно достать одну деталь:SELECT DATE_PART('hour', TIMESTAMP '2026-06-17 14:32:09') AS hour_of_day;Результат:
Синтаксис такой:
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;Результат будет примерно таким:
То есть PostgreSQL говорит:
dow;Что возвращает 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;Результат:
На экране это выглядит как обычные числа, но тип результата —
double precision. В большинстве отчётов это не мешает, но иногда важно при сравнении типов, округлении или передаче результата в другие выражения.Полезные поля для DATE_PART
У
DATE_PARTесть много полей. Новичку чаще всего нужны такие:'year''month''day''hour''minute''second''dow''isodow''doy''week''epoch'Пример с разными частями даты:
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;Запрос делает три вещи:
created_atтолько час.Результат может выглядеть так:
Такой отчёт помогает увидеть привычки пользователей: например, что больше всего заказов приходит в обед или вечером после работы.
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'неделя начинается с воскресенья:Для
'isodow'используется ISO-нумерация:Проверим на воскресенье:
SELECT DATE_PART('dow', DATE '2026-06-21') AS dow_num, DATE_PART('isodow', DATE '2026-06-21') AS isodow_num;Результат:
Дата одна и та же, но числа разные.
Для отчётов на русском и в европейской логике чаще удобнее
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?Потому что названия дней — это текст. Если сортировать по тексту, порядок может стать алфавитным, а не календарным. Номер дня недели даёт правильный порядок: понедельник, вторник, среда и так далее.
Как правильно фильтровать выходные
Если вы используете
'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;Результат:
Это удобно для отчётов вида «продажи по неделям»:
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'используют не ради красивого вывода, а ради расчётов.Например, можно посчитать возраст аккаунта в днях:
SELECT id, DATE_PART('epoch', NOW() - created_at) / 86400 AS account_age_days FROM users ORDER BY account_age_days DESC LIMIT 10;Что здесь происходит:
NOW() - created_atдаёт интервал между текущим моментом и датой регистрации.DATE_PART('epoch', ...)превращает этот интервал в секунды.Почему 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;Результат:
Вот здесь очень важный момент.
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;Запрос делает следующее:
pending.Деление на 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
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)Главное отличие — день недели.
Для одной и той же даты воскресенья разные СУБД могут вернуть разные числа:
DATE_PART('dow', d)DATE_PART('isodow', d)DAYOFWEEK(d)toDayOfWeek(d)Поэтому любой фильтр выходных, отчёт по дням недели или
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, если можно написать фильтр диапазоном;Если коротко по смыслу:
DATE_PARTнужен тогда, когда дата хранится целиком, а для отчёта или расчёта вам нужна только одна её числовая часть.