EXTRACT — это способ достать из даты или времени отдельную часть: год, месяц, день, час, минуту, день недели, квартал и многое другое.
Представь, что дата — это не одно простое значение, а аккуратно упакованная коробка. Внутри лежат сразу несколько деталей:
- год;
- месяц;
- день;
- часы;
- минуты;
- секунды;
- день недели;
- номер недели;
- квартал.
EXTRACT открывает эту коробку и достаёт ровно ту деталь, которая нужна в запросе.
Например, из даты 2026-03-15 можно достать:
- год:
2026;
- месяц:
3;
- день:
15.
Это особенно полезно в аналитике: посчитать регистрации по годам, найти заказы по месяцам, отобрать события только по понедельникам или измерить разницу между двумя событиями в секундах.
В реальных таблицах даты почти всегда хранятся целиком. Например, в колонке created_at может лежать значение:
TIMESTAMP '2026-03-15 10:30:00'
Для базы данных это одно значение. Но человеку часто нужна не вся дата, а отдельный смысловой кусочек.
Например:
- для отчёта по годам нужен только год;
- для отчёта по месяцам нужен месяц;
- для фильтра «только рабочие дни» нужен день недели;
- для анализа активности по часам нужен час;
- для расчёта длительности нужны секунды.
Вот здесь и появляется EXTRACT.
Он отвечает на вопрос:
«Достань из этой даты конкретную часть и верни её числом».
Базовый синтаксис
Общий вид такой:
EXTRACT(field FROM date_expression)
Где:
field — какую часть даты нужно достать;
date_expression — дата, время, timestamp или интервал.
Примеры:
SELECT EXTRACT(YEAR FROM TIMESTAMP '2026-03-15 10:30:00');
SELECT EXTRACT(MONTH FROM DATE '2026-03-15');
SELECT EXTRACT(DAY FROM DATE '2026-03-15');
SELECT EXTRACT(HOUR FROM TIMESTAMP '2026-03-15 10:30:00');
Результат будет таким:
| expression |
result |
YEAR |
2026 |
MONTH |
3 |
DAY |
15 |
HOUR |
10 |
То есть EXTRACT не возвращает новую дату. Он возвращает число.
Вот поля, которые встречаются чаще всего.
| Поле |
Что возвращает |
YEAR |
Год |
MONTH |
Месяц от 1 до 12 |
DAY |
День месяца от 1 до 31 |
HOUR |
Час от 0 до 23 |
MINUTE |
Минуту от 0 до 59 |
SECOND |
Секунду, иногда с дробной частью |
DOW |
День недели: 0 — воскресенье, 6 — суббота |
ISODOW |
День недели по ISO: 1 — понедельник, 7 — воскресенье |
DOY |
День года от 1 до 366 |
WEEK |
Номер ISO-недели от 1 до 53 |
QUARTER |
Квартал от 1 до 4 |
EPOCH |
Количество секунд |
Для новичка самые важные: YEAR, MONTH, DAY, HOUR, ISODOW и EPOCH.
С ними ты уже сможешь решать большую часть обычных задач с датами.
Простой пример
Допустим, есть дата заказа:
SELECT
EXTRACT(YEAR FROM TIMESTAMP '2026-03-15 10:30:00') AS order_year,
EXTRACT(MONTH FROM TIMESTAMP '2026-03-15 10:30:00') AS order_month,
EXTRACT(DAY FROM TIMESTAMP '2026-03-15 10:30:00') AS order_day;
Результат:
| order_year |
order_month |
order_day |
| 2026 |
3 |
15 |
База взяла один timestamp и разложила его на понятные части.
Пример с таблицей orders
Пусть есть таблица orders:
| id |
created_at |
amount |
| 1 |
2026-01-15 10:00:00 |
100 |
| 2 |
2026-02-03 14:00:00 |
250 |
| 3 |
2026-02-20 11:30:00 |
80 |
| 4 |
2026-03-05 09:00:00 |
500 |
Нужно посчитать количество заказов и сумму продаж по месяцам.
SELECT
EXTRACT(MONTH FROM created_at) AS month,
COUNT(*) AS orders_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY 1
ORDER BY 1;
Результат:
| month |
orders_count |
total_amount |
| 1 |
1 |
100 |
| 2 |
2 |
330 |
| 3 |
1 |
500 |
Здесь EXTRACT(MONTH FROM created_at) достал номер месяца из каждого заказа. Потом GROUP BY собрал заказы с одинаковым месяцем в группы.
Группировка по году и месяцу
Одна частая ошибка новичков — группировать только по месяцу и забывать про год.
Например:
SELECT
EXTRACT(MONTH FROM created_at) AS month,
COUNT(*) AS orders_count
FROM orders
GROUP BY 1
ORDER BY 1;
На первый взгляд всё хорошо. Но если в таблице есть данные за несколько лет, январь 2025 года и январь 2026 года попадут в одну группу.
Чаще правильно писать так:
SELECT
EXTRACT(YEAR FROM created_at) AS year,
EXTRACT(MONTH FROM created_at) AS month,
COUNT(*) AS orders_count
FROM orders
GROUP BY 1, 2
ORDER BY 1, 2;
Теперь январь разных лет не смешается.
Результат будет выглядеть примерно так:
| year |
month |
orders_count |
| 2025 |
12 |
18 |
| 2026 |
1 |
24 |
| 2026 |
2 |
31 |
| 2026 |
3 |
19 |
Это уже нормальная основа для отчёта.
Для отчётов по периодам в SQL часто используют не только EXTRACT, но и DATE_TRUNC.
Разница простая:
EXTRACT достаёт часть даты и возвращает число;
DATE_TRUNC обрезает дату до начала периода и возвращает дату или timestamp.
Например:
SELECT
EXTRACT(YEAR FROM created_at) AS year,
EXTRACT(MONTH FROM created_at) AS month,
COUNT(*) AS users_count
FROM users
WHERE created_at >= TIMESTAMP '2026-01-01 00:00:00'
AND created_at < TIMESTAMP '2027-01-01 00:00:00'
GROUP BY 1, 2
ORDER BY 1, 2;
А вот вариант через DATE_TRUNC:
SELECT
DATE_TRUNC('month', created_at) AS month_start,
COUNT(*) AS users_count
FROM users
WHERE created_at >= TIMESTAMP '2026-01-01 00:00:00'
AND created_at < TIMESTAMP '2027-01-01 00:00:00'
GROUP BY 1
ORDER BY 1;
Если created_at равен 2026-03-15 10:30:00, то:
SELECT DATE_TRUNC('month', TIMESTAMP '2026-03-15 10:30:00');
Вернёт начало месяца:
2026-03-01 00:00:00
Когда что выбирать?
EXTRACT удобно использовать, когда нужны отдельные числовые колонки:
| year |
month |
users_count |
| 2026 |
1 |
120 |
| 2026 |
2 |
155 |
DATE_TRUNC удобно использовать, когда нужен один столбец периода:
| month_start |
users_count |
| 2026-01-01 00:00:00 |
120 |
| 2026-02-01 00:00:00 |
155 |
Для графиков, BI-отчётов и временных рядов DATE_TRUNC часто удобнее. Для учебных задач и отдельных компонентов даты EXTRACT выглядит нагляднее.
Фильтр по месяцу
Допустим, нужно найти все события, которые произошли в марте любого года.
SELECT *
FROM events
WHERE EXTRACT(MONTH FROM occurred_at) = 3;
Такой запрос найдёт март и в 2024, и в 2025, и в 2026 году.
Это важно понимать: здесь мы фильтруем именно номер месяца, а не конкретный март конкретного года.
Если нужен март 2026 года, лучше писать диапазон:
SELECT *
FROM events
WHERE occurred_at >= TIMESTAMP '2026-03-01 00:00:00'
AND occurred_at < TIMESTAMP '2026-04-01 00:00:00';
Так запрос точнее: он берёт только даты с начала марта 2026 до начала апреля 2026.
На маленьких таблицах разницы можно не заметить. Но на больших таблицах это очень важно.
Допустим, на колонке occurred_at есть обычный индекс. Такой запрос часто может использовать индекс:
SELECT *
FROM events
WHERE occurred_at >= TIMESTAMP '2026-03-01 00:00:00'
AND occurred_at < TIMESTAMP '2026-04-01 00:00:00';
А такой запрос может помешать обычному индексу:
SELECT *
FROM events
WHERE EXTRACT(MONTH FROM occurred_at) = 3;
Почему?
Потому что база должна взять значение occurred_at, применить к нему функцию EXTRACT, получить месяц и только потом сравнить его с 3.
То есть индекс по исходной дате уже не так полезен.
Простое правило:
Если фильтруешь конкретный период, используй диапазон дат.
Хорошо:
SELECT *
FROM orders
WHERE created_at >= TIMESTAMP '2026-01-01 00:00:00'
AND created_at < TIMESTAMP '2026-02-01 00:00:00';
Менее удачно для больших таблиц:
SELECT *
FROM orders
WHERE EXTRACT(YEAR FROM created_at) = 2026
AND EXTRACT(MONTH FROM created_at) = 1;
EXTRACT в фильтре не запрещён. Просто для производительности диапазонная форма часто лучше.
Фильтр по дню недели
Одна из самых приятных задач для EXTRACT — работа с днями недели.
Например, нужно выбрать события, которые случились в понедельник:
SELECT *
FROM events
WHERE EXTRACT(ISODOW FROM occurred_at) = 1;
Почему ISODOW, а не DOW?
Потому что у них разная нумерация.
| Поле |
Как нумеруются дни |
DOW |
0 — воскресенье, 1 — понедельник, ..., 6 — суббота |
ISODOW |
1 — понедельник, 2 — вторник, ..., 7 — воскресенье |
В обычной жизни в России и Европе неделя чаще начинается с понедельника. Поэтому для понятной логики обычно удобнее ISODOW.
Например, выбрать события только в будни:
SELECT *
FROM events
WHERE EXTRACT(ISODOW FROM occurred_at) BETWEEN 1 AND 5;
А выбрать только выходные:
SELECT *
FROM events
WHERE EXTRACT(ISODOW FROM occurred_at) IN (6, 7);
Фильтр по рабочему времени
Можно совместить день недели и час.
Например, нужно найти события, которые произошли в рабочее время: с понедельника по пятницу, с 9:00 до 17:59.
SELECT *
FROM events
WHERE EXTRACT(ISODOW FROM occurred_at) BETWEEN 1 AND 5
AND EXTRACT(HOUR FROM occurred_at) BETWEEN 9 AND 17;
Здесь логика такая:
ISODOW BETWEEN 1 AND 5 — только понедельник, вторник, среда, четверг и пятница;
HOUR BETWEEN 9 AND 17 — часы от 9 до 17 включительно.
То есть время 17:45 подойдёт, потому что час равен 17.
А время 18:00 уже не подойдёт, потому что час равен 18.
EPOCH — особое поле. Оно работает с секундами.
В PostgreSQL EXTRACT(EPOCH FROM ...) может использоваться в двух популярных случаях:
- получить Unix timestamp;
- посчитать длительность интервала в секундах.
Unix timestamp — это количество секунд с начала эпохи Unix: 1970-01-01 00:00:00 UTC.
Пример:
SELECT EXTRACT(EPOCH FROM TIMESTAMP '2026-03-15 12:00:00');
Результатом будет количество секунд.
Но на практике чаще EPOCH используют для интервалов.
Например, нужно узнать, сколько секунд между двумя timestamp-значениями:
SELECT
EXTRACT(
EPOCH FROM (
TIMESTAMP '2026-03-15 10:30:00'
- TIMESTAMP '2026-03-15 09:00:00'
)
) AS seconds_diff;
Результат:
5400 секунд — это полтора часа.
Перевод секунд в минуты, часы и дни
Раз EPOCH возвращает секунды, их можно делить.
Например, разница в минутах:
SELECT
EXTRACT(
EPOCH FROM (
TIMESTAMP '2026-03-15 10:30:00'
- TIMESTAMP '2026-03-15 09:00:00'
)
) / 60 AS minutes_diff;
Результат:
Разница в часах:
SELECT
EXTRACT(
EPOCH FROM (
TIMESTAMP '2026-03-15 10:30:00'
- TIMESTAMP '2026-03-15 09:00:00'
)
) / 3600 AS hours_diff;
Результат:
Разница в днях:
SELECT
name,
EXTRACT(EPOCH FROM (NOW() - created_at)) / 86400 AS days_since_signup
FROM users;
Здесь 86400 — количество секунд в сутках.
EPOCH может вернуть дробное число
Если timestamp содержит микросекунды, результат тоже может быть дробным.
Например:
SELECT EXTRACT(EPOCH FROM TIMESTAMP '2026-03-15 12:00:00.123456');
Результат будет с дробной частью.
Если нужен целый Unix timestamp, можно привести результат к целому типу:
SELECT EXTRACT(EPOCH FROM TIMESTAMP '2026-03-15 12:00:00')::BIGINT;
В PostgreSQL ::BIGINT — это приведение типа.
Часовые пояса: важный момент
С датами и временем всегда есть тонкое место — часовой пояс.
Если колонка хранит TIMESTAMPTZ, то EXTRACT учитывает часовой пояс текущей сессии.
Например, один и тот же момент времени может быть:
09:00 в UTC;
12:00 в Москве;
16:00 во Вьетнаме.
Если ты достаёшь час через EXTRACT(HOUR FROM ...), важно понимать: час в какой зоне ты хочешь получить?
В PostgreSQL можно явно указать часовой пояс через AT TIME ZONE.
Например, достать час в московском времени:
SELECT EXTRACT(HOUR FROM tstz AT TIME ZONE 'Europe/Moscow')
FROM events;
А если нужно получить час во вьетнамском времени:
SELECT EXTRACT(HOUR FROM tstz AT TIME ZONE 'Asia/Ho_Chi_Minh')
FROM events;
Главная мысль:
Если отчёт зависит от локального времени пользователя, не забывай про часовой пояс.
Иначе можно получить странные результаты: например, пользователь сделал заказ вечером, а в отчёте он попал в другой день, потому что база считала время в UTC.
Группировка по кварталам
EXTRACT удобно использовать и для кварталов.
SELECT
EXTRACT(YEAR FROM created_at) AS year,
EXTRACT(QUARTER FROM created_at) AS quarter,
SUM(amount) AS total_amount
FROM orders
GROUP BY 1, 2
ORDER BY 1, 2;
Результат:
| year |
quarter |
total_amount |
| 2026 |
1 |
930 |
| 2026 |
2 |
1240 |
| 2026 |
3 |
880 |
| 2026 |
4 |
1510 |
И снова важное правило: квартал лучше группировать вместе с годом.
Плохо:
SELECT
EXTRACT(QUARTER FROM created_at) AS quarter,
SUM(amount) AS total_amount
FROM orders
GROUP BY 1
ORDER BY 1;
Так первый квартал 2025 года и первый квартал 2026 года попадут в одну группу.
Лучше:
SELECT
EXTRACT(YEAR FROM created_at) AS year,
EXTRACT(QUARTER FROM created_at) AS quarter,
SUM(amount) AS total_amount
FROM orders
GROUP BY 1, 2
ORDER BY 1, 2;
День года и номер недели
Иногда нужны менее очевидные части даты.
Например, день года:
SELECT EXTRACT(DOY FROM DATE '2026-03-15') AS day_of_year;
DOY показывает номер дня внутри года: от 1 до 365 или 366.
Ещё есть номер недели:
SELECT EXTRACT(WEEK FROM DATE '2026-03-15') AS week_number;
В PostgreSQL WEEK возвращает номер ISO-недели.
С неделями надо быть внимательным: в разных базах данных правила могут отличаться. Особенно если ты переносишь запрос из PostgreSQL в MySQL или наоборот.
Отличия в MySQL
В MySQL тоже есть EXTRACT, но для многих задач чаще используют короткие функции.
Например:
SELECT YEAR('2026-03-15');
SELECT MONTH('2026-03-15');
SELECT DAY('2026-03-15');
SELECT HOUR('2026-03-15 10:30:00');
Для Unix timestamp в MySQL используют UNIX_TIMESTAMP.
SELECT UNIX_TIMESTAMP('2026-03-15 12:00:00');
Для дня недели есть DAYOFWEEK.
SELECT DAYOFWEEK('2026-03-15');
В MySQL у DAYOFWEEK своя нумерация: 1 — воскресенье, 2 — понедельник, и так далее.
Поэтому при переносе логики между PostgreSQL и MySQL всегда проверяй нумерацию дней недели. Это маленькая деталь, которая легко ломает отчёты.
Отличия в SQLite
В SQLite нет EXTRACT в привычном виде. Там обычно используют STRFTIME.
SELECT STRFTIME('%Y', '2026-03-15');
SELECT STRFTIME('%m', '2026-03-15');
SELECT STRFTIME('%d', '2026-03-15');
SELECT STRFTIME('%w', '2026-03-15');
Особенность STRFTIME: он часто возвращает текст, а не число.
Например, месяц может вернуться как строка '03', а не число 3.
Если нужно число, можно привести тип:
SELECT CAST(STRFTIME('%m', '2026-03-15') AS INTEGER);
Отличия в SQL Server
В SQL Server для похожих задач используют DATEPART.
SELECT DATEPART(year, '2026-03-15');
SELECT DATEPART(month, '2026-03-15');
SELECT DATEPART(day, '2026-03-15');
SELECT DATEPART(hour, '2026-03-15 10:30:00');
То есть идея та же самая: достать отдельную часть даты. Просто имя функции другое.
Частые ошибки новичков
Ошибка 1. Забыть год при группировке по месяцу
Проблемный вариант:
SELECT
EXTRACT(MONTH FROM created_at) AS month,
COUNT(*) AS orders_count
FROM orders
GROUP BY 1
ORDER BY 1;
Если в таблице несколько лет, январь разных лет смешается.
Правильнее:
SELECT
EXTRACT(YEAR FROM created_at) AS year,
EXTRACT(MONTH FROM created_at) AS month,
COUNT(*) AS orders_count
FROM orders
GROUP BY 1, 2
ORDER BY 1, 2;
Ошибка 2. Использовать DOW вместо ISODOW
DOW начинается с воскресенья:
| value |
day |
| 0 |
Sunday |
| 1 |
Monday |
| 6 |
Saturday |
ISODOW начинается с понедельника:
| value |
day |
| 1 |
Monday |
| 2 |
Tuesday |
| 7 |
Sunday |
Если тебе нужна привычная неделя с понедельника по воскресенье, обычно выбирай ISODOW.
SELECT *
FROM events
WHERE EXTRACT(ISODOW FROM occurred_at) BETWEEN 1 AND 5;
Работает, но может быть медленнее:
SELECT *
FROM orders
WHERE EXTRACT(YEAR FROM created_at) = 2026
AND EXTRACT(MONTH FROM created_at) = 3;
Лучше для конкретного месяца:
SELECT *
FROM orders
WHERE created_at >= TIMESTAMP '2026-03-01 00:00:00'
AND created_at < TIMESTAMP '2026-04-01 00:00:00';
Так базе проще использовать обычный индекс по created_at.
Ошибка 4. Не учитывать часовой пояс
Если бизнес думает в локальном времени, а данные хранятся в UTC, отчёт может съехать.
Например, заказ был сделан поздно вечером по местному времени, но в UTC это уже другой день или другой час.
Используй явное преобразование:
SELECT
EXTRACT(HOUR FROM created_at AT TIME ZONE 'Europe/Moscow') AS local_hour,
COUNT(*) AS orders_count
FROM orders
GROUP BY 1
ORDER BY 1;
Ошибка 5. Ждать от EPOCH целое число
EPOCH может вернуть значение с дробной частью.
Если нужен целый результат:
SELECT EXTRACT(EPOCH FROM NOW())::BIGINT;
Ошибка 6. Считать недели одинаковыми во всех базах
В PostgreSQL EXTRACT(WEEK FROM date) работает с ISO-неделями.
В MySQL у WEEK(date) есть разные режимы. Они могут влиять на то, с какого дня начинается неделя и как считается первая неделя года.
Если делаешь отчёт по неделям, обязательно проверь правила конкретной СУБД.
EXTRACT хорошо подходит, когда тебе нужно:
- вывести год, месяц или день отдельной колонкой;
- сгруппировать данные по году, месяцу или кварталу;
- отфильтровать события по дню недели;
- понять, в какой час пользователи чаще совершают действия;
- посчитать длительность в секундах через
EPOCH;
- разобрать timestamp на отдельные части для отчёта.
Например, отчёт по активности пользователей по часам:
SELECT
EXTRACT(HOUR FROM created_at) AS hour,
COUNT(*) AS events_count
FROM events
GROUP BY 1
ORDER BY 1;
Результат:
| hour |
events_count |
| 0 |
12 |
| 1 |
8 |
| 2 |
5 |
| 9 |
44 |
| 10 |
61 |
| 11 |
73 |
Так можно быстро увидеть, когда пользователи наиболее активны.
EXTRACT не всегда лучший инструмент.
Если тебе нужно отобрать конкретный период, например март 2026 года, чаще лучше использовать диапазон дат:
SELECT *
FROM events
WHERE occurred_at >= TIMESTAMP '2026-03-01 00:00:00'
AND occurred_at < TIMESTAMP '2026-04-01 00:00:00';
Если тебе нужен отчёт по месяцам как временной ряд, часто удобнее DATE_TRUNC:
SELECT
DATE_TRUNC('month', created_at) AS month_start,
COUNT(*) AS events_count
FROM events
GROUP BY 1
ORDER BY 1;
А если тебе нужны именно отдельные числа — год, месяц, час, день недели — тогда EXTRACT подходит отлично.
Главное из статьи
EXTRACT — это функция для извлечения отдельной части из даты, времени, timestamp или интервала.
Базовый синтаксис:
EXTRACT(field FROM date_expression)
Самые частые поля:
| Поле |
Зачем нужно |
YEAR |
Достать год |
MONTH |
Достать месяц |
DAY |
Достать день месяца |
HOUR |
Достать час |
ISODOW |
Достать день недели с понедельника |
QUARTER |
Достать квартал |
EPOCH |
Получить секунды |
Главные правила:
EXTRACT возвращает число, а не дату.
- Для привычной недели чаще используй
ISODOW, а не DOW.
- При группировке по месяцу или кварталу не забывай добавлять год.
- Для фильтра по конкретному периоду обычно лучше диапазон дат, а не
EXTRACT в WHERE.
- Для отчётов по месяцам часто удобнее
DATE_TRUNC.
- При работе с часами и днями внимательно относись к часовым поясам.
- В MySQL для Unix timestamp используют
UNIX_TIMESTAMP.
- В SQLite вместо
EXTRACT обычно используют STRFTIME.
- В SQL Server похожая функция называется
DATEPART.
Если сказать совсем просто: EXTRACT нужен тогда, когда дата слишком большая, а тебе нужна только одна её часть.
EXTRACT— это способ достать из даты или времени отдельную часть: год, месяц, день, час, минуту, день недели, квартал и многое другое.Представь, что дата — это не одно простое значение, а аккуратно упакованная коробка. Внутри лежат сразу несколько деталей:
EXTRACTоткрывает эту коробку и достаёт ровно ту деталь, которая нужна в запросе.Например, из даты
2026-03-15можно достать:2026;3;15.Это особенно полезно в аналитике: посчитать регистрации по годам, найти заказы по месяцам, отобрать события только по понедельникам или измерить разницу между двумя событиями в секундах.
Зачем нужен EXTRACT
В реальных таблицах даты почти всегда хранятся целиком. Например, в колонке
created_atможет лежать значение:TIMESTAMP '2026-03-15 10:30:00'Для базы данных это одно значение. Но человеку часто нужна не вся дата, а отдельный смысловой кусочек.
Например:
Вот здесь и появляется
EXTRACT.Он отвечает на вопрос:
Базовый синтаксис
Общий вид такой:
EXTRACT(field FROM date_expression)Где:
field— какую часть даты нужно достать;date_expression— дата, время, timestamp или интервал.Примеры:
SELECT EXTRACT(YEAR FROM TIMESTAMP '2026-03-15 10:30:00'); SELECT EXTRACT(MONTH FROM DATE '2026-03-15'); SELECT EXTRACT(DAY FROM DATE '2026-03-15'); SELECT EXTRACT(HOUR FROM TIMESTAMP '2026-03-15 10:30:00');Результат будет таким:
YEARMONTHDAYHOURТо есть
EXTRACTне возвращает новую дату. Он возвращает число.Самые полезные поля EXTRACT
Вот поля, которые встречаются чаще всего.
YEARMONTHDAYHOURMINUTESECONDDOWISODOWDOYWEEKQUARTEREPOCHДля новичка самые важные:
YEAR,MONTH,DAY,HOUR,ISODOWиEPOCH.С ними ты уже сможешь решать большую часть обычных задач с датами.
Простой пример
Допустим, есть дата заказа:
SELECT EXTRACT(YEAR FROM TIMESTAMP '2026-03-15 10:30:00') AS order_year, EXTRACT(MONTH FROM TIMESTAMP '2026-03-15 10:30:00') AS order_month, EXTRACT(DAY FROM TIMESTAMP '2026-03-15 10:30:00') AS order_day;Результат:
База взяла один timestamp и разложила его на понятные части.
Пример с таблицей orders
Пусть есть таблица
orders:Нужно посчитать количество заказов и сумму продаж по месяцам.
SELECT EXTRACT(MONTH FROM created_at) AS month, COUNT(*) AS orders_count, SUM(amount) AS total_amount FROM orders GROUP BY 1 ORDER BY 1;Результат:
Здесь
EXTRACT(MONTH FROM created_at)достал номер месяца из каждого заказа. ПотомGROUP BYсобрал заказы с одинаковым месяцем в группы.Группировка по году и месяцу
Одна частая ошибка новичков — группировать только по месяцу и забывать про год.
Например:
SELECT EXTRACT(MONTH FROM created_at) AS month, COUNT(*) AS orders_count FROM orders GROUP BY 1 ORDER BY 1;На первый взгляд всё хорошо. Но если в таблице есть данные за несколько лет, январь 2025 года и январь 2026 года попадут в одну группу.
Чаще правильно писать так:
SELECT EXTRACT(YEAR FROM created_at) AS year, EXTRACT(MONTH FROM created_at) AS month, COUNT(*) AS orders_count FROM orders GROUP BY 1, 2 ORDER BY 1, 2;Теперь январь разных лет не смешается.
Результат будет выглядеть примерно так:
Это уже нормальная основа для отчёта.
EXTRACT или DATE_TRUNC
Для отчётов по периодам в SQL часто используют не только
EXTRACT, но иDATE_TRUNC.Разница простая:
EXTRACTдостаёт часть даты и возвращает число;DATE_TRUNCобрезает дату до начала периода и возвращает дату или timestamp.Например:
SELECT EXTRACT(YEAR FROM created_at) AS year, EXTRACT(MONTH FROM created_at) AS month, COUNT(*) AS users_count FROM users WHERE created_at >= TIMESTAMP '2026-01-01 00:00:00' AND created_at < TIMESTAMP '2027-01-01 00:00:00' GROUP BY 1, 2 ORDER BY 1, 2;А вот вариант через
DATE_TRUNC:SELECT DATE_TRUNC('month', created_at) AS month_start, COUNT(*) AS users_count FROM users WHERE created_at >= TIMESTAMP '2026-01-01 00:00:00' AND created_at < TIMESTAMP '2027-01-01 00:00:00' GROUP BY 1 ORDER BY 1;Если
created_atравен2026-03-15 10:30:00, то:SELECT DATE_TRUNC('month', TIMESTAMP '2026-03-15 10:30:00');Вернёт начало месяца:
2026-03-01 00:00:00Когда что выбирать?
EXTRACTудобно использовать, когда нужны отдельные числовые колонки:DATE_TRUNCудобно использовать, когда нужен один столбец периода:Для графиков, BI-отчётов и временных рядов
DATE_TRUNCчасто удобнее. Для учебных задач и отдельных компонентов датыEXTRACTвыглядит нагляднее.Фильтр по месяцу
Допустим, нужно найти все события, которые произошли в марте любого года.
SELECT * FROM events WHERE EXTRACT(MONTH FROM occurred_at) = 3;Такой запрос найдёт март и в 2024, и в 2025, и в 2026 году.
Это важно понимать: здесь мы фильтруем именно номер месяца, а не конкретный март конкретного года.
Если нужен март 2026 года, лучше писать диапазон:
SELECT * FROM events WHERE occurred_at >= TIMESTAMP '2026-03-01 00:00:00' AND occurred_at < TIMESTAMP '2026-04-01 00:00:00';Так запрос точнее: он берёт только даты с начала марта 2026 до начала апреля 2026.
Почему диапазон часто лучше EXTRACT в WHERE
На маленьких таблицах разницы можно не заметить. Но на больших таблицах это очень важно.
Допустим, на колонке
occurred_atесть обычный индекс. Такой запрос часто может использовать индекс:SELECT * FROM events WHERE occurred_at >= TIMESTAMP '2026-03-01 00:00:00' AND occurred_at < TIMESTAMP '2026-04-01 00:00:00';А такой запрос может помешать обычному индексу:
SELECT * FROM events WHERE EXTRACT(MONTH FROM occurred_at) = 3;Почему?
Потому что база должна взять значение
occurred_at, применить к нему функциюEXTRACT, получить месяц и только потом сравнить его с3.То есть индекс по исходной дате уже не так полезен.
Простое правило:
Хорошо:
SELECT * FROM orders WHERE created_at >= TIMESTAMP '2026-01-01 00:00:00' AND created_at < TIMESTAMP '2026-02-01 00:00:00';Менее удачно для больших таблиц:
SELECT * FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2026 AND EXTRACT(MONTH FROM created_at) = 1;EXTRACTв фильтре не запрещён. Просто для производительности диапазонная форма часто лучше.Фильтр по дню недели
Одна из самых приятных задач для
EXTRACT— работа с днями недели.Например, нужно выбрать события, которые случились в понедельник:
SELECT * FROM events WHERE EXTRACT(ISODOW FROM occurred_at) = 1;Почему
ISODOW, а неDOW?Потому что у них разная нумерация.
DOWISODOWВ обычной жизни в России и Европе неделя чаще начинается с понедельника. Поэтому для понятной логики обычно удобнее
ISODOW.Например, выбрать события только в будни:
SELECT * FROM events WHERE EXTRACT(ISODOW FROM occurred_at) BETWEEN 1 AND 5;А выбрать только выходные:
SELECT * FROM events WHERE EXTRACT(ISODOW FROM occurred_at) IN (6, 7);Фильтр по рабочему времени
Можно совместить день недели и час.
Например, нужно найти события, которые произошли в рабочее время: с понедельника по пятницу, с 9:00 до 17:59.
SELECT * FROM events WHERE EXTRACT(ISODOW FROM occurred_at) BETWEEN 1 AND 5 AND EXTRACT(HOUR FROM occurred_at) BETWEEN 9 AND 17;Здесь логика такая:
ISODOW BETWEEN 1 AND 5— только понедельник, вторник, среда, четверг и пятница;HOUR BETWEEN 9 AND 17— часы от 9 до 17 включительно.То есть время
17:45подойдёт, потому что час равен17.А время
18:00уже не подойдёт, потому что час равен18.EXTRACT и EPOCH
EPOCH— особое поле. Оно работает с секундами.В PostgreSQL
EXTRACT(EPOCH FROM ...)может использоваться в двух популярных случаях:Unix timestamp — это количество секунд с начала эпохи Unix:
1970-01-01 00:00:00 UTC.Пример:
SELECT EXTRACT(EPOCH FROM TIMESTAMP '2026-03-15 12:00:00');Результатом будет количество секунд.
Но на практике чаще
EPOCHиспользуют для интервалов.Например, нужно узнать, сколько секунд между двумя timestamp-значениями:
SELECT EXTRACT( EPOCH FROM ( TIMESTAMP '2026-03-15 10:30:00' - TIMESTAMP '2026-03-15 09:00:00' ) ) AS seconds_diff;Результат:
5400секунд — это полтора часа.Перевод секунд в минуты, часы и дни
Раз
EPOCHвозвращает секунды, их можно делить.Например, разница в минутах:
SELECT EXTRACT( EPOCH FROM ( TIMESTAMP '2026-03-15 10:30:00' - TIMESTAMP '2026-03-15 09:00:00' ) ) / 60 AS minutes_diff;Результат:
Разница в часах:
SELECT EXTRACT( EPOCH FROM ( TIMESTAMP '2026-03-15 10:30:00' - TIMESTAMP '2026-03-15 09:00:00' ) ) / 3600 AS hours_diff;Результат:
Разница в днях:
SELECT name, EXTRACT(EPOCH FROM (NOW() - created_at)) / 86400 AS days_since_signup FROM users;Здесь
86400— количество секунд в сутках.EPOCH может вернуть дробное число
Если timestamp содержит микросекунды, результат тоже может быть дробным.
Например:
SELECT EXTRACT(EPOCH FROM TIMESTAMP '2026-03-15 12:00:00.123456');Результат будет с дробной частью.
Если нужен целый Unix timestamp, можно привести результат к целому типу:
SELECT EXTRACT(EPOCH FROM TIMESTAMP '2026-03-15 12:00:00')::BIGINT;В PostgreSQL
::BIGINT— это приведение типа.Часовые пояса: важный момент
С датами и временем всегда есть тонкое место — часовой пояс.
Если колонка хранит
TIMESTAMPTZ, тоEXTRACTучитывает часовой пояс текущей сессии.Например, один и тот же момент времени может быть:
09:00в UTC;12:00в Москве;16:00во Вьетнаме.Если ты достаёшь час через
EXTRACT(HOUR FROM ...), важно понимать: час в какой зоне ты хочешь получить?В PostgreSQL можно явно указать часовой пояс через
AT TIME ZONE.Например, достать час в московском времени:
SELECT EXTRACT(HOUR FROM tstz AT TIME ZONE 'Europe/Moscow') FROM events;А если нужно получить час во вьетнамском времени:
SELECT EXTRACT(HOUR FROM tstz AT TIME ZONE 'Asia/Ho_Chi_Minh') FROM events;Главная мысль:
Иначе можно получить странные результаты: например, пользователь сделал заказ вечером, а в отчёте он попал в другой день, потому что база считала время в UTC.
Группировка по кварталам
EXTRACTудобно использовать и для кварталов.SELECT EXTRACT(YEAR FROM created_at) AS year, EXTRACT(QUARTER FROM created_at) AS quarter, SUM(amount) AS total_amount FROM orders GROUP BY 1, 2 ORDER BY 1, 2;Результат:
И снова важное правило: квартал лучше группировать вместе с годом.
Плохо:
SELECT EXTRACT(QUARTER FROM created_at) AS quarter, SUM(amount) AS total_amount FROM orders GROUP BY 1 ORDER BY 1;Так первый квартал 2025 года и первый квартал 2026 года попадут в одну группу.
Лучше:
SELECT EXTRACT(YEAR FROM created_at) AS year, EXTRACT(QUARTER FROM created_at) AS quarter, SUM(amount) AS total_amount FROM orders GROUP BY 1, 2 ORDER BY 1, 2;День года и номер недели
Иногда нужны менее очевидные части даты.
Например, день года:
SELECT EXTRACT(DOY FROM DATE '2026-03-15') AS day_of_year;DOYпоказывает номер дня внутри года: от 1 до 365 или 366.Ещё есть номер недели:
SELECT EXTRACT(WEEK FROM DATE '2026-03-15') AS week_number;В PostgreSQL
WEEKвозвращает номер ISO-недели.С неделями надо быть внимательным: в разных базах данных правила могут отличаться. Особенно если ты переносишь запрос из PostgreSQL в MySQL или наоборот.
Отличия в MySQL
В MySQL тоже есть
EXTRACT, но для многих задач чаще используют короткие функции.Например:
SELECT YEAR('2026-03-15'); SELECT MONTH('2026-03-15'); SELECT DAY('2026-03-15'); SELECT HOUR('2026-03-15 10:30:00');Для Unix timestamp в MySQL используют
UNIX_TIMESTAMP.SELECT UNIX_TIMESTAMP('2026-03-15 12:00:00');Для дня недели есть
DAYOFWEEK.SELECT DAYOFWEEK('2026-03-15');В MySQL у
DAYOFWEEKсвоя нумерация:1— воскресенье,2— понедельник, и так далее.Поэтому при переносе логики между PostgreSQL и MySQL всегда проверяй нумерацию дней недели. Это маленькая деталь, которая легко ломает отчёты.
Отличия в SQLite
В SQLite нет
EXTRACTв привычном виде. Там обычно используютSTRFTIME.SELECT STRFTIME('%Y', '2026-03-15'); SELECT STRFTIME('%m', '2026-03-15'); SELECT STRFTIME('%d', '2026-03-15'); SELECT STRFTIME('%w', '2026-03-15');Особенность
STRFTIME: он часто возвращает текст, а не число.Например, месяц может вернуться как строка
'03', а не число3.Если нужно число, можно привести тип:
SELECT CAST(STRFTIME('%m', '2026-03-15') AS INTEGER);Отличия в SQL Server
В SQL Server для похожих задач используют
DATEPART.SELECT DATEPART(year, '2026-03-15'); SELECT DATEPART(month, '2026-03-15'); SELECT DATEPART(day, '2026-03-15'); SELECT DATEPART(hour, '2026-03-15 10:30:00');То есть идея та же самая: достать отдельную часть даты. Просто имя функции другое.
Частые ошибки новичков
Ошибка 1. Забыть год при группировке по месяцу
Проблемный вариант:
SELECT EXTRACT(MONTH FROM created_at) AS month, COUNT(*) AS orders_count FROM orders GROUP BY 1 ORDER BY 1;Если в таблице несколько лет, январь разных лет смешается.
Правильнее:
SELECT EXTRACT(YEAR FROM created_at) AS year, EXTRACT(MONTH FROM created_at) AS month, COUNT(*) AS orders_count FROM orders GROUP BY 1, 2 ORDER BY 1, 2;Ошибка 2. Использовать DOW вместо ISODOW
DOWначинается с воскресенья:ISODOWначинается с понедельника:Если тебе нужна привычная неделя с понедельника по воскресенье, обычно выбирай
ISODOW.SELECT * FROM events WHERE EXTRACT(ISODOW FROM occurred_at) BETWEEN 1 AND 5;Ошибка 3. Фильтровать период через EXTRACT вместо диапазона
Работает, но может быть медленнее:
SELECT * FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2026 AND EXTRACT(MONTH FROM created_at) = 3;Лучше для конкретного месяца:
SELECT * FROM orders WHERE created_at >= TIMESTAMP '2026-03-01 00:00:00' AND created_at < TIMESTAMP '2026-04-01 00:00:00';Так базе проще использовать обычный индекс по
created_at.Ошибка 4. Не учитывать часовой пояс
Если бизнес думает в локальном времени, а данные хранятся в UTC, отчёт может съехать.
Например, заказ был сделан поздно вечером по местному времени, но в UTC это уже другой день или другой час.
Используй явное преобразование:
SELECT EXTRACT(HOUR FROM created_at AT TIME ZONE 'Europe/Moscow') AS local_hour, COUNT(*) AS orders_count FROM orders GROUP BY 1 ORDER BY 1;Ошибка 5. Ждать от EPOCH целое число
EPOCHможет вернуть значение с дробной частью.Если нужен целый результат:
SELECT EXTRACT(EPOCH FROM NOW())::BIGINT;Ошибка 6. Считать недели одинаковыми во всех базах
В PostgreSQL
EXTRACT(WEEK FROM date)работает с ISO-неделями.В MySQL у
WEEK(date)есть разные режимы. Они могут влиять на то, с какого дня начинается неделя и как считается первая неделя года.Если делаешь отчёт по неделям, обязательно проверь правила конкретной СУБД.
Когда EXTRACT — хороший выбор
EXTRACTхорошо подходит, когда тебе нужно:EPOCH;Например, отчёт по активности пользователей по часам:
SELECT EXTRACT(HOUR FROM created_at) AS hour, COUNT(*) AS events_count FROM events GROUP BY 1 ORDER BY 1;Результат:
Так можно быстро увидеть, когда пользователи наиболее активны.
Когда лучше не использовать EXTRACT
EXTRACTне всегда лучший инструмент.Если тебе нужно отобрать конкретный период, например март 2026 года, чаще лучше использовать диапазон дат:
SELECT * FROM events WHERE occurred_at >= TIMESTAMP '2026-03-01 00:00:00' AND occurred_at < TIMESTAMP '2026-04-01 00:00:00';Если тебе нужен отчёт по месяцам как временной ряд, часто удобнее
DATE_TRUNC:SELECT DATE_TRUNC('month', created_at) AS month_start, COUNT(*) AS events_count FROM events GROUP BY 1 ORDER BY 1;А если тебе нужны именно отдельные числа — год, месяц, час, день недели — тогда
EXTRACTподходит отлично.Главное из статьи
EXTRACT— это функция для извлечения отдельной части из даты, времени, timestamp или интервала.Базовый синтаксис:
EXTRACT(field FROM date_expression)Самые частые поля:
YEARMONTHDAYHOURISODOWQUARTEREPOCHГлавные правила:
EXTRACTвозвращает число, а не дату.ISODOW, а неDOW.EXTRACTвWHERE.DATE_TRUNC.UNIX_TIMESTAMP.EXTRACTобычно используютSTRFTIME.DATEPART.Если сказать совсем просто:
EXTRACTнужен тогда, когда дата слишком большая, а тебе нужна только одна её часть.