Tá an t-alt seo i Rúisis faoi láthair — tá an t-aistriúchán Béarla ar siúl.
В PostgreSQL функция AGE отвечает на вопрос «сколько прошло времени» так, как это понимает человек: в годах, месяцах и днях, а не в сыром количестве суток. Это делает её незаменимой для возраста пользователей, стажа сотрудников и «давности» заказов.
Возраст, стаж и срок жизни заказа звучат как простая разница дат, но AGE отвечает на них именно по-календарному: она прокручивает дату на целые годы и месяцы, а остаток выражает в днях. Поэтому «2 года 11 месяцев» она покажет как 2 years 11 mons, а не как абстрактные 1065 дней. У AGE две формы — с одним аргументом и с двумя, — и обе возвращают тип interval, который PostgreSQL умеет печатать, сравнивать с литералом и разбирать через EXTRACT. Ниже разберём обе формы, ловушку с порядком аргументов, календарную природу месяцев и то, как достать из интервала чистое число лет.
Два аргумента против одного
У AGE есть две формы. С двумя аргументами она считает интервал между двумя моментами, причём порядок — AGE(end_ts, start_ts), то есть «конец минус начало».
SELECT AGE(TIMESTAMP '2024-03-01', TIMESTAMP '2021-11-15') AS gap;
С одним аргументом точкой отсчёта неявно становится current_date (полночь сегодня), поэтому результат меняется день ото дня:
SELECT AGE(TIMESTAMP '1990-06-17') AS since_birth;
Сравните это с обычным вычитанием. Оператор - над двумя датами возвращает просто число дней — это полезно для арифметики, но не отвечает на вопрос «сколько лет и месяцев». Именно поэтому для человекочитаемого возраста почти всегда берут AGE, а сырое вычитание оставляют для расчётов, где важна точность в сутках.
SELECT DATE '2024-03-01' - DATE '2021-11-15' AS raw_days;
Возраст пользователей и стаж заказов
Возьмём наши таблицы. Чтобы узнать, как давно зарегистрирован пользователь, передайте created_at одним аргументом:
SELECT id, email, AGE(created_at) AS account_age
FROM users
ORDER BY created_at;
«Давность» заказа считается так же. Удобно фильтровать по интервалу напрямую — PostgreSQL умеет сравнивать interval с литералом:
SELECT o.id, o.amount, AGE(o.created_at) AS order_age
FROM orders o
WHERE o.status = 'paid'
AND AGE(o.created_at) > INTERVAL '90 days';
Для сотрудников AGE отлично описывает стаж, если в таблице есть дата найма (здесь подставлен created_at как пример точки отсчёта):
SELECT name, dept, AGE(NOW(), created_at) AS tenure
FROM employees
ORDER BY tenure DESC;
Почему месяцы зависят от календаря
Главная особенность interval, который возвращает AGE: месяцы считаются по календарю, а не как фиксированные 30 дней. PostgreSQL «прокручивает» дату сначала на целые годы, потом на целые месяцы, и только остаток выражает в днях.
SELECT AGE(DATE '2024-03-31', DATE '2024-01-31') AS feb_gap;
Из-за этого один и тот же интервал в днях может дать разное число месяцев — февраль короче июля, а високосный год добавляет лишний день. Это правильное поведение для «возраста», но источник сюрпризов, если вы ждёте, что месяц всегда равен 30 дням.
AGE всегда нормализует результат: годы, месяцы, дни.
- Календарные месяцы не равны 30 дням — длина зависит от конкретных дат.
- Для точного числа суток берите вычитание
date - date или EXTRACT(EPOCH ...).
Интервал красиво печатается, но для сравнений и группировок нужно число. EXTRACT достаёт нужное поле из интервала:
SELECT id, email,
EXTRACT(YEAR FROM AGE(created_at))::int AS full_years
FROM users;
Ловушка: EXTRACT(YEAR FROM AGE(...)) даёт только годы интервала и отбрасывает месяцы. Для пользователя со стажем «2 года 11 месяцев» вы получите 2, а не округление до 3. Если нужны полные годы — это именно то, что надо; если нужно общее число месяцев, считайте years * 12 + months.
SELECT id,
EXTRACT(YEAR FROM AGE(created_at)) * 12
+ EXTRACT(MONTH FROM AGE(created_at)) AS total_months
FROM users;
Различия в других СУБД
AGE — расширение PostgreSQL, прямого аналога в стандарте нет.
- MySQL: используйте
TIMESTAMPDIFF(YEAR, start, end) для целых лет или DATEDIFF для дней. Готового «годы-месяцы-дни» интервала нет.
- ClickHouse: есть
age('year', start, end) и dateDiff('day', ...); единицу всегда задаёте явно, символьного интервала тоже нет.
Главные сюрпризы AGE тоже календарные. День рождения 29 февраля в невисокосный год нормализуется к 1 марта, поэтому интервал для такого пользователя «прыгает» не так, как для остальных. Если в AGE(end, start) перепутать порядок аргументов, результат станет отрицательным интервалом (-2 years -3 mons ...), и фильтр вроде > INTERVAL '90 days' тихо перестанет срабатывать. А created_at типа timestamptz AGE приводит к текущему часовому поясу сессии — у клиента в другой зоне «сегодня» начинается в другой момент, и форма с одним аргументом может дать на сутки иной результат.
И помните про границу применимости: AGE строит удобочитаемый интервал, но не годится как точная длительность. Для биллинга по суткам, SLA в часах или сортировки по «настоящему» возрасту берите date - date или EXTRACT(EPOCH FROM ...), потому что один и тот же interval '1 mon' соответствует то 28, то 31 дню. Фильтр AGE(o.created_at) > INTERVAL '90 days' к тому же оборачивает колонку в функцию и закрывает путь к индексу по created_at — на больших таблицах быстрее условие вида o.created_at < NOW() - INTERVAL '90 days'.
Если код должен быть переносимым, считайте годы через TIMESTAMPDIFF/dateDiff, а удобный человекочитаемый интервал стройте только под PostgreSQL.
В PostgreSQL функция
AGEотвечает на вопрос «сколько прошло времени» так, как это понимает человек: в годах, месяцах и днях, а не в сыром количестве суток. Это делает её незаменимой для возраста пользователей, стажа сотрудников и «давности» заказов.Возраст, стаж и срок жизни заказа звучат как простая разница дат, но
AGEотвечает на них именно по-календарному: она прокручивает дату на целые годы и месяцы, а остаток выражает в днях. Поэтому «2 года 11 месяцев» она покажет как2 years 11 mons, а не как абстрактные 1065 дней. УAGEдве формы — с одним аргументом и с двумя, — и обе возвращают типinterval, который PostgreSQL умеет печатать, сравнивать с литералом и разбирать черезEXTRACT. Ниже разберём обе формы, ловушку с порядком аргументов, календарную природу месяцев и то, как достать из интервала чистое число лет.Два аргумента против одного
У
AGEесть две формы. С двумя аргументами она считает интервал между двумя моментами, причём порядок —AGE(end_ts, start_ts), то есть «конец минус начало».SELECT AGE(TIMESTAMP '2024-03-01', TIMESTAMP '2021-11-15') AS gap; -- 2 years 3 mons 14 daysС одним аргументом точкой отсчёта неявно становится
current_date(полночь сегодня), поэтому результат меняется день ото дня:SELECT AGE(TIMESTAMP '1990-06-17') AS since_birth; -- evaluated against midnight todayСравните это с обычным вычитанием. Оператор
-над двумя датами возвращает просто число дней — это полезно для арифметики, но не отвечает на вопрос «сколько лет и месяцев». Именно поэтому для человекочитаемого возраста почти всегда берутAGE, а сырое вычитание оставляют для расчётов, где важна точность в сутках.SELECT DATE '2024-03-01' - DATE '2021-11-15' AS raw_days; -- 837Возраст пользователей и стаж заказов
Возьмём наши таблицы. Чтобы узнать, как давно зарегистрирован пользователь, передайте
created_atодним аргументом:SELECT id, email, AGE(created_at) AS account_age FROM users ORDER BY created_at;«Давность» заказа считается так же. Удобно фильтровать по интервалу напрямую — PostgreSQL умеет сравнивать
intervalс литералом:SELECT o.id, o.amount, AGE(o.created_at) AS order_age FROM orders o WHERE o.status = 'paid' AND AGE(o.created_at) > INTERVAL '90 days';Для сотрудников
AGEотлично описывает стаж, если в таблице есть дата найма (здесь подставленcreated_atкак пример точки отсчёта):SELECT name, dept, AGE(NOW(), created_at) AS tenure FROM employees ORDER BY tenure DESC;Почему месяцы зависят от календаря
Главная особенность
interval, который возвращаетAGE: месяцы считаются по календарю, а не как фиксированные 30 дней. PostgreSQL «прокручивает» дату сначала на целые годы, потом на целые месяцы, и только остаток выражает в днях.SELECT AGE(DATE '2024-03-31', DATE '2024-01-31') AS feb_gap; -- 2 mons (not 60 days, not 59)Из-за этого один и тот же интервал в днях может дать разное число месяцев — февраль короче июля, а високосный год добавляет лишний день. Это правильное поведение для «возраста», но источник сюрпризов, если вы ждёте, что месяц всегда равен 30 дням.
AGEвсегда нормализует результат: годы, месяцы, дни.date - dateилиEXTRACT(EPOCH ...).Извлекаем годы через EXTRACT
Интервал красиво печатается, но для сравнений и группировок нужно число.
EXTRACTдостаёт нужное поле из интервала:SELECT id, email, EXTRACT(YEAR FROM AGE(created_at))::int AS full_years FROM users;SELECT id, EXTRACT(YEAR FROM AGE(created_at)) * 12 + EXTRACT(MONTH FROM AGE(created_at)) AS total_months FROM users;Различия в других СУБД
AGE— расширение PostgreSQL, прямого аналога в стандарте нет.TIMESTAMPDIFF(YEAR, start, end)для целых лет илиDATEDIFFдля дней. Готового «годы-месяцы-дни» интервала нет.age('year', start, end)иdateDiff('day', ...); единицу всегда задаёте явно, символьного интервала тоже нет.Главные сюрпризы
AGEтоже календарные. День рождения 29 февраля в невисокосный год нормализуется к 1 марта, поэтому интервал для такого пользователя «прыгает» не так, как для остальных. Если вAGE(end, start)перепутать порядок аргументов, результат станет отрицательным интервалом (-2 years -3 mons ...), и фильтр вроде> INTERVAL '90 days'тихо перестанет срабатывать. Аcreated_atтипаtimestamptzAGEприводит к текущему часовому поясу сессии — у клиента в другой зоне «сегодня» начинается в другой момент, и форма с одним аргументом может дать на сутки иной результат.И помните про границу применимости:
AGEстроит удобочитаемый интервал, но не годится как точная длительность. Для биллинга по суткам, SLA в часах или сортировки по «настоящему» возрасту беритеdate - dateилиEXTRACT(EPOCH FROM ...), потому что один и тот жеinterval '1 mon'соответствует то 28, то 31 дню. ФильтрAGE(o.created_at) > INTERVAL '90 days'к тому же оборачивает колонку в функцию и закрывает путь к индексу поcreated_at— на больших таблицах быстрее условие видаo.created_at < NOW() - INTERVAL '90 days'.Если код должен быть переносимым, считайте годы через
TIMESTAMPDIFF/dateDiff, а удобный человекочитаемый интервал стройте только под PostgreSQL.