sqlpostgresqlintervaldates

JUSTIFY_INTERVAL in PostgreSQL: Making Durations Human-Readable

JUSTIFY_HOURS, JUSTIFY_DAYS and JUSTIFY_INTERVAL roll a raw interval into a clean days-and-months shape.

3 min læsningReferencesql · postgresql · interval · dates · justify
Denne artikel er i øjeblikket på russisk — den engelske oversættelse er undervejs.

Когда вы складываете и вычитаете интервалы в PostgreSQL, результат часто получается «сырым»: 36 часов, 90 дней, 800 минут. Семейство функций JUSTIFY_* приводит такой интервал к человекочитаемому виду — переносит лишние часы в сутки, а лишние сутки в месяцы.

Важно понимать, что именно делает JUSTIFY_* и чего не делает. Эти функции — чистое переоформление длительности: они не привязаны к конкретной дате, не знают о календаре и не меняют исходное событие. JUSTIFY_HOURS('25 hours') всегда даёт 1 day 01:00:00 независимо от того, попали ли эти 25 часов на переход летнего времени. Поэтому нормализация безопасна для читаемого вывода в отчёте, но опасна, если её результат подставить туда, где ждут реальный календарный возраст или точную дату. Держите эту границу в голове: JUSTIFY_* форматирует длительность, а не вычисляет её заново.

Три функции и что они переносят

В PostgreSQL есть три функции нормализации, и каждая отвечает за свой «разряд»:

  • JUSTIFY_HOURS(i) — забирает каждые 24 часа и превращает их в 1 день.
  • JUSTIFY_DAYS(i) — забирает каждые 30 дней и превращает их в 1 месяц.
  • JUSTIFY_INTERVAL(i) — делает оба переноса сразу и приводит знаки к согласованному виду.
SELECT JUSTIFY_HOURS(INTERVAL '36 hours')   AS h,   -- 1 day 12:00:00
       JUSTIFY_DAYS(INTERVAL '90 days')      AS d,   -- 3 mons
       JUSTIFY_INTERVAL(INTERVAL '1 mon 33 days 27 hours') AS full;
-- full -> 2 mons 4 days 03:00:00

Обратите внимание: ни одна из функций не «уплощает» интервал до секунд. PostgreSQL хранит интервал тремя независимыми полями — месяцы, дни и микросекунды, — и JUSTIFY_* лишь перекладывает значения между этими полями, не меняя смысла.

Зачем это нужно: длительность заказа

Допустим, мы считаем, сколько времени заказ провёл в обработке. Простое вычитание времени даёт интервал, где всё «висит» в часах:

SELECT id,
       created_at,
       NOW() - created_at AS raw_age
FROM orders
WHERE status = 'processing';
-- raw_age might be, e.g., '52:30:00'

Строка 52:30:00 технически верна, но в отчёте её читать неудобно. Завернём результат в JUSTIFY_HOURS:

SELECT id,
       JUSTIFY_HOURS(NOW() - created_at) AS readable_age
FROM orders
WHERE status = 'processing';
-- readable_age -> 2 days 04:30:00

Теперь видно «2 дня и сколько-то часов» — именно то, что ожидает увидеть менеджер.

Стаж сотрудников и крупные суммы дней

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

SELECT dept,
       JUSTIFY_DAYS(SUM(NOW() - created_at)) AS dept_tenure
FROM employees
GROUP BY dept;

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

SELECT name,
       JUSTIFY_INTERVAL(salary_review_at - hired_at) AS service
FROM employees
ORDER BY service DESC;

Ловушка: 30 дней — это не календарный месяц

Главная ловушка JUSTIFY_DAYS и JUSTIFY_INTERVAL в том, что они считают месяц равным ровно 30 дням. Реальные месяцы такими не бывают: в феврале 28 или 29 дней, в июле — 31.

SELECT JUSTIFY_INTERVAL(INTERVAL '60 days');
-- 2 mons   (not "1 month plus some calendar days")

Из-за этого нормализованный интервал годится для отчётов и человекочитаемых меток, но не для точной арифметики дат. Запомните разницу:

  • JUSTIFY_* оперирует фиксированными правилами: 24 часа = 1 день, 30 дней = 1 месяц.
  • AGE(end, start) смотрит на реальный календарь и даёт настоящие месяцы и дни.

Если нужен честный «возраст», берите AGE. Если нужно лишь аккуратно показать накопленную длительность — JUSTIFY_*. На практике это значит: не суммируйте JUSTIFY_DAYS(...) по сотрудникам, чтобы получить «общий стаж в месяцах» для расчёта выплат — 30-дневные месяцы накопят заметную ошибку. Для денег и сроков считайте через AGE или прямую разницу дат, а JUSTIFY_* оставьте последнему шагу — выводу на экран.

Различия в других СУБД

Функции JUSTIFY_* — это специфика PostgreSQL, прямых аналогов в других движках нет.

  • MySQL: отдельного типа interval нет, разницу обычно держат в секундах или днях. Нормализацию делают вручную через DIV и MOD, например seconds DIV 86400 для дней.
  • ClickHouse: интервалы тоже не нормализуются автоматически; используйте dateDiff('day', ...) и арифметику над числами, чтобы получить нужные разряды.

Из-за этой ручной нормализации перенос логики между движками — самое уязвимое место: то, что в PostgreSQL делает один JUSTIFY_INTERVAL, в MySQL и ClickHouse превращается в цепочку DIV/MOD с собственными правилами округления и знака для отрицательных длительностей. Прежде чем доверять такому коду, прогоните граничные случаи: ровно 24 часа, ровно 30 дней, отрицательный интервал (hired_at позже salary_review_at) и интервал из чистых месяцев без дней. Именно на этих границах три реализации расходятся, хотя на «обычных» значениях дают одно и то же.

Вывод: JUSTIFY_* — удобный инструмент презентации именно в PostgreSQL. В переносимом коде нормализуйте длительности самостоятельно и помните, что «месяц = 30 дней» — это упрощение, а не правда календаря.

Øv dig på rigtige opgaver

Løs opgaver i SQL-træneren med øjeblikkelig bedømmelse og hints.

Åbn træneren