SQL para análise de dados · Guias · 30 de outubro de 2026 · 11 min de leitura

Funções de data no SQL: EXTRACT, date_trunc, INTERVAL e diferença entre datas

Um calendário de parede e um painel de partidas com horários sendo recortados em dias, semanas e meses

Quatro tipos de funções de data cobrem quase todas as tarefas de um analista. EXTRACT tira uma parte da data (o ano, o mês, a hora, o dia da semana). date_trunc arredonda um timestamp para baixo até o início do dia, da semana ou do mês, que é como você agrupa por período. Intervalos (+ INTERVAL '2 hours') movem uma data para a frente ou para trás. Subtração dá o tempo entre duas datas. Aprenda esses quatro e você monta a maioria dos relatórios baseados em tempo.

Todos os exemplos rodam no banco de dados de uma companhia aérea usado no curso de SQL da Nomto: 33.121 voos de 12 de julho a 10 de setembro de 2026 e 262.788 reservas, com horários no fuso de Moscou. As consultas usam a sintaxe do PostgreSQL e rodam no DuckDB, que é o motor da sala de SQL da Nomto; cada tabela de resultado é a saída real. Quando o PostgreSQL e o DuckDB dão nomes diferentes a uma função, o guia avisa.

DATE, TIMESTAMP e INTERVAL

Três tipos fazem o trabalho:

Tipo Guarda Exemplo
DATE um dia do calendário 2026-07-12
TIMESTAMP um dia e um horário 2026-07-12 09:35:00
INTERVAL uma duração 04:37:00, 1 month 24 days

Nos dados da companhia aérea, flights.scheduled_departure, flights.actual_departure e bookings.book_date são timestamps. Para transformar um timestamp em data, faça o cast: scheduled_departure::date (ou o padrão CAST(scheduled_departure AS DATE)). Os dois funcionam no PostgreSQL e no DuckDB.

Contagem diária de voos nos três primeiros dias de agosto:

SELECT scheduled_departure::date AS day,
       COUNT(*) AS flights
FROM flights
WHERE scheduled_departure >= '2026-08-01'
  AND scheduled_departure <  '2026-08-04'
GROUP BY 1
ORDER BY 1;
day flights
2026-08-01 568
2026-08-02 558
2026-08-03 536

Repare no filtro: “a partir do início do primeiro dia, antes do início do dia seguinte ao último”. Isso funciona com timestamps sem aplicar nenhuma função à coluna, o que também deixa o banco usar um índice. BETWEEN '2026-08-01' AND '2026-08-03' pararia à meia-noite do dia 3 de agosto e perderia, em silêncio, quase um dia inteiro.

EXTRACT: pegar o ano, o mês, a hora ou o dia da semana

EXTRACT(parte FROM valor) devolve um componente como número:

SELECT flight_no,
       scheduled_departure,
       EXTRACT(year  FROM scheduled_departure) AS yr,
       EXTRACT(month FROM scheduled_departure) AS mon,
       EXTRACT(day   FROM scheduled_departure) AS d,
       EXTRACT(hour  FROM scheduled_departure) AS hr,
       EXTRACT(dow   FROM scheduled_departure) AS dow
FROM flights
WHERE flight_id = 1;
flight_no scheduled_departure yr mon d hr dow
PG0405 2026-07-12 09:35:00 2026 7 12 9 0

12 de julho de 2026 é um domingo, e dow devolve 0 para ele: tanto no PostgreSQL quanto no DuckDB, dow vai de 0 (domingo) a 6 (sábado). Se você quer segunda = 1 e domingo = 7, use isodow. Outras partes úteis: quarter (trimestre), week (número da semana ISO), minute e epoch (segundos desde 1970, ou a duração de um intervalo em segundos; mais sobre isso abaixo).

O uso típico é agrupar por uma parte da data. Os horários de partida mais movimentados:

SELECT EXTRACT(hour FROM scheduled_departure) AS hour,
       COUNT(*) AS flights
FROM flights
GROUP BY 1
ORDER BY flights DESC
LIMIT 5;
hour flights
11 4004
14 3313
13 3223
9 3221
10 3046

E por dia da semana, começando na segunda:

SELECT EXTRACT(isodow FROM scheduled_departure) AS iso_dow,
       COUNT(*) AS flights
FROM flights
GROUP BY 1
ORDER BY 1;
iso_dow flights
1 4824
2 4905
3 4932
4 4950
5 3944
6 4544
7 5022

A sexta-feira é o dia mais tranquilo desta programação, e o domingo, o mais movimentado. Lembre que o período não tem um número inteiro de semanas, então alguns dias da semana aparecem uma vez a mais que outros. Para uma comparação justa, divida pela quantidade de cada dia da semana no intervalo.

PostgreSQL vs DuckDB. Os dois suportam EXTRACT(parte FROM x) e a forma de função date_part('parte', x). O PostgreSQL 14+ devolve numeric no EXTRACT e double precision no date_part; o DuckDB devolve inteiros. O DuckDB também tem atalhos como year(x), hour(x), dayname(x) e monthname(x), que o PostgreSQL não tem.

date_trunc: agrupar por dia, semana ou mês

EXTRACT(month ...) devolve 7 para julho de qualquer ano, o que fica errado quando seus dados cobrem mais de um ano. Para agrupar por período do calendário, arredonde o timestamp para baixo até o início do período com date_trunc('unidade', valor):

SELECT date_trunc('month', book_date) AS month,
       COUNT(*) AS bookings,
       ROUND(SUM(total_amount)) AS revenue
FROM bookings
GROUP BY 1
ORDER BY 1;
month bookings revenue
2026-06-01 00:00:00 24952 19647411
2026-07-01 00:00:00 171646 135972835
2026-08-01 00:00:00 66190 52049563

Junho e agosto são meses parciais aqui: as reservas começam em 17 de junho e os dados param em 11 de agosto. Sempre confira a primeira e a última data antes de comparar períodos.

Semanas funcionam do mesmo jeito, e date_trunc('week', ...) começa a semana na segunda-feira tanto no PostgreSQL quanto no DuckDB:

SELECT date_trunc('week', scheduled_departure) AS week,
       COUNT(*) AS flights
FROM flights
GROUP BY 1
ORDER BY 1
LIMIT 5;
week flights
2026-07-06 00:00:00 558
2026-07-13 00:00:00 3798
2026-07-20 00:00:00 3798
2026-07-27 00:00:00 3798
2026-08-03 00:00:00 3798

A primeira semana só tem o domingo, 12 de julho, e é por isso que ela tem 558 voos. Outras unidades: 'day', 'hour', 'quarter', 'year'.

date_trunc sobre um timestamp devolve um timestamp. Acrescente ::date se quiser uma data limpa no relatório. Repare que um período sem linhas simplesmente não aparece; se você precisa de todos os dias ou meses na saída, inclusive os vazios, monte um calendário antes, como mostra o guia de date spine (em inglês).

Somar e subtrair intervalos

Some ou subtraia um INTERVAL para mover um timestamp:

SELECT flight_no,
       scheduled_departure,
       scheduled_departure + INTERVAL '2 hours' AS plus_2h,
       scheduled_departure - INTERVAL '1 day'   AS minus_1d,
       CAST(scheduled_departure AS DATE) + 7    AS week_later
FROM flights
WHERE flight_id = 1;
flight_no scheduled_departure plus_2h minus_1d week_later
PG0405 2026-07-12 09:35:00 2026-07-12 11:35:00 2026-07-11 09:35:00 2026-07-19

Somar um inteiro simples a um DATE soma dias, nos dois bancos. Num timestamp, use um intervalo.

A aritmética de meses pede cuidado, porque os meses têm tamanhos diferentes. Somar um mês a 31 de janeiro não transborda para março; para no último dia de fevereiro:

SELECT DATE '2026-01-31' + INTERVAL '1 month' AS a;
a
2026-02-28 00:00:00

A mesma ideia dá o último dia de um mês: início do mês, mais um mês, menos um dia.

SELECT d,
       date_trunc('month', d) + INTERVAL '1 month' - INTERVAL '1 day' AS month_end,
       last_day(d) AS ld
FROM (SELECT DATE '2026-02-10' AS d);
d month_end ld
2026-02-10 2026-02-28 00:00:00 2026-02-28

last_day existe no DuckDB (e no MySQL), mas não no PostgreSQL; a versão com date_trunc funciona em qualquer lugar.

Intervalos também são a forma de escrever filtros de “últimos N dias”. Neste conjunto de dados, o retrato foi tirado em 11 de agosto de 2026 às 18:00, então use esse momento em vez de now(): WHERE book_date >= TIMESTAMP '2026-08-11 18:00' - INTERVAL '7 days'. Num banco ao vivo, now() e current_date funcionam no PostgreSQL e no DuckDB.

Calcular dias e minutos entre duas datas

Subtrair um timestamp de outro dá um intervalo. Os maiores atrasos de partida:

SELECT flight_no,
       scheduled_departure,
       actual_departure,
       actual_departure - scheduled_departure AS delay,
       EXTRACT(epoch FROM actual_departure - scheduled_departure) / 60 AS delay_min
FROM flights
WHERE actual_departure IS NOT NULL
ORDER BY delay DESC
LIMIT 3;
flight_no scheduled_departure actual_departure delay delay_min
PG0589 2026-07-25 15:30:00 2026-07-25 20:07:00 04:37:00 277
PG0164 2026-07-25 15:25:00 2026-07-25 19:53:00 04:28:00 268
PG0364 2026-07-15 11:45:00 2026-07-15 16:12:00 04:27:00 267

Um intervalo como 04:37:00 é fácil de ler, mas você não consegue tirar a média dele num KPI nem compará-lo com um número. EXTRACT(epoch FROM intervalo) o transforma em segundos; divida por 60 para minutos ou por 3600 para horas. Isso funciona igual no PostgreSQL e no DuckDB, então é o hábito mais seguro. Atraso médio por mês:

SELECT date_trunc('month', scheduled_departure)::date AS month,
       COUNT(*) AS departed,
       ROUND(AVG(EXTRACT(epoch FROM actual_departure - scheduled_departure) / 60), 1) AS avg_delay_min
FROM flights
WHERE actual_departure IS NOT NULL
GROUP BY 1
ORDER BY 1;
month departed avg_delay_min
2026-07-01 10826 12.8
2026-08-01 5947 11.2

Para dias inteiros, subtraia duas datas. DATE - DATE devolve um número inteiro de dias nos dois bancos. Com quanta antecedência os passageiros reservam? Cada linha abaixo é um trecho de voo de uma reserva:

SELECT b.book_ref,
       b.book_date,
       f.scheduled_departure,
       f.scheduled_departure::date - b.book_date::date AS days_ahead
FROM bookings b
JOIN tickets t         USING (book_ref)
JOIN ticket_flights tf USING (ticket_no)
JOIN flights f         USING (flight_id)
ORDER BY b.book_ref, f.scheduled_departure
LIMIT 5;
book_ref book_date scheduled_departure days_ahead
00000F 2026-07-01 03:12:00 2026-07-12 17:15:00 11
00000F 2026-07-01 03:12:00 2026-07-20 10:45:00 19
000012 2026-07-10 09:02:00 2026-07-24 17:05:00 14
000012 2026-07-10 09:02:00 2026-08-05 18:15:00 26
000068 2026-08-11 14:27:00 2026-08-24 10:30:00 13

Separe a diferença em faixas com CASE e você tem um relatório de antecedência de reserva:

SELECT CASE WHEN days < 7  THEN '0-6 days'
            WHEN days < 14 THEN '7-13 days'
            WHEN days < 30 THEN '14-29 days'
            ELSE '30+ days' END AS booked_ahead,
       COUNT(*) AS segments
FROM (
  SELECT f.scheduled_departure::date - b.book_date::date AS days
  FROM bookings b
  JOIN tickets t         USING (book_ref)
  JOIN ticket_flights tf USING (ticket_no)
  JOIN flights f         USING (flight_id)
) s
GROUP BY 1
ORDER BY MIN(days);
booked_ahead segments
0-6 days 125
7-13 days 101479
14-29 days 891131
30+ days 52991

O guia de CASE WHEN no SQL explica por que a ordem dos ramos aqui vai do menor limite para o maior.

date_diff, DATEDIFF e age

Cada banco oferece atalhos diferentes, e é aqui que consultas copiadas quebram:

Tarefa PostgreSQL DuckDB MySQL / SQL Server
Dias entre duas datas d2 - d1 (datas) d2 - d1 ou date_diff('day', t1, t2) DATEDIFF(...)
Minutos entre timestamps EXTRACT(epoch FROM t2 - t1) / 60 igual, ou date_diff('minute', t1, t2) TIMESTAMPDIFF / DATEDIFF(minute, ...)
Anos, meses e dias entre age(t2, t1) age(t2, t1) sem equivalente direto
Formatar como texto to_char(t, 'YYYY-MM') strftime(t, '%Y-%m') DATE_FORMAT / FORMAT

O PostgreSQL não tem date_diff nem DATEDIFF, e o DuckDB não tem to_char. O age devolve um intervalo que respeita o calendário: age(TIMESTAMP '2026-08-11 18:00', TIMESTAMP '2026-06-17 00:00') dá 1 month 24 days 18:00:00 no DuckDB (o PostgreSQL imprime 1 mon 24 days 18:00:00).

Uma armadilha do date_diff do DuckDB: ele conta quantas fronteiras foram cruzadas, e não quantas unidades inteiras se passaram:

SELECT date_diff('day', TIMESTAMP '2026-07-12 23:50', TIMESTAMP '2026-07-13 00:10') AS dd;
dd
1

Vinte minutos, um “dia”. Para dias do calendário (reservou no dia 10, voou no dia 24), é exatamente o que você quer. Para tempo decorrido (o atraso passou de um dia?), calcule o intervalo e use EXTRACT(epoch ...).

Erros comuns

Datas costumam aparecer junto com agrupamentos e totais acumulados; veja GROUP BY e HAVING e o guia de running total no SQL (em inglês). Colunas de texto têm um kit próprio, tratado em funções de texto no SQL.

Perguntas frequentes

Como pegar só a data de um timestamp no SQL?

Faça o cast: scheduled_departure::date ou CAST(scheduled_departure AS DATE). Os dois funcionam no PostgreSQL e no DuckDB. date_trunc('day', ...) também descarta o horário, mas mantém o tipo timestamp.

Qual a diferença entre EXTRACT e date_trunc?

EXTRACT devolve uma parte da data como número (7 para julho). date_trunc devolve um timestamp completo arredondado para baixo até o início do período (2026-07-01 00:00:00). Use EXTRACT para perguntas de sazonalidade (“qual hora é a mais movimentada?”) e date_trunc para séries temporais (“reservas por mês”).

Como calcular a quantidade de dias entre duas datas no PostgreSQL?

Subtraia duas datas: end_date - start_date devolve um inteiro. Se as colunas forem timestamps, faça o cast antes (t2::date - t1::date) para dias do calendário, ou use EXTRACT(epoch FROM t2 - t1) / 86400 para dias decorridos com fração.

date_trunc(‘week’) começa na segunda ou no domingo?

Na segunda, no PostgreSQL e no DuckDB. Para semanas que começam no domingo, desloque a data: date_trunc('week', d + INTERVAL '1 day') - INTERVAL '1 day'.

Existe DATEADD no PostgreSQL?

Não. Some um intervalo: order_date + INTERVAL '3 days', ou + 3 numa coluna DATE.

Outros idiomas: English, Español, Tiếng Việt, 简体中文