Dit artikel is momenteel in het Russisch — de Engelse vertaling is in uitvoering.
Когда вы складываете и вычитаете интервалы в 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,
JUSTIFY_DAYS(INTERVAL '90 days') AS d,
JUSTIFY_INTERVAL(INTERVAL '1 mon 33 days 27 hours') AS full;
Обратите внимание: ни одна из функций не «уплощает» интервал до секунд. PostgreSQL хранит интервал тремя независимыми полями — месяцы, дни и микросекунды, — и JUSTIFY_* лишь перекладывает значения между этими полями, не меняя смысла.
Зачем это нужно: длительность заказа
Допустим, мы считаем, сколько времени заказ провёл в обработке. Простое вычитание времени даёт интервал, где всё «висит» в часах:
SELECT id,
created_at,
NOW() - created_at AS raw_age
FROM orders
WHERE status = 'processing';
Строка 52:30:00 технически верна, но в отчёте её читать неудобно. Завернём результат в JUSTIFY_HOURS:
SELECT id,
JUSTIFY_HOURS(NOW() - created_at) AS readable_age
FROM orders
WHERE status = 'processing';
Теперь видно «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');
Из-за этого нормализованный интервал годится для отчётов и человекочитаемых меток, но не для точной арифметики дат. Запомните разницу:
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 дней» — это упрощение, а не правда календаря.
Когда вы складываете и вычитаете интервалы в 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, прямых аналогов в других движках нет.intervalнет, разницу обычно держат в секундах или днях. Нормализацию делают вручную черезDIVиMOD, напримерseconds DIV 86400для дней.dateDiff('day', ...)и арифметику над числами, чтобы получить нужные разряды.Из-за этой ручной нормализации перенос логики между движками — самое уязвимое место: то, что в PostgreSQL делает один
JUSTIFY_INTERVAL, в MySQL и ClickHouse превращается в цепочкуDIV/MODс собственными правилами округления и знака для отрицательных длительностей. Прежде чем доверять такому коду, прогоните граничные случаи: ровно 24 часа, ровно 30 дней, отрицательный интервал (hired_atпозжеsalary_review_at) и интервал из чистых месяцев без дней. Именно на этих границах три реализации расходятся, хотя на «обычных» значениях дают одно и то же.Вывод:
JUSTIFY_*— удобный инструмент презентации именно в PostgreSQL. В переносимом коде нормализуйте длительности самостоятельно и помните, что «месяц = 30 дней» — это упрощение, а не правда календаря.