Функции даты в SQL: текущая дата, разница дат, EXTRACT и формат
Справочник функций даты в SQL на учебной базе: текущая дата, часть даты, date_trunc, разница дат в днях, часах и месяцах, интервалы, формат вывода и отличия СУБД.
Содержание статьи
Функции даты ищут в момент, когда запрос уже почти написан: нужно достать месяц, посчитать дни до оплаты, прибавить неделю или вывести дату как 03.08.2026. Трудность не в синтаксисе, а в том, что у каждой базы свой набор функций и свои правила. В PostgreSQL нет datediff, в DuckDB нет to_char, а date_trunc от даты в обеих возвращает не дату. Это справочник: задача, запись в PostgreSQL и DuckDB, пример на учебной базе SQL-курса с результатом и ловушка, если она есть. В конце — таблица для MySQL, ClickHouse и SQL Server. Про границы периодов, недели, календарь дат и часовые пояса отчётов есть отдельная статья о периодах в SQL; здесь — сами функции.
Коротко
Все примеры ниже выполняются в песочнице курса (DuckDB) и проверены на PostgreSQL 16. Где записи расходятся, это сказано явно.
- Текущая дата —
current_date, момент времени —now(). Обе зависят от часового пояса сессии: в одну и ту же минуту PostgreSQL в UTC ответил 23 сентября, в Москве — 24-го. - Часть даты —
extract(isodow FROM d),extract(month FROM d). Номер дня недели бывает с нуля и с воскресенья, поэтому для выходных надёжнееisodow: 6 и 7. - Разница в днях —
d2 - d1для двух дат: целое число в обеих базах.datediffв PostgreSQL нет. - Разница в часах —
extract(epoch FROM t2 - t1) / 3600.date_part('hour', интервал)возвращает только часовое поле: 1 вместо 49. - Месяцы считают по-разному:
ageв PostgreSQL даёт полные месяцы,date_diff('month', …)в DuckDB — пересечённые границы: от 30 июня до 1 июля это 1 месяц. date_trunc('month', d)возвращает метку времени, а не дату. Приводите результат к DATE.
Шпаргалка: функции даты в PostgreSQL и DuckDB
Таблица отвечает на вопрос «как это пишется». Разделы ниже показывают, что функция возвращает на настоящих данных и где ошибается.
| Задача | PostgreSQL | DuckDB |
|---|---|---|
| Текущая дата | current_date | current_date |
| Текущий момент | now(), current_timestamp | now(), current_timestamp |
| Год, месяц, день | extract(month FROM d) | extract(month FROM d), month(d) |
| День недели, пн = 1 | extract(isodow FROM d) | extract(isodow FROM d), isodow(d) |
| Название дня | to_char(d, 'Day') | dayname(d) |
| Начало месяца | date_trunc('month', d)::date | CAST(date_trunc('month', d) AS DATE) |
| Последний день месяца | (date_trunc('month', d) + INTERVAL '1 month - 1 day')::date | last_day(d) |
| Разница в днях | d2 - d1 | d2 - d1, date_diff('day', d1, d2) |
| Разница в часах | extract(epoch FROM t2 - t1) / 3600 | extract(epoch FROM t2 - t1) / 3600, date_diff('hour', t1, t2) |
| Полные месяцы | age(d2, d1) | age(d2, d1) |
| Прибавить дни | d + 7 | d + 7 |
| Прибавить месяц | d + INTERVAL '1 month' | d + INTERVAL '1 month' |
| Дата в строку | to_char(d, 'DD.MM.YYYY') | strftime(d, '%d.%m.%Y') |
| Строка в дату | to_date(s, 'DD.MM.YYYY') | strptime(s, '%d.%m.%Y') |
| Дата из частей | make_date(2026, 8, 1) | make_date(2026, 8, 1) |
Как получить текущую дату и время в SQL
current_date возвращает дату, now() и current_timestamp — момент времени с часовым поясом, localtimestamp — тот же момент без пояса. Названия одинаковые в PostgreSQL и DuckDB, типы тоже: date, timestamp with time zone и timestamp.
«Сегодня» считается в часовом поясе сессии, а не сервера и не вашем. Мы проверили это в одну и ту же минуту. PostgreSQL с настройкой TimeZone = UTC вернул current_date 23 сентября 2026 года, после SET TIME ZONE 'Europe/Moscow' — 24 сентября. Отчёт «за вчера», запущенный между полуночью и тремя часами ночи по Москве из сессии в UTC, покажет позавчера.
У учебной базы своё «сегодня». Это снимок: регистрации с 1 июня по 29 августа 2026 года, выгрузка событий закрыта 30 августа в 12:59. Если в запросе к такой базе написать current_date, окно «последние 7 дней» окажется пустым. В аналитике на снимках берите дату отсечки из самих данных: max(event_time) или явную дату в параметре.
SELECT
current_date AS today,
now() AS now_with_tz,
localtimestamp AS now_local,
(SELECT max(event_time) FROM events) AS data_until;
-- data_until: 2026-08-30 12:59:00
-- today зависит от часового пояса сессииКак достать из даты год, месяц и день недели: EXTRACT
extract(поле FROM дата) — стандартная запись, она работает в обеих базах. Поля, которые нужны чаще всего: year, quarter, month, day, doy (день года), hour, week (номер ISO-недели), dow и isodow. date_part('month', d) делает то же самое. В DuckDB есть и короткие функции: year(d), month(d), dayname(d). В PostgreSQL их нет, dayname там падает с ошибкой function dayname(date) does not exist.
С днём недели легко ошибиться. dow нумерует дни с воскресенья: воскресенье 0, суббота 6. isodow — с понедельника: понедельник 1, воскресенье 7. Условие «выходные» через dow IN (0, 6) и через isodow IN (6, 7) даёт одно и то же, но смешение двух нумераций в одном запросе превращает воскресенье в понедельник. Пишите isodow: его правило совпадает с календарём.
Тип результата тоже различается. В PostgreSQL extract возвращает numeric, а date_part — double precision. Для группировки это неважно, для склейки в строку и сравнения с текстом — важно.
На учебной базе разбивка по дню недели показывает форму, которую не видно в месячных числах: в будни регистрируются от 763 до 828 человек, в субботу 350, в воскресенье 283. Продукт рабочий, и выходные дают только 13,7% регистраций — 633 из 4 613.
Посчитано на учебной базе SQL-курса через extract(isodow FROM signup_date). Выходные — 633 регистрации из 4 613.
SELECT
extract(isodow FROM signup_date) AS iso_dow,
count(*) AS signups
FROM users
GROUP BY iso_dow
ORDER BY iso_dow;
-- 1: 763, 2: 773, 3: 819, 4: 828, 5: 797, 6: 350, 7: 283Как округлить дату до месяца или недели: date_trunc
date_trunc('month', d) отбрасывает всё мельче месяца и возвращает его начало. Это основной способ группировать по периоду: регистрации в учебной базе растут от 1 042 в июне до 1 676 в июле и 1 895 в августе.
Ловушка в типе. От даты date_trunc в PostgreSQL возвращает timestamp with time zone: дата сначала превращается в полночь в часовом поясе сессии. В UTC результат выглядит как 2026-08-01 00:00:00+00, в Москве — 2026-08-01 00:00:00+03. DuckDB возвращает TIMESTAMP без пояса. В обеих базах это не дата, и при соединении с календарём, сравнении с колонкой DATE или выгрузке в BI такое значение ведёт себя иначе, чем вы ждёте. Приводите результат к дате сразу: CAST(date_trunc('month', d) AS DATE).
Последний день месяца в DuckDB возвращает last_day(d). В PostgreSQL такой функции нет, запрос падает с function last_day(date) does not exist. Там конец месяца собирают из начала: (date_trunc('month', d) + INTERVAL '1 month - 1 day')::date для 10 февраля 2026 года даёт 28 февраля. Для фильтров конец месяца обычно не нужен вовсе: полуоткрытый интервал «от первого числа до первого числа следующего месяца» надёжнее.
Неделя в date_trunc('week', …) начинается в понедельник, и последняя неделя месяца при фильтре по месяцу выходит обрезанной. Об этом и о неделе с воскресенья — в статье про периоды.
SELECT
CAST(date_trunc('month', signup_date) AS DATE) AS month,
count(*) AS signups
FROM users
GROUP BY month
ORDER BY month;
-- 2026-06-01: 1042, 2026-07-01: 1676, 2026-08-01: 1895Как посчитать разницу между датами в днях
Для двух колонок типа DATE разница — простое вычитание: paid_at - signup_date. PostgreSQL возвращает integer, DuckDB — BIGINT, число дней в обоих случаях. Функции datediff в PostgreSQL нет: datediff('day', …) падает с function datediff(unknown, date, date) does not exist, а запись в стиле SQL Server DATEDIFF(day, …) — с column "day" does not exist, потому что day без кавычек читается как колонка.
Пример на учебной базе: сколько дней проходит от регистрации до первой оплаты. Первую оплату берём как min(paid_at) на пользователя, иначе продления удлинят срок. Плательщиков 912, самый быстрый платит на третий день, самый долгий — на двадцатый, медиана — 11 дней.
Медиана здесь честнее среднего: среднее 11,1 почти совпадает, но на данных с длинным хвостом они расходятся сильно. percentile_cont(0.5) WITHIN GROUP (ORDER BY …) работает и в PostgreSQL, и в DuckDB.
WITH first_pay AS (
SELECT user_id, min(paid_at) AS first_paid_at
FROM payments
GROUP BY user_id
)
SELECT
count(*) AS payers,
min(f.first_paid_at - u.signup_date) AS min_days,
percentile_cont(0.5) WITHIN GROUP (ORDER BY f.first_paid_at - u.signup_date) AS median_days,
max(f.first_paid_at - u.signup_date) AS max_days
FROM first_pay f
JOIN users u ON u.user_id = f.user_id;
-- 912 плательщиков: 3, 11, 20 днейРазница в часах и минутах: интервал и epoch
Разность двух меток времени — это не число, а интервал: 2 days 01:00:00. Чтобы получить часы, интервал переводят в секунды через extract(epoch FROM …) и делят на 3 600. Для минут — на 60.
Популярная ошибка — date_part('hour', t2 - t1). Она возвращает не длительность в часах, а часовое поле интервала. Для разницы между 29 августа 09:00 и 31 августа 10:00 это 1, хотя прошло 49 часов. PostgreSQL и DuckDB здесь ведут себя одинаково.
У DuckDB есть ещё date_diff('hour', t1, t2), и он считает не длительность, а число пересечённых границ часа. Между 09:59 и 10:01 прошло две минуты, а date_diff('hour', …) вернёт 1. Так же устроены dateDiff в ClickHouse и DATEDIFF в SQL Server. Для длительности используйте epoch.
На учебной базе так считается пауза между соседними событиями одного пользователя. Медиана — 47 часов, 90-й перцентиль — 260 часов, почти 11 дней. Это двое суток между заходами у типичного пользователя и длинный хвост тех, кто возвращается раз в полторы недели.
| Выражение | Результат | Что это |
|---|---|---|
| t2 - t1 | 2 days 01:00:00 | интервал |
| extract(epoch FROM t2 - t1) / 3600 | 49 | длительность в часах |
| date_part('hour', t2 - t1) | 1 | часовое поле интервала |
WITH ordered AS (
SELECT
user_id,
event_time,
lag(event_time) OVER (PARTITION BY user_id ORDER BY event_time, event_id) AS prev_time
FROM events
)
SELECT
count(*) AS gaps,
percentile_cont(0.5) WITHIN GROUP (ORDER BY extract(epoch FROM event_time - prev_time) / 3600) AS median_hours,
percentile_cont(0.9) WITHIN GROUP (ORDER BY extract(epoch FROM event_time - prev_time) / 3600) AS p90_hours
FROM ordered
WHERE prev_time IS NOT NULL;
-- 30728 пауз, медиана 47, p90 260Как посчитать разницу в месяцах: age и date_diff
Месяцы разной длины, поэтому «сколько месяцев между датами» — вопрос определения. Базы отвечают на него двумя способами.
age(d2, d1) в PostgreSQL и DuckDB возвращает интервал в календарных единицах: от 1 июня до 30 августа 2026 года — 2 mons 29 days. Полные месяцы из него достают так: extract(year FROM age(…)) * 12 + extract(month FROM age(…)), для этого примера 2. Так считают возраст подписки или стаж клиента.
date_diff('month', d1, d2) в DuckDB считает пересечённые границы месяцев. От 30 июня до 1 июля прошёл один день, а результат — 1 месяц. age для той же пары вернёт 1 day. Для когорт «месяц регистрации → месяц оплаты» границы — именно то, что нужно. Для «сколько месяцев клиент с нами» — нет.
В PostgreSQL date_diff нет: function date_diff(unknown, date, date) does not exist. Если запрос переезжает из DuckDB, месячную разницу на границах пишут через date_trunc и age или через разность extract(year) * 12 + extract(month).
SELECT
age(DATE '2026-08-30', DATE '2026-06-01') AS age_interval, -- 2 mons 29 days
extract(year FROM age(DATE '2026-08-30', DATE '2026-06-01')) * 12
+ extract(month FROM age(DATE '2026-08-30', DATE '2026-06-01')) AS full_months; -- 2Как прибавить к дате дни, недели или месяц
К дате можно прибавить целое число дней: signup_date + 14 — это дата через две недели, тип остаётся DATE в обеих базах. С интервалом тип меняется: DATE '2026-08-23' + INTERVAL '7 days' возвращает метку времени 2026-08-30 00:00:00. Для сравнения с timestamp-колонкой это удобно, для соединения с колонкой DATE — нет.
С месяцами арифметика несимметрична. 31 января плюс месяц — 28 февраля 2026 года. 28 февраля плюс месяц — 28 марта, а не 31-е. Два шага по месяцу от 31 января приводят к 28 марта, а шаг на INTERVAL '2 months' — к 31 марта. Для «конца следующего месяца» считайте от начала месяца, а не от текущей даты.
На учебной базе окно из интервала отвечает на вопрос, какая часть плательщиков платит в первые две недели. 583 из 912, то есть 63,9%, оплатили раньше, чем через 14 дней после регистрации. Это сопоставимо с медианой в 11 дней из раздела выше.
| Выражение | Результат | Тип |
|---|---|---|
| DATE '2026-08-23' + 7 | 2026-08-30 | date |
| DATE '2026-08-23' + INTERVAL '7 days' | 2026-08-30 00:00:00 | timestamp |
| DATE '2026-01-31' + INTERVAL '1 month' | 2026-02-28 00:00:00 | timestamp |
| DATE '2026-02-28' + INTERVAL '1 month' | 2026-03-28 00:00:00 | timestamp |
| DATE '2026-03-31' - INTERVAL '1 month' | 2026-02-28 00:00:00 | timestamp |
WITH first_pay AS (
SELECT user_id, min(paid_at) AS first_paid_at
FROM payments
GROUP BY user_id
)
SELECT
count(*) AS payers,
count(*) FILTER (WHERE f.first_paid_at < u.signup_date + 14) AS paid_within_14_days
FROM first_pay f
JOIN users u ON u.user_id = f.user_id;
-- 912 и 583Формат даты: вывести как 03.08.2026 и прочитать из строки
Здесь базы расходятся сильнее всего. PostgreSQL форматирует через to_char(d, 'DD.MM.YYYY') и читает строку через to_date(s, 'DD.MM.YYYY'). DuckDB использует шаблоны в стиле C: strftime(d, '%d.%m.%Y') и strptime(s, '%d.%m.%Y'). Чужая функция в каждой из баз падает: to_char в DuckDB — Scalar Function with name to_char does not exist!, strftime в PostgreSQL — function strftime(date, unknown) does not exist.
Названия месяцев и дней зависят от языка. to_char(d, 'Month') в PostgreSQL даёт английское название, а с префиксом TM — название по настройке lc_time. На сервере с lc_time = en_US.utf8 и TMMonth вернёт August. DuckDB отдаёт английские названия всегда.
strptime в DuckDB возвращает TIMESTAMP, to_date в PostgreSQL — DATE. Если результат идёт в соединение с колонкой DATE, приведите его явно.
Практический совет: форматируйте даты в BI или в коде отчёта, а в SQL держите их типом DATE. Строка 03.08.2026 не сортируется как дата и не участвует в арифметике. Про то, как строка 03.08.2026 без явного формата становится 8 марта, — в статье про типы данных.
| Задача | PostgreSQL | DuckDB | Результат |
|---|---|---|---|
| Дата в строку | to_char(DATE '2026-08-03', 'DD.MM.YYYY') | strftime(DATE '2026-08-03', '%d.%m.%Y') | 03.08.2026 |
| Дата и время | to_char(ts, 'YYYY-MM-DD HH24:MI') | strftime(ts, '%Y-%m-%d %H:%M') | 2026-08-03 14:05 |
| Строка в дату | to_date('03.08.2026', 'DD.MM.YYYY') | strptime('03.08.2026', '%d.%m.%Y') | date и timestamp |
Номер недели и год: 1 января 2027 года — это 53-я неделя
extract(week FROM d) возвращает номер ISO-недели. Неделя принадлежит тому году, на который приходится её четверг, поэтому первые дни января иногда попадают в последнюю неделю прошлого года. 1 января 2027 года — пятница: extract(week …) даёт 53, extract(year …) — 2027, а extract(isoyear …) — 2026.
Если группировать по паре «year и week», 1 января 2027 года окажется в несуществующей 53-й неделе 2027 года, отдельно от остальных дней той же недели. Для номера недели всегда берите isoyear, а для группировки лучше дата начала недели из date_trunc('week', d): у неё нет такой проблемы.
Функции даты в MySQL, ClickHouse и SQL Server
Таблица составлена по официальной документации MySQL 8.4, ClickHouse и SQL Server. Главное, что стоит запомнить: DATEDIFF в MySQL возвращает дни и принимает сначала конечную дату, а DATEDIFF в SQL Server и dateDiff в ClickHouse принимают единицу, начальную и конечную дату и считают пересечённые границы.
| Задача | MySQL | ClickHouse | SQL Server |
|---|---|---|---|
| Текущая дата | CURDATE() | today() | CAST(GETDATE() AS date) |
| Начало месяца | DATE_TRUNC нет; DATE_FORMAT(d, '%Y-%m-01') даёт строку | toStartOfMonth(d), date_trunc('month', d) | DATETRUNC(month, d), с SQL Server 2022 |
| Последний день месяца | LAST_DAY(d) | toLastDayOfMonth(d) | EOMONTH(d) |
| Разница в днях | DATEDIFF(конец, начало), время отбрасывается | dateDiff('day', начало, конец) | DATEDIFF(day, начало, конец) |
| Разница в других единицах | TIMESTAMPDIFF(unit, начало, конец) | dateDiff('hour', начало, конец) | DATEDIFF(hour, начало, конец) |
| Прибавить дни | DATE_ADD(d, INTERVAL 7 DAY) | addDays(d, 7) | DATEADD(day, 7, d) |
| День недели | DAYOFWEEK(d): 1 = воскресенье | toDayOfWeek(d): 1 = понедельник | DATEPART(weekday, d) зависит от SET DATEFIRST |
| Дата в строку | DATE_FORMAT(d, '%d.%m.%Y') | formatDateTime(d, '%d.%m.%Y') | FORMAT(d, ...) |
| Строка в дату | STR_TO_DATE(s, '%d.%m.%Y') | parseDateTime(s, формат) | CAST или CONVERT |
Частые вопросы
Как получить текущую дату в SQL? В PostgreSQL, DuckDB и ClickHouse — current_date (в ClickHouse ещё today()), в MySQL — CURDATE(), в SQL Server — CAST(GETDATE() AS date). Помните о часовом поясе сессии.
Как вывести месяц из даты? Номер — extract(month FROM d). Начало месяца для группировки — CAST(date_trunc('month', d) AS DATE). Название — to_char(d, 'TMMonth') в PostgreSQL или monthname(d) в DuckDB.
Как посчитать разницу между датами? Для DATE — вычитание, результат в днях. Для timestamp — extract(epoch FROM t2 - t1) в секундах. Для месяцев — age с полными месяцами или date_diff с границами, в зависимости от вопроса.
Как выбрать данные за текущий месяц? WHERE d >= date_trunc('month', current_date) AND d < date_trunc('month', current_date) + INTERVAL '1 month'. Не extract(month FROM d) = extract(month FROM current_date): такое условие захватит тот же месяц прошлого года и не даст базе использовать индекс.
Практика на учебной базе
Три задачи для песочницы курса. Числа должны совпасть до единицы.
Первая. Посчитайте медиану дней от регистрации до первой оплаты отдельно для каждого тарифа первой оплаты. Подсказка: тариф первого платежа достаётся через min_by(plan, paid_at) в DuckDB. Ответ: basic и pro — 11 дней, team — 9,5.
Вторая. Какая доля событий приходится на время с 9:00 до 17:59? Подсказка: extract(hour FROM event_time) BETWEEN 9 AND 17. Ответ: 27 345 из 35 341, 77,4%.
Третья. Сколько пользователей, зарегистрированных по 22 августа, открыли продукт ровно на седьмой день после регистрации? Подсказка: сравните CAST(event_time AS DATE) с signup_date + 7. Ответ: 881 из 4 109.
Итог
Функции даты почти не падают, они возвращают правдоподобный ответ не того типа или не той единицы. date_trunc отдаёт метку времени вместо даты, date_part от интервала — одно поле вместо длительности, date_diff — границы вместо прошедшего времени, current_date — дату в чужом часовом поясе. Перед тем как отдавать расчёт, проверьте тип результата и одну пару дат руками.
Даты и интервалы в SQL-курсе разобраны в десятой главе: срок до оплаты, выручка по неделям и границы периода на заданиях с автоматической проверкой. Первые главы курса открыты бесплатно.
Материалы по теме
CASE WHEN в SQL: сегменты, условные метрики и порядок условий
CASE WHEN в SQL на учебной базе продукта: синтаксис, сегменты пользователей, почему порядок WHEN меняет ответ, ELSE и NULL, CASE внутри COUNT, в GROUP BY и ORDER BY.

Типы данных в SQL и CAST: почему конверсия равна нулю и сумма не сходится
Типы данных в SQL на реальной учебной базе: целочисленное деление в PostgreSQL и DuckDB, DECIMAL против DOUBLE для денег, round и boolean, текст в число и дату, BETWEEN по timestamp и приведение типа в WHERE, которое выключает индекс.
Что такое SQL простыми словами: реляционная база, таблицы и запросы
Что такое SQL и реляционная база данных: таблица, строка и колонка, первый запрос с результатом, JOIN, команды языка, СУБД и диалекты, что SQL умеет и чего не умеет.