sqlpostgresqlmysqldates

NOW, CURRENT_DATE y aritmetica con INTERVAL en SQL

Como filtrar filas de los ultimos 7 dias y del mes actual con NOW(), CURRENT_DATE y aritmetica de INTERVAL en PostgreSQL, MySQL y ClickHouse.

9 min de lecturaReferencesql · postgresql · mysql · dates · interval · clickhouse
Este artículo está actualmente en ruso — la traducción está en curso.

Фильтры по датам в SQL кажутся простыми только на первый взгляд.

Написал условие, выбрал нужный период — и вроде всё должно работать. Но на практике именно с датами часто появляются странные ошибки:

  • отчёт «за сегодня» показывает пустой результат;
  • «последние 7 дней» считаются не так, как ожидал бизнес;
  • текущий месяц захватывает лишние данные;
  • запрос внезапно перестаёт использовать индекс;
  • цифры в базе не совпадают с цифрами в дашборде.

Обычно проблема не в SQL как таковом, а в том, что разработчик не различает несколько важных вещей:

  • текущий момент времени;
  • текущую дату;
  • скользящий период;
  • календарный период;
  • включительную и исключающую границу фильтра.

В этой статье разберём, чем отличаются NOW(), CURRENT_DATE и INTERVAL, как писать фильтры по датам правильно и почему лучше сравнивать колонку с границами периода, а не оборачивать её в функции.

NOW() и CURRENT_DATE: в чём разница

В PostgreSQL есть несколько способов получить текущую дату и время. Самые частые — это NOW() и CURRENT_DATE.

Они похожи по смыслу, но возвращают разные вещи.

NOW() возвращает текущий момент времени: дату, часы, минуты, секунды и часовой пояс.

SELECT NOW();

Пример результата:

2026-06-17 14:30:00+00

CURRENT_DATE возвращает только текущую дату, без часов, минут и секунд.

SELECT CURRENT_DATE;

Пример результата:

2026-06-17

Можно посмотреть их вместе:

SELECT
  NOW() AS current_moment,
  CURRENT_DATE AS today,
  CURRENT_TIME AS current_time;

Результат может быть таким:

current_moment          | today      | current_time
------------------------+------------+---------------
2026-06-17 14:30:00+00  | 2026-06-17 | 14:30:00+00

Главная разница простая:

NOW() — это прямо сейчас, с точным временем. CURRENT_DATE — это сегодняшняя дата, как календарный день.

Эта разница особенно важна в фильтрах.

Почему created_at >= NOW() не означает «за сегодня»

Допустим, у нас есть таблица пользователей:

id | email           | created_at
---+-----------------+---------------------
1  | a@example.com   | 2026-06-17 09:15:00
2  | b@example.com   | 2026-06-17 12:40:00
3  | c@example.com   | 2026-06-16 18:20:00

Сейчас:

2026-06-17 14:30:00

Новичок может написать такой запрос, чтобы получить пользователей за сегодня:

SELECT id, email, created_at
FROM users
WHERE created_at >= NOW();

Но такой запрос почти всегда вернёт пустой результат.

Почему?

Потому что NOW() — это не начало сегодняшнего дня. Это текущий момент: 2026-06-17 14:30:00.

Условие:

created_at >= NOW()

означает:

покажи строки, созданные не раньше 14:30:00 сегодняшнего дня

То есть пользователи, которые зарегистрировались утром или днём до 14:30, в результат не попадут.

Для отчёта «за сегодня» обычно нужно другое условие:

SELECT id, email, created_at
FROM users
WHERE created_at >= CURRENT_DATE;

CURRENT_DATE — это сегодняшняя дата без времени. При сравнении с timestamp она фактически воспринимается как начало дня:

2026-06-17 00:00:00

То есть условие:

created_at >= CURRENT_DATE

означает:

покажи строки с начала сегодняшнего дня

Это уже гораздо ближе к тому, что обычно имеют в виду под фразой «данные за сегодня».

Правильный фильтр «за сегодня»

Самый аккуратный вариант фильтра за сегодня — указать и нижнюю, и верхнюю границу:

SELECT id, email, created_at
FROM users
WHERE created_at >= CURRENT_DATE
  AND created_at <  CURRENT_DATE + INTERVAL '1 day';

Такой запрос означает:

начиная с сегодняшней полуночи
и строго до завтрашней полуночи

Например, если сегодня 17 июня 2026 года, то фильтр будет таким:

created_at >= 2026-06-17 00:00:00
created_at <  2026-06-18 00:00:00

Обратите внимание на важный момент: верхняя граница записана через <, а не через <=.

Это хороший стиль для работы с датами и временем.

Почему так лучше?

Потому что у времени может быть точность до секунд, миллисекунд или микросекунд. Если писать конец дня через 23:59:59, можно случайно потерять строки, которые были созданы в 23:59:59.500.

Поэтому удобнее мыслить периодами так:

нижняя граница включается, верхняя граница не включается.

То есть:

created_at >= period_start
AND created_at < next_period_start

Этот подход хорошо работает для дней, месяцев, лет и любых других периодов.

Что такое INTERVAL

INTERVAL — это длительность.

С его помощью можно прибавлять или вычитать из даты часы, дни, месяцы и годы.

Примеры:

SELECT NOW() - INTERVAL '1 hour';
SELECT NOW() - INTERVAL '7 days';
SELECT CURRENT_DATE + INTERVAL '1 day';
SELECT CURRENT_DATE + INTERVAL '1 month';

Можно использовать и составные интервалы:

SELECT NOW() - INTERVAL '1 day 6 hours';

Это значит:

текущий момент минус 1 день и 6 часов

INTERVAL очень часто используется в фильтрах:

WHERE created_at >= NOW() - INTERVAL '7 days'

Так мы говорим базе:

покажи строки, созданные за последние 7 суток

И вот здесь важно не перепутать два разных смысла.

Последние 7 дней и последние 7 суток — не всегда одно и то же

Фраза «последние 7 дней» может означать разные вещи.

Иногда имеют в виду последние ровно 7 суток от текущего момента.

Например, сейчас:

2026-06-17 14:30:00

Тогда последние 7 суток — это период:

с 2026-06-10 14:30:00
до 2026-06-17 14:30:00

Для такого сценария подходит NOW():

SELECT id, email, created_at
FROM users
WHERE created_at >= NOW() - INTERVAL '7 days'
ORDER BY created_at DESC;

Этот запрос отвечает на вопрос:

Что произошло за последние ровно 7 суток от текущего момента?

Но иногда бизнес имеет в виду другое:

Покажи данные за последние 7 календарных дней, начиная с полуночи.

Например:

с 2026-06-11 00:00:00
до 2026-06-18 00:00:00

Тогда лучше отталкиваться от CURRENT_DATE:

SELECT id, email, created_at
FROM users
WHERE created_at >= CURRENT_DATE - INTERVAL '6 days'
  AND created_at <  CURRENT_DATE + INTERVAL '1 day'
ORDER BY created_at DESC;

Почему 6 days, а не 7 days?

Потому что если сегодня 17 июня и мы хотим включить сегодняшний день, то 7 календарных дней — это:

11, 12, 13, 14, 15, 16, 17 июня

То есть сегодня плюс 6 предыдущих дней.

Главное — не запоминать формулу механически, а понимать смысл:

  • NOW() - INTERVAL '7 days' — последние 7 суток от текущей секунды;
  • CURRENT_DATE - INTERVAL '6 days' — календарные дни, начиная с полуночи.

Перед написанием запроса всегда полезно уточнить, какой именно вариант нужен.

Пример: выручка за последние 24 часа

Допустим, у нас есть таблица заказов:

id | amount | status | created_at
---+--------+--------+---------------------
1  | 1500   | paid   | 2026-06-17 10:15:00
2  | 2300   | paid   | 2026-06-16 20:40:00
3  | 900    | cancel | 2026-06-16 19:10:00
4  | 700    | paid   | 2026-06-15 12:00:00

Нужно посчитать выручку за последние 24 часа по статусам:

SELECT
  status,
  COUNT(*) AS orders_count,
  SUM(amount) AS revenue
FROM orders
WHERE created_at >= NOW() - INTERVAL '24 hours'
GROUP BY status;

Такой запрос берёт не календарный день, а именно последние 24 часа.

Если сейчас 2026-06-17 14:30:00, то нижняя граница будет:

2026-06-16 14:30:00

В отчёт попадут заказы, созданные после этого момента.

Это удобно для оперативных метрик:

  • сколько заказов пришло за последние 24 часа;
  • сколько ошибок произошло за последний час;
  • сколько пользователей было активно за последние 15 минут;
  • сколько платежей прошло за последние 7 суток.

Текущий месяц: почему это не «последние 30 дней»

С текущим месяцем похожая история.

Фраза «текущий месяц» не означает последние 30 дней. Она означает период с первого числа текущего месяца до первого числа следующего месяца.

Например, если сегодня 17 июня 2026 года, то текущий месяц — это:

с 2026-06-01 00:00:00
до 2026-07-01 00:00:00

Для такого фильтра удобно использовать date_trunc.

SELECT id, amount, created_at
FROM orders
WHERE created_at >= date_trunc('month', CURRENT_DATE)
  AND created_at <  date_trunc('month', CURRENT_DATE) + INTERVAL '1 month';

Что здесь происходит:

date_trunc('month', CURRENT_DATE)

возвращает начало текущего месяца.

Если сегодня 17 июня, результат будет:

2026-06-01 00:00:00

А эта часть:

date_trunc('month', CURRENT_DATE) + INTERVAL '1 month'

даёт начало следующего месяца:

2026-07-01 00:00:00

В итоге мы получаем аккуратный полуинтервал:

created_at >= 2026-06-01 00:00:00
created_at <  2026-07-01 00:00:00

Такой фильтр правильно работает для месяцев разной длины:

  • февраль может быть 28 или 29 дней;
  • апрель — 30 дней;
  • июль — 31 день.

Нам не нужно вручную думать, сколько дней в текущем месяце. База сама корректно прибавит один календарный месяц.

Почему не стоит фильтровать месяц через EXTRACT

Иногда можно увидеть такой запрос:

SELECT id, amount, created_at
FROM orders
WHERE EXTRACT(MONTH FROM created_at) = 6;

На первый взгляд он выбирает июнь. Но у него есть две проблемы.

Первая проблема: он выберет июнь любого года.

То есть в результат могут попасть данные за:

июнь 2024
июнь 2025
июнь 2026

Если не добавить проверку года, отчёт будет неверным.

Вторая проблема: мы применяем функцию к колонке created_at.

EXTRACT(MONTH FROM created_at)

Из-за этого базе сложнее использовать обычный индекс по created_at. В больших таблицах такой запрос может работать заметно медленнее.

Лучше писать через границы периода:

SELECT id, amount, created_at
FROM orders
WHERE created_at >= DATE '2026-06-01'
  AND created_at <  DATE '2026-07-01';

Или для текущего месяца:

SELECT id, amount, created_at
FROM orders
WHERE created_at >= date_trunc('month', CURRENT_DATE)
  AND created_at <  date_trunc('month', CURRENT_DATE) + INTERVAL '1 month';

Такой подход обычно и понятнее, и надёжнее, и дружелюбнее к индексам.

Главное правило для быстрых фильтров по датам

Старайтесь оставлять колонку слева «голой», без функций.

Плохо:

WHERE date_trunc('day', created_at) = CURRENT_DATE

Почему плохо?

Потому что база должна применить date_trunc к каждой строке таблицы, прежде чем понять, подходит строка или нет.

Лучше:

WHERE created_at >= CURRENT_DATE
  AND created_at <  CURRENT_DATE + INTERVAL '1 day'

Здесь колонка created_at остаётся как есть. Мы просто сравниваем её с двумя готовыми границами.

Это важный принцип:

Не превращайте каждую дату в таблице. Лучше заранее посчитайте границы периода и сравните с ними колонку.

На маленькой таблице разницы можно не заметить. Но на таблице с миллионами заказов, событий или логов это может быть разница между быстрым отчётом и тяжёлым запросом.

Фильтр за вчера

Ещё один частый сценарий — получить данные за вчерашний день.

Правильный вариант:

SELECT id, amount, created_at
FROM orders
WHERE created_at >= CURRENT_DATE - INTERVAL '1 day'
  AND created_at <  CURRENT_DATE;

Если сегодня 17 июня, то запрос выберет период:

с 2026-06-16 00:00:00
до 2026-06-17 00:00:00

То есть весь вчерашний день.

Не нужно писать так:

WHERE created_at::date = CURRENT_DATE - INTERVAL '1 day'

Или так:

WHERE date_trunc('day', created_at) = CURRENT_DATE - INTERVAL '1 day'

Такие варианты выглядят короче, но они обрабатывают саму колонку функцией. Для больших таблиц это часто хуже.

Фильтр за текущий год

Для текущего года логика такая же, как для месяца.

SELECT id, amount, created_at
FROM orders
WHERE created_at >= date_trunc('year', CURRENT_DATE)
  AND created_at <  date_trunc('year', CURRENT_DATE) + INTERVAL '1 year';

Если сегодня 17 июня 2026 года, то период будет таким:

с 2026-01-01 00:00:00
до 2027-01-01 00:00:00

Такой подход лучше, чем просто проверять:

EXTRACT(YEAR FROM created_at) = 2026

Потому что сравнение по диапазону обычно проще для чтения и лучше подходит для использования индекса по дате.

NOW() внутри транзакции

В PostgreSQL у NOW() есть важная особенность: значение фиксируется на момент начала транзакции.

То есть если транзакция началась в 14:30:00, то все вызовы NOW() внутри этой транзакции будут возвращать одно и то же время.

Пример:

BEGIN;

SELECT NOW();

-- some long-running work may happen here

SELECT NOW();

COMMIT;

Оба SELECT NOW() вернут одинаковое значение.

Это не ошибка. Наоборот, это полезная гарантия.

База как будто говорит:

В рамках одной транзакции мы смотрим на время одинаково.

Для бизнес-запросов это обычно хорошо. Если вы считаете отчёт, обновляете данные или проверяете условия, граница времени не будет «уплывать» во время выполнения операции.

Но иногда нужно получить именно реальное текущее время в момент вызова. Для этого в PostgreSQL есть clock_timestamp().

SELECT clock_timestamp();

В отличие от NOW(), эта функция показывает фактическое время на часах и может меняться при каждом вызове.

NOW(), statement_timestamp() и clock_timestamp()

В PostgreSQL есть несколько похожих функций, и их легко перепутать.

NOW() и CURRENT_TIMESTAMP возвращают время начала текущей транзакции.

SELECT NOW();
SELECT CURRENT_TIMESTAMP;

statement_timestamp() возвращает время начала текущего SQL-оператора.

SELECT statement_timestamp();

clock_timestamp() возвращает реальное текущее время на часах.

SELECT clock_timestamp();

Для обычных фильтров чаще всего нужен именно NOW().

Например:

WHERE created_at >= NOW() - INTERVAL '7 days'

Это стабильная граница. Она не изменится в середине транзакции.

clock_timestamp() чаще нужен для диагностики, логирования или измерения длительности отдельных шагов.

Для обычного отчёта «за последние 7 дней» почти всегда лучше использовать NOW(), а не clock_timestamp().

Часовые пояса: важный нюанс

Если в таблице поле created_at хранится с типом timestamptz, то часовой пояс может влиять на то, где именно начинается день.

Например, один и тот же момент времени может быть:

2026-06-17 22:30 UTC

А в другом часовом поясе это уже:

2026-06-18 01:30

То есть для одного отчёта событие относится к 17 июня, а для другого — к 18 июня.

Поэтому перед фильтрами «за день», «за месяц» или «за неделю» важно понимать:

В каком часовом поясе бизнес смотрит календарь?

Если отчёт должен строиться по московскому времени, сингапурскому времени или времени клиента, это нужно учитывать явно.

Пример для отчёта по московскому времени:

SELECT id, amount, created_at
FROM orders
WHERE created_at >= (CURRENT_DATE AT TIME ZONE 'Europe/Moscow')
  AND created_at <  ((CURRENT_DATE + INTERVAL '1 day') AT TIME ZONE 'Europe/Moscow');

В реальных проектах работу с часовыми поясами лучше согласовывать отдельно: как хранятся даты, в какой зоне строятся отчёты и что именно считается началом дня.

Но общее правило такое:

Сначала определите нужный часовой пояс, потом стройте календарные границы.

Отличия в MySQL

В MySQL идея такая же, но синтаксис немного отличается.

Аналог CURRENT_DATE — это CURDATE().

SELECT CURDATE();

Аналог текущего момента времени — NOW().

SELECT NOW();

Фильтр за последние 7 дней в MySQL:

SELECT id, email, created_at
FROM users
WHERE created_at >= NOW() - INTERVAL 7 DAY;

Обратите внимание на синтаксис:

INTERVAL 7 DAY

В MySQL число и единица интервала пишутся без кавычек, а единица обычно указывается в единственном числе:

INTERVAL 1 DAY
INTERVAL 7 DAY
INTERVAL 3 HOUR
INTERVAL 1 MONTH

Фильтр за сегодня:

SELECT id, email, created_at
FROM users
WHERE created_at >= CURDATE()
  AND created_at <  CURDATE() + INTERVAL 1 DAY;

Фильтр за текущий месяц:

SELECT id, amount, created_at
FROM orders
WHERE created_at >= DATE_FORMAT(CURDATE(), '%Y-%m-01')
  AND created_at <  DATE_FORMAT(CURDATE() + INTERVAL 1 MONTH, '%Y-%m-01');

В MySQL также есть важное отличие между NOW() и SYSDATE().

NOW() обычно возвращает стабильное время в рамках выполнения оператора. SYSDATE() возвращает фактическое текущее время в момент вызова.

В обычных бизнес-фильтрах чаще используют NOW().

Отличия в ClickHouse

В ClickHouse текущий момент можно получить через now():

SELECT now();

Фильтр за последние 7 дней можно написать так:

SELECT id, email, created_at
FROM users
WHERE created_at >= now() - INTERVAL 7 DAY;

Или через специальную функцию:

SELECT id, email, created_at
FROM users
WHERE created_at >= subtractDays(now(), 7);

Для начала месяца в ClickHouse часто используют toStartOfMonth:

SELECT id, amount, created_at
FROM orders
WHERE created_at >= toStartOfMonth(now())
  AND created_at <  addMonths(toStartOfMonth(now()), 1);

Для начала дня есть toStartOfDay:

SELECT id, amount, created_at
FROM orders
WHERE created_at >= toStartOfDay(now())
  AND created_at <  toStartOfDay(now()) + INTERVAL 1 DAY;

Общая идея остаётся такой же, как в PostgreSQL:

определяем начало периода, определяем начало следующего периода, фильтруем данные между ними.

Меняется только синтаксис функций.

Полезные шаблоны фильтров

За сегодня

WHERE created_at >= CURRENT_DATE
  AND created_at <  CURRENT_DATE + INTERVAL '1 day'

За вчера

WHERE created_at >= CURRENT_DATE - INTERVAL '1 day'
  AND created_at <  CURRENT_DATE

За последние 24 часа

WHERE created_at >= NOW() - INTERVAL '24 hours'

За последние 7 суток

WHERE created_at >= NOW() - INTERVAL '7 days'

За текущий месяц

WHERE created_at >= date_trunc('month', CURRENT_DATE)
  AND created_at <  date_trunc('month', CURRENT_DATE) + INTERVAL '1 month'

За текущий год

WHERE created_at >= date_trunc('year', CURRENT_DATE)
  AND created_at <  date_trunc('year', CURRENT_DATE) + INTERVAL '1 year'

Эти шаблоны полезно не просто копировать, а понимать. Почти везде используется одна и та же идея: нижняя граница включается, верхняя не включается.

Что важно запомнить

NOW() и CURRENT_DATE решают разные задачи.

NOW() нужен, когда важен текущий момент времени:

NOW() - INTERVAL '7 days'

Так мы получаем скользящее окно, например последние 7 суток.

CURRENT_DATE нужен, когда важен календарный день:

CURRENT_DATE

Так мы получаем начало сегодняшнего дня.

INTERVAL нужен, чтобы прибавлять или вычитать длительность:

INTERVAL '1 day'
INTERVAL '7 days'
INTERVAL '1 month'

Для календарных периодов лучше использовать полуинтервалы:

created_at >= period_start
AND created_at < next_period_start

И главное правило для хороших запросов:

Не оборачивайте колонку с датой в функцию, если можно сравнить её с готовыми границами.

Вместо этого:

WHERE date_trunc('day', created_at) = CURRENT_DATE

лучше писать так:

WHERE created_at >= CURRENT_DATE
  AND created_at <  CURRENT_DATE + INTERVAL '1 day'

Такой запрос обычно понятнее, безопаснее и лучше подходит для использования индекса.

Короткий вывод

Фильтры по датам в SQL строятся вокруг простой идеи: нужно чётко понимать, какой период мы выбираем.

Если нужен период от текущего момента назад — используем NOW() и INTERVAL.

Например:

WHERE created_at >= NOW() - INTERVAL '7 days'

Если нужен календарный период — используем CURRENT_DATE, date_trunc и границы периода.

Например:

WHERE created_at >= date_trunc('month', CURRENT_DATE)
  AND created_at <  date_trunc('month', CURRENT_DATE) + INTERVAL '1 month'

Когда вы правильно задаёте границы, отчёты перестают спорить с календарём, данные становятся предсказуемыми, а запросы остаются быстрыми даже на больших таблицах.

Practica con ejercicios reales

Resuelve ejercicios en el entrenador de SQL con corrección instantánea y pistas.

Abrir el entrenador