sqlpostgresqlmake-datemake-time

SQL make_date and make_time: Build Dates and Times from Integer Parts

How to build a date or time from separate integer report columns without format strings, and why make_date validates out-of-range parts.

3 мин четенеReferencesql · postgresql · make-date · make-time · date-functions · mysql
Тази статия в момента е на руски — английският превод е в процес на изготвяне.

make_date и make_time собирают значение из готовых чисел: вы передаёте год, месяц, день (или час, минуту, секунду) как отдельные целые, а не парсите строку. Это идеальный инструмент, когда части даты уже лежат в разных колонках отчёта.

Обе функции принимают целые числа и возвращают типизированное значение date или time, сразу проверяя каждую часть на допустимый диапазон. Это снимает два класса проблем разом: не нужно собирать промежуточную строку и угадывать её формат, и не нужно вручную проверять, что месяц лежит в 1..12, а день не выходит за границу месяца. Типичные источники таких разрозненных частей — выгрузки CSV, веб-формы и старые ERP, где год, месяц и день попали в отдельные колонки. Ниже разберём базовый синтаксис, сборку даты из колонок, поведение на некорректных частях и отличия в MySQL и ClickHouse.

Базовый синтаксис

make_date(year, month, day) возвращает date, make_time(hour, min, sec) возвращает time. Аргументы — целые числа (у секунд тип double precision, чтобы хранить доли). Никаких форматных строк вроде 'YYYY-MM-DD' и связанных с ними сюрпризов локали.

SELECT
  make_date(2024, 3, 15)   AS d,    -- 2024-03-15
  make_time(14, 30, 0)     AS t;    -- 14:30:00

Секунды принимают дробь, поэтому момент с миллисекундами собирается без приведения строк:

SELECT make_time(9, 5, 30.5) AS t;  -- 09:05:30.5

Полную метку времени даёт make_timestamp(year, month, day, hour, min, sec), а версию с зоной — make_timestamptz(...).

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

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

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

make_date снимает боль конкатенации и CAST: не нужно собирать строку y || '-' || m || '-' || d и надеяться, что одиночная цифра месяца не сломает формат. Числа идут в функцию как есть.

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

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

Проверка диапазона частей

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

SELECT make_date(2024, 13, 1);   -- ERROR: date field value out of range
SELECT make_date(2024, 2, 30);   -- ERROR: date field value out of range

Это и плюс, и подводный камень:

  • Плюс: грязные данные (месяц 13, день 0) обнаруживаются сразу, а не превращаются тихо в неверную дату.
  • Гочи: один битый ряд в большой выборке роняет весь запрос. Если данные не вычищены, проверяйте части заранее.

Защита от плохих строк через предварительный фильтр:

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;

Отрицательный год трактуется как до нашей эры, поэтому make_date(-1, 1, 1) — это не ошибка, а 0001-01-01 BC. Ноль в году запрещён.

Когда выгоднее make_time

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

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

Собранное time можно прибавить к дате и получить полноценный момент без разбора строк:

SELECT
  u.id,
  make_date(2024, 3, 15) + make_time(18, 0, 0) AS reminder_at
FROM users u
WHERE u.country = 'DE';

Различия в MySQL и ClickHouse

Имена и поведение отличаются от PostgreSQL.

  • MySQL функции make_date в привычном смысле нет. Есть MAKEDATE(year, dayofyear) — она ждёт год и день года, а не месяц с днём. Для сборки из месяца и дня берут STR_TO_DATE, а для времени — MAKETIME(hour, minute, second).
SELECT
  STR_TO_DATE('2024-3-15', '%Y-%c-%e') AS d,   -- 2024-03-15
  MAKETIME(14, 30, 0)                  AS t;   -- 14:30:00
  • ClickHouse предлагает makeDate(year, month, day) и makeDateTime(year, month, day, hour, minute, second), имена совпадают по смыслу с PostgreSQL.
SELECT
  makeDate(2024, 3, 15)                  AS d,
  makeDateTime(2024, 3, 15, 14, 30, 0)   AS t;

Главные расхождения при переносе сосредоточены именно в сборке даты из частей. В PostgreSQL make_date(y, m, d) ждёт год, месяц и день и падает с ошибкой на месяце 13 или дне 30 февраля. В MySQL MAKEDATE(year, dayofyear) принимает год и день года, поэтому те же три числа (2024, 3, 15) дадут не 15 марта, а 15-й день 2024 года, то есть 2024-01-15; никакой ошибки при этом не будет. В ClickHouse makeDate(year, month, day) повторяет сигнатуру PostgreSQL, но выход за диапазон не вызывает исключения — значение приводится к границе типа Date. Поэтому при миграции запроса проверяйте не только имя функции, но и порядок и смысл аргументов.

На практике это означает: после переноса прогоните на каждой СУБД одну и ту же тройку чисел с заведомо некорректной частью — например make_date(2024, 13, 1) против makeDate(2024, 13, 1) и MAKEDATE(2024, 13) — и сравните, что вернётся: ошибка, приведённая к границе дата или сдвиг по дню года. Так вы увидите расхождение до того, как его поймает отчёт.

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

Упражнявай се на реални задачи

Решавай задачи в SQL тренажора с незабавно оценяване и подсказки.

Отвори тренажора