SQLLab
Все статьи

Функции для работы с датами в SQL (PostgreSQL)

Работа с датами в SQL: DATE_TRUNC, EXTRACT, INTERVAL, AGE, NOW, TO_CHAR. Примеры для аналитики — метрики по периодам, разница дат, форматирование.

18 марта 2026 г.·6 мин чтения·

Даты — одна из самых частых тем в аналитических SQL-запросах. Метрики по дням/месяцам, возраст пользователя, дни до дедлайна — всё это требует работы с датами. В этой статье — всё что нужно знать для работы с датами в PostgreSQL.

Типы данных для дат

-- DATE — только дата (год, месяц, день)
SELECT '2026-03-15'::DATE;

-- TIMESTAMP — дата и время без часового пояса
SELECT '2026-03-15 14:30:00'::TIMESTAMP;

-- TIMESTAMPTZ — дата и время с часовым поясом (рекомендуется)
SELECT NOW();  -- текущее время с TZ

-- INTERVAL — промежуток времени
SELECT INTERVAL '7 days';
SELECT INTERVAL '1 month 15 days';

Текущая дата и время

-- Текущая дата (без времени)
SELECT CURRENT_DATE;          -- 2026-03-18
SELECT CURRENT_DATE::DATE;    -- то же самое

-- Текущее дата+время
SELECT NOW();                 -- 2026-03-18 14:30:00+03
SELECT CURRENT_TIMESTAMP;     -- аналог NOW()

-- Только время
SELECT CURRENT_TIME;

-- Начало дня (сегодня в 00:00)
SELECT DATE_TRUNC('day', NOW());

DATE_TRUNC: обрезка до периода

Самая полезная функция для аналитики. Обрезает дату до нужной точности.

SELECT DATE_TRUNC('year',    '2026-03-15 14:30'::TIMESTAMP); -- 2026-01-01 00:00
SELECT DATE_TRUNC('quarter', '2026-03-15 14:30'::TIMESTAMP); -- 2026-01-01 00:00
SELECT DATE_TRUNC('month',   '2026-03-15 14:30'::TIMESTAMP); -- 2026-03-01 00:00
SELECT DATE_TRUNC('week',    '2026-03-15 14:30'::TIMESTAMP); -- 2026-03-09 00:00 (пн)
SELECT DATE_TRUNC('day',     '2026-03-15 14:30'::TIMESTAMP); -- 2026-03-15 00:00
SELECT DATE_TRUNC('hour',    '2026-03-15 14:30'::TIMESTAMP); -- 2026-03-15 14:00

Классическое применение — метрики по периодам:

-- Выручка по месяцам
SELECT
  DATE_TRUNC('month', created_at)::DATE AS month,
  COUNT(*)                               AS orders,
  SUM(amount)                            AS revenue
FROM orders
WHERE status = 'paid'
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY month;

-- Активные пользователи по неделям (WAU)
SELECT
  DATE_TRUNC('week', event_date)::DATE AS week_start,
  COUNT(DISTINCT user_id)              AS wau
FROM events
GROUP BY DATE_TRUNC('week', event_date)
ORDER BY week_start;

EXTRACT: извлечение части даты

SELECT EXTRACT(year    FROM NOW());  -- 2026
SELECT EXTRACT(month   FROM NOW());  -- 3
SELECT EXTRACT(day     FROM NOW());  -- 18
SELECT EXTRACT(hour    FROM NOW());  -- 14
SELECT EXTRACT(dow     FROM NOW());  -- 0=воскресенье, 1=понедельник...6=суббота
SELECT EXTRACT(isodow  FROM NOW());  -- 1=пн...7=вс (ISO)
SELECT EXTRACT(week    FROM NOW());  -- номер недели ISO (1-53)
SELECT EXTRACT(quarter FROM NOW());  -- квартал (1-4)
SELECT EXTRACT(epoch   FROM NOW());  -- Unix timestamp (секунды с 1970-01-01)

Применение:

-- Продажи по дням недели (0=вс, 1=пн, ...)
SELECT
  EXTRACT(dow FROM created_at)::INT AS day_of_week,
  TO_CHAR(created_at, 'Day')        AS day_name,
  COUNT(*)                          AS orders,
  SUM(amount)                       AS revenue
FROM orders
GROUP BY EXTRACT(dow FROM created_at), TO_CHAR(created_at, 'Day')
ORDER BY day_of_week;

-- Только заказы в рабочее время (9-18)
SELECT * FROM orders
WHERE EXTRACT(hour FROM created_at) BETWEEN 9 AND 17
  AND EXTRACT(isodow FROM created_at) BETWEEN 1 AND 5;

INTERVAL: арифметика с датами

-- Прибавить/вычесть период
SELECT NOW() + INTERVAL '7 days';
SELECT NOW() - INTERVAL '1 month';
SELECT NOW() + INTERVAL '1 year 3 months 15 days';

-- Пользователи, зарегистрированные за последние 30 дней
SELECT * FROM users
WHERE created_at >= NOW() - INTERVAL '30 days';

-- Заказы за прошлый месяц
SELECT * FROM orders
WHERE created_at >= DATE_TRUNC('month', NOW()) - INTERVAL '1 month'
  AND created_at <  DATE_TRUNC('month', NOW());

-- Пользователи, не заходившие 90+ дней (риск оттока)
SELECT user_id, last_login
FROM users
WHERE last_login < NOW() - INTERVAL '90 days'
  AND is_active = true;

AGE: разница между датами

-- Возраст пользователя
SELECT
  user_id,
  birth_date,
  AGE(NOW(), birth_date::TIMESTAMP) AS age,
  EXTRACT(year FROM AGE(NOW(), birth_date::TIMESTAMP)) AS age_years
FROM users;

-- Сколько прошло с регистрации
SELECT
  user_id,
  created_at,
  AGE(NOW(), created_at) AS account_age
FROM users;

Разница через вычитание (проще для дней):

-- Количество дней с регистрации
SELECT
  user_id,
  CURRENT_DATE - created_at::DATE AS days_since_registration
FROM users;

-- Дней до конца подписки
SELECT
  user_id,
  subscription_end::DATE - CURRENT_DATE AS days_left
FROM subscriptions
WHERE subscription_end > NOW();

TO_CHAR: форматирование даты

SELECT TO_CHAR(NOW(), 'DD.MM.YYYY');          -- 18.03.2026
SELECT TO_CHAR(NOW(), 'DD Month YYYY');        -- 18 March 2026
SELECT TO_CHAR(NOW(), 'YYYY-MM-DD HH24:MI');   -- 2026-03-18 14:30
SELECT TO_CHAR(NOW(), 'Day, DD Mon YYYY');      -- Wednesday, 18 Mar 2026
SELECT TO_CHAR(NOW(), 'Q');                    -- 1 (квартал)
SELECT TO_CHAR(NOW(), 'IW');                   -- 12 (неделя ISO)

TO_DATE / TO_TIMESTAMP: парсинг строк

-- Строка в дату
SELECT TO_DATE('15.03.2026', 'DD.MM.YYYY');
SELECT TO_DATE('March 15, 2026', 'Month DD, YYYY');

-- Строка в timestamp
SELECT TO_TIMESTAMP('15.03.2026 14:30', 'DD.MM.YYYY HH24:MI');

Типичные задачи в аналитике

Когортный анализ: когда зарегистрировался и когда сделал первый заказ

SELECT
  DATE_TRUNC('month', u.created_at)::DATE         AS cohort,
  DATE_TRUNC('month', MIN(o.created_at))::DATE     AS first_order_month,
  EXTRACT(month FROM AGE(
    DATE_TRUNC('month', MIN(o.created_at)),
    DATE_TRUNC('month', u.created_at)
  ))::INT                                          AS months_to_convert,
  COUNT(DISTINCT u.id)                             AS users
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY DATE_TRUNC('month', u.created_at), u.id
ORDER BY cohort;

Заполнить пропущенные даты нулями

-- Генерация ряда дат через generate_series
SELECT
  d::DATE AS date,
  COALESCE(r.revenue, 0) AS revenue
FROM generate_series(
  '2026-01-01'::DATE,
  '2026-03-18'::DATE,
  '1 day'::INTERVAL
) AS d
LEFT JOIN (
  SELECT created_at::DATE AS date, SUM(amount) AS revenue
  FROM orders
  GROUP BY created_at::DATE
) r ON r.date = d::DATE
ORDER BY d;

Скользящее среднее за 7 дней

SELECT
  order_date,
  revenue,
  AVG(revenue) OVER (
    ORDER BY order_date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ) AS rolling_7d_avg
FROM daily_revenue
ORDER BY order_date;

Попробуй сам: все три запроса выше можно написать и проверить прямо в браузере — в тренажёре SQL Lab есть датасеты с реальными полями created_at и order_date. Никакой установки, работает сразу.


Шпаргалка по функциям дат

ЗадачаФункция / Синтаксис
СегодняCURRENT_DATE
СейчасNOW()
Начало месяцаDATE_TRUNC('month', NOW())
Прошлый месяцDATE_TRUNC('month', NOW()) - INTERVAL '1 month'
Год из датыEXTRACT(year FROM date_col)
Разница в дняхdate1::DATE - date2::DATE
ФорматироватьTO_CHAR(date_col, 'DD.MM.YYYY')
Ряд датgenerate_series(start, end, '1 day')
Возраст в годахEXTRACT(year FROM AGE(NOW(), birth_date))

Что дальше

Функции дат — один из самых частых запросов на SQL-собеседованиях. Чтобы запомнить их не зазубриванием, а через практику:

Отработай на реальных задачах:

  • Тренажёр SQL — задачи с датасетами orders, users, events. Можно сразу писать DATE_TRUNC, EXTRACT, AGE и видеть результат.
  • Курсы SQL — если хочешь пройти темы системно, от основ до оконных функций.
  • Подготовка к собеседованию — вопросы по датам встречаются в блоке «Аналитические функции».

Попробуй написать запрос прямо сейчас — открыть тренажёр →

Похожие статьи

Попробуй на практике

Тренажёр с реальными задачами — бесплатно и без регистрации

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