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

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
- Agrupar por
EXTRACT(month ...)com dados de vários anos. Julho de 2025 e julho de 2026 acabam no mesmo grupo. Usedate_trunc('month', ...). BETWEENcom timestamps. O limite superior é a meia-noite do início do último dia. Use>= início AND < próximo_início.- Envolver a coluna numa função dentro do
WHERE.WHERE date_trunc('day', scheduled_departure) = '2026-08-01'está correto, mas obriga o banco a calcular a função em cada linha. Um intervalo sobre a coluna bruta é equivalente e pode usar um índice. - Tirar média de intervalos para um KPI. Converta antes para minutos ou horas com
EXTRACT(epoch ...), para que o número seja fácil de ler e comparar. - Usar
now()num retrato dos dados. Conjuntos de dados de treino e históricos têm o próprio “hoje”. Fixe no código o momento do retrato. - Esquecer os fusos horários. Todos os horários deste conjunto de dados estão no fuso de Moscou, gravados como timestamps simples. Em dados reais, confira se a coluna é
timestampoutimestamptze em que fuso os eventos foram registrados antes de extrair a hora.
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, 简体中文