sqlpostgresqlmake-datemake-time

make_date и make_time в PostgreSQL: как собрать дату и время из отдельных чисел

MAKE_DATE и MAKE_TIME безопасно собирают значения из числовых колонок и сразу показывают ошибки диапазона вместо тихой нормализации.

9 мин чтенияСправочникsql · postgresql · make-date · make-time · date-functions · mysql

Иногда дата приходит в базу не красивой строкой 2024-03-15, а по частям:

  • отдельно год;
  • отдельно месяц;
  • отдельно день.

Так часто бывает после загрузки CSV, интеграции со старой ERP-системой, импорта из Excel или обработки веб-формы. В одной колонке лежит 2024, в другой 3, в третьей 15.

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

В PostgreSQL для таких случаев есть функции make_date и make_time.

Они собирают нормальные типизированные значения из готовых чисел:

  • make_date возвращает date;
  • make_time возвращает time.

Главная идея простая:

Если части даты или времени уже лежат в отдельных колонках, не склеивайте строку. Соберите значение напрямую.

Что делает make_date

Функция make_date принимает три числа:

make_date(year, month, day)

И возвращает дату.

SELECT make_date(2024, 3, 15) AS d;

Результат:

2024-03-15

Здесь всё читается буквально:

  • 2024 — год;
  • 3 — месяц;
  • 15 — день.

Никаких строковых форматов вроде YYYY-MM-DD, никаких лишних CAST, никакой склейки через ||.

PostgreSQL сразу понимает, что вы хотите получить значение типа date.

Что делает make_time

Функция make_time работает похожим образом, но собирает время.

make_time(hour, min, sec)

Пример:

SELECT make_time(14, 30, 0) AS t;

Результат:

14:30:00

Аргументы означают:

  • 14 — час;
  • 30 — минута;
  • 0 — секунда.

Секунды могут быть дробными. Это удобно, если в данных есть миллисекунды или доли секунды.

SELECT make_time(9, 5, 30.5) AS t;

Результат:

09:05:30.5

То есть не нужно собирать строку вроде 09:05:30.5. Можно передать числа напрямую.

Зачем это нужно

Представьте таблицу order_parts, куда попали данные из старой системы.

order_id | y    | m | d
---------+------+---+----
101      | 2024 | 3 | 15
102      | 2024 | 3 | 16
103      | 2024 | 4 | 2

Год, месяц и день лежат отдельно.

С помощью make_date можно собрать полноценную дату:

SELECT
    order_id,
    make_date(y, m, d) AS order_date
FROM order_parts;

Результат будет таким:

order_id | order_date
---------+------------
101      | 2024-03-15
102      | 2024-03-16
103      | 2024-04-02

Это намного чище, чем собирать строку вручную:

(y || '-' || m || '-' || d)::date

Такой вариант сложнее читать и легче сломать. Особенно если месяц или день пришли без ведущего нуля.

make_date сразу говорит читателю запроса:

Мы собираем дату из года, месяца и дня.

Сборка даты из колонок

Самый полезный сценарий — когда части даты лежат в разных колонках.

Допустим, есть таблица order_parts.

SELECT
    order_id,
    make_date(y, m, d) AS order_date
FROM order_parts;

Такой запрос превращает три числовые колонки в одну нормальную дату.

Дальше с этой датой можно работать как обычно:

  • сортировать;
  • фильтровать;
  • группировать;
  • соединять с другими таблицами;
  • сравнивать с другими датами.

Например, отсортируем заказы по собранной дате:

SELECT
    order_id,
    make_date(y, m, d) AS order_date
FROM order_parts
ORDER BY order_date;

Или сгруппируем строки по месяцу:

SELECT
    date_trunc('month', make_date(y, m, d)) AS order_month,
    count(*) AS orders_count
FROM order_parts
GROUP BY order_month
ORDER BY order_month;

Сначала мы собираем дату, а потом используем обычные инструменты PostgreSQL для работы с датами.

Фильтр по дате из параметров

make_date удобно использовать не только с колонками, но и с параметрами.

Например, приложение передало три числа:

  • год 2024;
  • месяц 3;
  • день 15.

Нужно найти оплаченные заказы за этот день.

SELECT
    id,
    user_id,
    amount
FROM orders
WHERE created_at::date = make_date(2024, 3, 15)
  AND status = 'paid';

Запрос читается естественно:

Возьми дату из чисел 2024, 3, 15 и сравни с датой создания заказа.

Это понятнее, чем собирать строку и потом приводить её к типу date.

make_date проверяет корректность даты

Важное преимущество make_date: функция не делает вид, что всё хорошо, если дата неправильная.

Например, месяца 13 не существует.

SELECT make_date(2024, 13, 1);

PostgreSQL вернёт ошибку.

Дня 30 в феврале 2024 года тоже нет.

SELECT make_date(2024, 2, 30);

И это тоже ошибка.

Это хорошее поведение: грязные данные обнаруживаются сразу.

Если в импортированной таблице месяц 13, день 0 или 31 апреля, лучше узнать об этом на этапе обработки, а не получить тихо неправильный отчёт.

Почему ошибка — это и плюс, и минус

С одной стороны, ошибка защищает от мусора в данных.

С другой стороны, один битый ряд может уронить весь запрос.

Допустим, в таблице миллион строк, и только в одной строке месяц равен 13.

Такой запрос не вернёт «все хорошие строки». Он завершится ошибкой:

SELECT
    order_id,
    make_date(y, m, d) AS order_date
FROM order_parts;

Поэтому, если данные не вычищены, сначала лучше отфильтровать явно неправильные значения.

SELECT
    order_id,
    make_date(y, m, d) AS order_date
FROM order_parts
WHERE m BETWEEN 1 AND 12
  AND d BETWEEN 1 AND 31;

Это базовая защита, но она не ловит все случаи. Например, она пропустит 2024-02-30, потому что день 30 сам по себе находится между 1 и 31.

Для более строгой проверки обычно используют отдельную валидацию данных на этапе загрузки или обработки.

Важная деталь про год

make_date принимает год как число.

Обычные годы работают ожидаемо:

SELECT make_date(2024, 1, 1) AS d;

Результат:

2024-01-01

Год 0 в PostgreSQL недопустим.

SELECT make_date(0, 1, 1);

Такой запрос вернёт ошибку.

А отрицательные годы относятся к датам до нашей эры. Например, -1 — это не ошибка, а год до нашей эры.

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

make_time для времени из отдельных частей

make_time полезен, когда отдельно хранятся час, минута и секунда.

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

dept    | start_hour | start_minute
--------+------------+-------------
Sales   | 9          | 0
Support | 10         | 30
QA      | 11         | 0

Можно собрать нормальное значение time.

SELECT
    dept,
    make_time(start_hour, start_minute, 0) AS shift_start
FROM department_settings;

Результат:

dept    | shift_start
--------+------------
Sales   | 09:00:00
Support | 10:30:00
QA      | 11:00:00

Теперь это не просто два числа, а полноценное время, с которым можно работать дальше.

Пример: единое время старта смены

Иногда время не хранится в таблице, а задаётся прямо в запросе.

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

SELECT
    e.dept,
    count(*) AS people,
    make_time(9, 0, 0) AS shift_start
FROM employees e
GROUP BY e.dept;

Здесь make_time(9, 0, 0) явно создаёт значение времени.

Можно было бы написать строку '09:00:00'::time, но make_time особенно хорош, когда час и минута приходят числами из параметров или другой таблицы.

Как собрать дату и время вместе

date и time можно сложить и получить timestamp.

Например, соберём дату 2024-03-15 и время 18:00:00.

SELECT
    make_date(2024, 3, 15) + make_time(18, 0, 0) AS reminder_at;

Результат:

2024-03-15 18:00:00

Это удобно, когда дата и время приходят отдельно.

Например, дата напоминания хранится как год, месяц и день, а время — как час и минута.

SELECT
    user_id,
    make_date(y, m, d) + make_time(h, min, 0) AS reminder_at
FROM reminders_raw;

Так мы получаем полноценную метку времени без разбора строк.

make_timestamp и make_timestamptz

Если нужно сразу собрать полную дату и время, в PostgreSQL есть make_timestamp.

SELECT make_timestamp(2024, 3, 15, 14, 30, 0) AS ts;

Результат:

2024-03-15 14:30:00

Сигнатура такая:

make_timestamp(year, month, day, hour, min, sec)

Если нужен вариант с часовым поясом, есть make_timestamptz.

SELECT make_timestamptz(2024, 3, 15, 14, 30, 0, 'Europe/Moscow') AS ts;

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

Например, пользователь выбрал в форме:

  • 2024;
  • 3;
  • 15;
  • 14;
  • 30;
  • Europe/Moscow.

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

Почему лучше не собирать дату строкой

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

SELECT
    (y || '-' || m || '-' || d)::date AS order_date
FROM order_parts;

На первый взгляд всё работает.

Но у этого способа есть минусы:

  • сложнее читать;
  • нужно думать о формате строки;
  • можно ошибиться с разделителями;
  • одиночные цифры месяца и дня могут вести себя не так, как ожидается;
  • парсинг строки обычно менее прозрачен, чем сборка типизированного значения.

make_date выражает намерение напрямую:

SELECT
    make_date(y, m, d) AS order_date
FROM order_parts;

Этот запрос проще поддерживать. Даже новичок быстро понимает, что происходит.

Пример из жизни: импорт из CSV

Допустим, вы загрузили CSV от партнёра. В файле дата заказа разложена по трём колонкам.

order_id,year,month,day,amount
101,2024,3,15,5000
102,2024,3,16,3200
103,2024,4,2,7900

После загрузки в промежуточную таблицу можно собрать дату так:

SELECT
    order_id,
    make_date(year, month, day) AS order_date,
    amount
FROM imported_orders;

А затем вставить данные в нормальную таблицу:

INSERT INTO orders (external_id, created_at, amount)
SELECT
    order_id,
    make_date(year, month, day),
    amount
FROM imported_orders;

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

Пример: расписание из формы

Допустим, пользователь создаёт напоминание в интерфейсе.

Форма отправляет отдельные значения:

  • год;
  • месяц;
  • день;
  • час;
  • минута.

В таблице reminder_input они лежат отдельно.

SELECT
    user_id,
    make_date(y, m, d) + make_time(h, min, 0) AS reminder_at
FROM reminder_input;

Получается полноценное значение:

2024-03-15 18:00:00

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

Как аккуратно назвать результат

Хорошее имя колонки помогает читать запрос.

Не очень удачно:

SELECT
    make_date(y, m, d) AS value
FROM order_parts;

Лучше:

SELECT
    make_date(y, m, d) AS order_date
FROM order_parts;

Для времени:

SELECT
    make_time(h, min, 0) AS shift_start
FROM shifts_raw;

Для полной метки времени:

SELECT
    make_date(y, m, d) + make_time(h, min, 0) AS starts_at
FROM events_raw;

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

Ошибки, о которых стоит помнить

Первая ошибка — передать аргументы не в том порядке.

Правильно:

SELECT make_date(2024, 3, 15) AS d;

Это 15 марта 2024.

Неправильно:

SELECT make_date(15, 3, 2024) AS d;

PostgreSQL попытается прочитать это как год 15, месяц 3, день 2024 и вернёт ошибку.

Вторая ошибка — думать, что make_date исправит плохую дату.

SELECT make_date(2024, 2, 30) AS d;

Такой даты нет, поэтому будет ошибка. Функция не переносит лишние дни в март.

Третья ошибка — проверять только диапазон 1..31 для дня и думать, что этого достаточно.

WHERE d BETWEEN 1 AND 31

Такой фильтр пропустит 2024-02-30, хотя это несуществующая дата.

Четвёртая ошибка — переносить запросы между СУБД по имени функции, не проверив сигнатуру. Особенно это касается MySQL.

MySQL: MAKEDATE работает иначе

В MySQL есть функция MAKEDATE, но она не является прямым аналогом PostgreSQL make_date.

В PostgreSQL:

make_date(year, month, day)

А в MySQL:

MAKEDATE(year, dayofyear)

То есть MySQL ждёт год и номер дня в году.

Например:

SELECT MAKEDATE(2024, 75) AS d;

Это дата, соответствующая 75-му дню 2024 года.

Если вам нужно собрать дату из года, месяца и дня в MySQL, часто используют STR_TO_DATE.

SELECT STR_TO_DATE('2024-3-15', '%Y-%c-%e') AS d;

Для времени в MySQL есть MAKETIME.

SELECT MAKETIME(14, 30, 0) AS t;

Главная ловушка: не переносите make_date(2024, 3, 15) из PostgreSQL в MySQL как MAKEDATE(2024, 3, 15). У функции другой смысл и другое количество аргументов.

ClickHouse: makeDate и makeDateTime

В ClickHouse есть функции, похожие по смыслу на PostgreSQL.

Для даты:

SELECT makeDate(2024, 3, 15) AS d;

Для даты и времени:

SELECT makeDateTime(2024, 3, 15, 14, 30, 0) AS ts;

Смысл близок к PostgreSQL: передаём год, месяц, день, час, минуту и секунду как отдельные числа.

Но при переносе всё равно нужно проверять поведение на плохих данных и граничных значениях. Разные СУБД могут по-разному реагировать на месяц 13, день 30 февраля или дату за пределами допустимого диапазона типа.

Как проверять перенос между СУБД

Если вы переносите запрос из PostgreSQL в MySQL или ClickHouse, не ограничивайтесь проверкой на красивой дате вроде 2024-03-15.

Обязательно проверьте граничные и плохие значения:

SELECT make_date(2024, 13, 1) AS d;
SELECT make_date(2024, 2, 30) AS d;
SELECT make_date(0, 1, 1) AS d;

В PostgreSQL такие случаи помогут быстро увидеть, где данные не проходят валидацию.

В другой СУБД результат может быть другим: ошибка, NULL, приведение к границе типа или совсем другая трактовка аргументов.

Главное правило при переносе:

Проверяйте не только имя функции, но и смысл каждого аргумента.

Когда использовать make_date и make_time

Используйте make_date, когда:

  • год, месяц и день уже лежат в отдельных колонках;
  • дата приходит из формы отдельными числами;
  • данные загружены из CSV или старой системы;
  • хочется избежать склейки строк;
  • нужно сразу получить тип date.

Используйте make_time, когда:

  • час, минута и секунда приходят отдельно;
  • время хранится в настройках;
  • нужно собрать время из параметров;
  • хочется получить тип time, а не строку.

Используйте make_timestamp или сложение date + time, когда нужно получить полную дату со временем.

Главное из статьи

make_date и make_time в PostgreSQL собирают дату и время из отдельных числовых частей.

make_date принимает год, месяц и день:

SELECT make_date(2024, 3, 15) AS d;

make_time принимает час, минуту и секунду:

SELECT make_time(14, 30, 0) AS t;

Это удобнее и безопаснее, чем склеивать строку и потом приводить её к нужному типу.

make_date сразу проверяет корректность даты. Месяц 13, день 30 февраля и год 0 вызовут ошибку, а не превратятся тихо в другую дату.

Если дата и время лежат отдельно, их можно соединить:

SELECT make_date(2024, 3, 15) + make_time(18, 0, 0) AS reminder_at;

Для полной сборки timestamp есть make_timestamp, а для момента с часовым поясом — make_timestamptz.

При переносе между СУБД будьте особенно внимательны: MySQL MAKEDATE принимает год и день года, а не год, месяц и день. В ClickHouse есть похожие makeDate и makeDateTime, но поведение на плохих значениях нужно проверять отдельно.

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

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

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