Все материалы
Продуктовая аналитикагайдстарт

Функции даты в SQL: текущая дата, разница дат, EXTRACT и формат

Справочник функций даты в SQL на учебной базе: текущая дата, часть даты, date_trunc, разница дат в днях, часах и месяцах, интервалы, формат вывода и отличия СУБД.

КПКейсПрактика23 сентября 2026 г.15 мин

Функции даты ищут в момент, когда запрос уже почти написан: нужно достать месяц, посчитать дни до оплаты, прибавить неделю или вывести дату как 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 16 и DuckDB
ЗадачаPostgreSQLDuckDB
Текущая датаcurrent_datecurrent_date
Текущий моментnow(), current_timestampnow(), current_timestamp
Год, месяц, деньextract(month FROM d)extract(month FROM d), month(d)
День недели, пн = 1extract(isodow FROM d)extract(isodow FROM d), isodow(d)
Название дняto_char(d, 'Day')dayname(d)
Начало месяцаdate_trunc('month', d)::dateCAST(date_trunc('month', d) AS DATE)
Последний день месяца(date_trunc('month', d) + INTERVAL '1 month - 1 day')::datelast_day(d)
Разница в дняхd2 - d1d2 - d1, date_diff('day', d1, d2)
Разница в часахextract(epoch FROM t2 - t1) / 3600extract(epoch FROM t2 - t1) / 3600, date_diff('hour', t1, t2)
Полные месяцыage(d2, d1)age(d2, d1)
Прибавить дниd + 7d + 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.

Регистрации по дням недели, 1 июня – 29 августа 2026

Посчитано на учебной базе SQL-курса через extract(isodow FROM signup_date). Выходные — 633 регистрации из 4 613.

Регистрации
Регистрации по дню недели, понедельник = 1
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 дней. Это двое суток между заходами у типичного пользователя и длинный хвост тех, кто возвращается раз в полторы недели.

Разница между 2026-08-29 09:00 и 2026-08-31 10:00
ВыражениеРезультатЧто это
t2 - t12 days 01:00:00интервал
extract(epoch FROM t2 - t1) / 360049длительность в часах
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).

Полные месяцы через age: PostgreSQL и DuckDB
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 дней из раздела выше.

Сложение дат с числом и интервалом, PostgreSQL 16 и DuckDB
ВыражениеРезультатТип
DATE '2026-08-23' + 72026-08-30date
DATE '2026-08-23' + INTERVAL '7 days'2026-08-30 00:00:00timestamp
DATE '2026-01-31' + INTERVAL '1 month'2026-02-28 00:00:00timestamp
DATE '2026-02-28' + INTERVAL '1 month'2026-03-28 00:00:00timestamp
DATE '2026-03-31' - INTERVAL '1 month'2026-02-28 00:00:00timestamp
Плательщики, оплатившие в первые 14 дней
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 марта, — в статье про типы данных.

Формат и разбор строки
ЗадачаPostgreSQLDuckDBРезультат
Дата в строку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 принимают единицу, начальную и конечную дату и считают пересечённые границы.

Те же задачи в других СУБД
ЗадачаMySQLClickHouseSQL 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-курсе разобраны в десятой главе: срок до оплаты, выручка по неделям и границы периода на заданиях с автоматической проверкой. Первые главы курса открыты бесплатно.

Продолжить чтение
Вся библиотека
Продуктовая аналитика3 августа 2026 г.13 мин
Квадрат проходит через прорезь и превращается в круг.

Типы данных в SQL и CAST: почему конверсия равна нулю и сумма не сходится

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

Читать материал
Продуктовая аналитика23 сентября 2026 г.13 мин

Что такое SQL простыми словами: реляционная база, таблицы и запросы

Что такое SQL и реляционная база данных: таблица, строка и колонка, первый запрос с результатом, JOIN, команды языка, СУБД и диалекты, что SQL умеет и чего не умеет.

Читать материал