sqlpostgresqldatesinterval

Aritmetica de datas no SQL: somar dias, subtrair datas e intervalos

Como somar inteiros a datas, subtrair datas em dias e somar intervalos a timestamps no PostgreSQL, MySQL e ClickHouse.

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

No PostgreSQL voce pode somar e subtrair datas e marcas de tempo como numeros, mas as regras dependem do tipo: um date nao se comporta como um timestamp. Entender essa diferenca evita os bugs classicos no calculo de vencimentos, tempo de casa e "ha quanto tempo" um evento aconteceu.

Somar dias a uma data

O caso mais comum e somar um inteiro a um date. O PostgreSQL trata isso 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

E pratico para datas de vencimento: uma fatura costuma ser paga N dias depois de o pedido ser criado.

SELECT id,
       created_at::date              AS placed_on,
       created_at::date + 14         AS due_date
FROM orders
WHERE status = 'pending';
  • Somar um inteiro a um date soma dias.
  • Voce nao pode somar um inteiro a um timestamp — isso e erro de tipo, e preciso um interval.
  • Para "um mes depois" nao escreva + 30: os meses tem tamanhos diferentes, use INTERVAL '1 month'.

Diferenca de datas versus diferenca de timestamp

Aqui esta a pegadinha principal. Subtrair dois valores date resulta em um numero inteiro de dias, enquanto subtrair dois valores timestamp resulta em um 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 isso a mesma consulta se comporta de forma diferente conforme o tipo das colunas. Para garantir dias inteiros, converta os dois operandos para date:

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

Pegadinha: NOW() - created_at sobre dois timestamp devolve um interval como 45 days 14:30:00, e compara-lo com > 30 falha com erro de tipo. Ou converte para date, ou compara contra um intervalo: NOW() - created_at > INTERVAL '30 days'.

Intervalos para timestamps

A um timestamp voce soma um interval, e nao um numero. Os intervalos respeitam o calendario: + INTERVAL '1 month' sobre 31 de janeiro cai em 28 (ou 29) de fevereiro, e nao em "30 dias depois".

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;

Os intervalos somam e escalam, entao uma janela de "ultimos seis meses" fica compacta:

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

Tempo de casa e idade em dias versus meses

Para o tempo de casa de um funcionario voce costuma querer dias brutos — entao subtraia valores date. Para uma idade legivel em anos e meses use AGE, que normaliza o intervalo pelo 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;

Lembre-se: 365 dias nem sempre e "um ano". Se voce precisa de anos completos, calcule-os com AGE e EXTRACT(YEAR ...) em vez de dividir os dias por 365.

Diferencas em outros bancos

A sintaxe muda bastante entre os bancos:

  • MySQL: subtrair datas nao devolve dias. Use DATEDIFF(end, start) para dias e DATE_ADD(d, INTERVAL 7 DAY) / DATE_SUB para deslocar. Um simples d + 7 nao faz o que voce espera.
  • ClickHouse: use date + 7 (dias) e as funcoes dateDiff('day', start, end), addDays, addMonths; voce sempre informa a unidade de forma explicita.

Se o codigo precisa ser portavel, nao dependa de date - date: envolva a logica em DATEDIFF/dateDiff e use intervalos nomeados.

Pratique com exercícios reais

Resolva exercícios no treinador de SQL com correção instantânea e dicas.

Abrir o treinador