sqlpostgresqldatesinterval

Aritmetica de fechas en SQL: sumar dias, restar fechas e intervalos

Como sumar enteros a fechas, restar fechas en dias y sumar intervalos a timestamps en PostgreSQL, MySQL y ClickHouse.

2 min de lecturaReferencesql · postgresql · dates · interval · timestamp

En PostgreSQL puedes sumar y restar fechas y marcas de tiempo como numeros, pero las reglas dependen del tipo: un date no se comporta como un timestamp. Entender la diferencia te ahorra los errores clasicos al calcular vencimientos, antiguedad y "cuanto hace que ocurrio" un evento.

Sumar dias a una fecha

El caso mas comun es sumar un entero a un date. PostgreSQL lo trata como dias:

SELECT DATE '2024-03-01' + 7  AS next_week;   -- 2024-03-08
SELECT DATE '2024-03-01' - 30 AS month_ago;   -- 2024-01-31

Es comodo para fechas de vencimiento: una factura suele pagarse N dias despues de crear el pedido.

SELECT id,
       created_at::date              AS placed_on,
       created_at::date + 14         AS due_date
FROM orders
WHERE status = 'pending';
  • Sumar un entero a un date suma dias.
  • No puedes sumar un entero a un timestamp — es un error de tipos, necesitas un interval.
  • Para "un mes despues" no escribas + 30: los meses tienen distinta longitud, usa INTERVAL '1 month'.

Diferencia de fechas frente a diferencia de timestamp

Aqui esta la trampa clave. Restar dos valores date da un numero entero de dias, mientras que restar dos valores timestamp da un interval.

SELECT DATE '2024-03-01' - DATE '2024-01-15'             AS days;   -- 46 (integer)
SELECT TIMESTAMP '2024-03-01 09:00'
     - TIMESTAMP '2024-01-15 18:30'                       AS span;   -- 45 days 14:30:00

Por eso la misma consulta se comporta distinto segun el tipo de las columnas. Para garantizar dias completos, convierte ambos operandos a date:

SELECT id,
       (NOW()::date - created_at::date) AS days_open
FROM orders
WHERE status = 'pending';

Cuidado: NOW() - created_at sobre dos timestamp devuelve un interval como 45 days 14:30:00, y compararlo con > 30 falla con un error de tipos. O conviertes a date, o comparas contra un intervalo: NOW() - created_at > INTERVAL '30 days'.

Intervalos para timestamps

A un timestamp le sumas un interval, no un numero. Los intervalos respetan el calendario: + INTERVAL '1 month' sobre el 31 de enero cae en el 28 (o 29) de febrero, no en "30 dias despues".

SELECT created_at + INTERVAL '7 days'   AS reminder_at,
       created_at + INTERVAL '1 month'  AS renew_at,
       created_at + INTERVAL '36 hours' AS grace_until
FROM users;

Los intervalos se suman y escalan, asi que una ventana de "ultimos seis meses" queda compacta:

SELECT id, email
FROM users
WHERE created_at >= NOW() - INTERVAL '6 months';

Antiguedad y edad en dias frente a meses

Para la antiguedad de un empleado a menudo quieres dias en bruto — entonces restas valores date. Para una edad legible en anos y meses usa AGE, que normaliza el intervalo por el calendario.

SELECT name, dept,
       NOW()::date - created_at::date   AS tenure_days,
       AGE(NOW(), created_at)           AS tenure_human
FROM employees
ORDER BY tenure_days DESC;

Recuerda: 365 dias no siempre es "un ano". Si necesitas anos cumplidos, calculalos con AGE y EXTRACT(YEAR ...) en lugar de dividir los dias entre 365.

Diferencias en otros motores

La sintaxis cambia bastante entre motores:

  • MySQL: restar fechas no te da dias. Usa DATEDIFF(end, start) para dias y DATE_ADD(d, INTERVAL 7 DAY) / DATE_SUB para desplazar. Un simple d + 7 no hace lo que esperas.
  • ClickHouse: usa date + 7 (dias) y las funciones dateDiff('day', start, end), addDays, addMonths; siempre indicas la unidad de forma explicita.

Si el codigo debe ser portable, no dependas de date - date: envuelve la logica en DATEDIFF/dateDiff y usa intervalos con nombre.

Practica con ejercicios reales

Resuelve ejercicios en el entrenador de SQL con corrección instantánea y pistas.

Abrir el entrenador