En PostgreSQL la funcion AGE responde a la pregunta "cuanto tiempo ha pasado" como lo entiende una persona: en anos, meses y dias, no como un conteo crudo de dias. Por eso es la herramienta natural para la edad de usuarios, la antiguedad de empleados o lo viejo que es un pedido.
Dos argumentos frente a uno
AGE tiene dos formas. Con dos argumentos calcula el intervalo entre dos momentos, y el orden es AGE(end_ts, start_ts) — fin menos inicio.
SELECT AGE(TIMESTAMP '2024-03-01', TIMESTAMP '2021-11-15') AS gap;
Con un solo argumento el punto de referencia pasa a ser current_date de forma implicita (medianoche de hoy), asi que el resultado cambia dia a dia:
SELECT AGE(TIMESTAMP '1990-06-17') AS since_birth;
Comparalo con la resta simple. El operador - sobre dos fechas devuelve solo un numero de dias — util, pero nunca responde "cuantos anos".
SELECT DATE '2024-03-01' - DATE '2021-11-15' AS raw_days;
Edad de usuarios y antiguedad de pedidos
Tomemos nuestras tablas. Para saber cuanto tiempo lleva un usuario con su cuenta, pasa created_at como argumento unico:
SELECT id, email, AGE(created_at) AS account_age
FROM users
ORDER BY created_at;
La antiguedad de un pedido funciona igual. Es comodo filtrar por el intervalo directamente — PostgreSQL compara un interval contra un literal:
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';
Para empleados AGE describe la antiguedad con claridad cuando tienes una fecha de alta (aqui created_at hace de punto de inicio):
SELECT name, dept, AGE(NOW(), created_at) AS tenure
FROM employees
ORDER BY tenure DESC;
Por que los meses dependen del calendario
El rasgo clave del interval que devuelve AGE: los meses se cuentan por el calendario, no como 30 dias fijos. PostgreSQL avanza la fecha primero por anos completos, luego por meses completos, y solo el resto se expresa en dias.
SELECT AGE(DATE '2024-03-31', DATE '2024-01-31') AS feb_gap;
Por eso el mismo lapso en dias puede dar un numero de meses distinto — febrero es mas corto que julio. Es correcto para una "edad", pero fuente de sorpresas si esperabas aritmetica en dias.
AGE siempre normaliza el resultado en anos, meses y dias.
- Los meses de calendario no son 30 dias — la duracion depende de las fechas reales.
- Para un conteo exacto de dias usa la resta
date - date o EXTRACT(EPOCH ...).
El intervalo se imprime bien, pero para comparar y agrupar necesitas un numero. EXTRACT saca un campo del intervalo:
SELECT id, email,
EXTRACT(YEAR FROM AGE(created_at))::int AS full_years
FROM users;
Cuidado: EXTRACT(YEAR FROM AGE(...)) devuelve solo los anos del intervalo y descarta los meses. Para un usuario con "2 anos 11 meses" obtienes 2, no un 3 redondeado. Si quieres anos cumplidos es justo lo correcto; si quieres el total de meses, calcula years * 12 + months.
SELECT id,
EXTRACT(YEAR FROM AGE(created_at)) * 12
+ EXTRACT(MONTH FROM AGE(created_at)) AS total_months
FROM users;
Diferencias en otros motores
AGE es una extension de PostgreSQL sin equivalente directo en el estandar.
- MySQL: usa
TIMESTAMPDIFF(YEAR, start, end) para anos completos o DATEDIFF para dias. No hay un intervalo anos-meses-dias listo para usar.
- ClickHouse: ofrece
age('year', start, end) y dateDiff('day', ...); siempre indicas la unidad de forma explicita, y tampoco hay un intervalo simbolico.
Si el codigo debe ser portable, calcula los anos con TIMESTAMPDIFF/dateDiff, y construye el intervalo legible solo en PostgreSQL.
En PostgreSQL la funcion
AGEresponde a la pregunta "cuanto tiempo ha pasado" como lo entiende una persona: en anos, meses y dias, no como un conteo crudo de dias. Por eso es la herramienta natural para la edad de usuarios, la antiguedad de empleados o lo viejo que es un pedido.Dos argumentos frente a uno
AGEtiene dos formas. Con dos argumentos calcula el intervalo entre dos momentos, y el orden esAGE(end_ts, start_ts)— fin menos inicio.SELECT AGE(TIMESTAMP '2024-03-01', TIMESTAMP '2021-11-15') AS gap; -- 2 years 3 mons 14 daysCon un solo argumento el punto de referencia pasa a ser
current_datede forma implicita (medianoche de hoy), asi que el resultado cambia dia a dia:SELECT AGE(TIMESTAMP '1990-06-17') AS since_birth; -- evaluated against midnight todayComparalo con la resta simple. El operador
-sobre dos fechas devuelve solo un numero de dias — util, pero nunca responde "cuantos anos".SELECT DATE '2024-03-01' - DATE '2021-11-15' AS raw_days; -- 837Edad de usuarios y antiguedad de pedidos
Tomemos nuestras tablas. Para saber cuanto tiempo lleva un usuario con su cuenta, pasa
created_atcomo argumento unico:SELECT id, email, AGE(created_at) AS account_age FROM users ORDER BY created_at;La antiguedad de un pedido funciona igual. Es comodo filtrar por el intervalo directamente — PostgreSQL compara un
intervalcontra un literal: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';Para empleados
AGEdescribe la antiguedad con claridad cuando tienes una fecha de alta (aquicreated_athace de punto de inicio):SELECT name, dept, AGE(NOW(), created_at) AS tenure FROM employees ORDER BY tenure DESC;Por que los meses dependen del calendario
El rasgo clave del
intervalque devuelveAGE: los meses se cuentan por el calendario, no como 30 dias fijos. PostgreSQL avanza la fecha primero por anos completos, luego por meses completos, y solo el resto se expresa en dias.SELECT AGE(DATE '2024-03-31', DATE '2024-01-31') AS feb_gap; -- 2 mons (not 60 days, not 59)Por eso el mismo lapso en dias puede dar un numero de meses distinto — febrero es mas corto que julio. Es correcto para una "edad", pero fuente de sorpresas si esperabas aritmetica en dias.
AGEsiempre normaliza el resultado en anos, meses y dias.date - dateoEXTRACT(EPOCH ...).Extraer anos con EXTRACT
El intervalo se imprime bien, pero para comparar y agrupar necesitas un numero.
EXTRACTsaca un campo del intervalo: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;Diferencias en otros motores
AGEes una extension de PostgreSQL sin equivalente directo en el estandar.TIMESTAMPDIFF(YEAR, start, end)para anos completos oDATEDIFFpara dias. No hay un intervalo anos-meses-dias listo para usar.age('year', start, end)ydateDiff('day', ...); siempre indicas la unidad de forma explicita, y tampoco hay un intervalo simbolico.Si el codigo debe ser portable, calcula los anos con
TIMESTAMPDIFF/dateDiff, y construye el intervalo legible solo en PostgreSQL.