DATE_TRUNC descarta qualquer parte de um timestamp mais fina que o periodo que voce pedir e devolve o inicio desse periodo. E o cavalo de batalha de toda analise temporal: linhas com precisao de milissegundos se agrupam em baldes limpos por hora, dia, semana, mes ou ano.
O que o DATE_TRUNC faz de fato
O primeiro argumento e o campo ('hour', 'day', 'week', 'month', 'year', etc.); o segundo e um timestamp ou uma date. O resultado e arredondado para baixo ate o inicio do periodo, zerando as unidades menores.
SELECT
DATE_TRUNC('month', TIMESTAMP '2026-06-17 14:32:09') AS month_start,
DATE_TRUNC('day', TIMESTAMP '2026-06-17 14:32:09') AS day_start,
DATE_TRUNC('hour', TIMESTAMP '2026-06-17 14:32:09') AS hour_start;
Propriedades principais:
- Sempre trunca para baixo, nunca para o mais proximo:
14:32 com 'hour' da 14:00.
- Retorna o mesmo tipo
timestamp, entao fica facil comparar e agrupar.
'week' no PostgreSQL comeca na segunda-feira (ISO), uma surpresa comum se voce esperava domingo.
Agrupando uma serie temporal em baldes
O caso de uso principal e calcular uma metrica por periodo. Quantos usuarios se cadastraram a cada mes?
SELECT
DATE_TRUNC('month', created_at) AS month,
COUNT(*) AS signups
FROM users
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY month;
A mesma ideia para receita diaria, agora a partir de orders e filtrando por status:
SELECT
DATE_TRUNC('day', created_at) AS day,
SUM(amount) AS revenue
FROM orders
WHERE status = 'paid'
GROUP BY DATE_TRUNC('day', created_at)
ORDER BY day;
Agrupe pela expressao DATE_TRUNC(...), e nao pelo created_at cru, senao cada segundo distinto vira uma linha propria.
GROUP BY so retorna periodos que tem dados. Se um dia nao teve pedidos, a linha simplesmente desaparece e seu grafico mostra uma linha quebrada. Para ter um eixo continuo, gere um calendario e faca um LEFT JOIN contra ele.
SELECT
cal.day,
COALESCE(SUM(o.amount), 0) AS revenue
FROM generate_series(
DATE_TRUNC('day', DATE '2026-06-01'),
DATE_TRUNC('day', DATE '2026-06-30'),
INTERVAL '1 day'
) AS cal(day)
LEFT JOIN orders o
ON DATE_TRUNC('day', o.created_at) = cal.day
AND o.status = 'paid'
GROUP BY cal.day
ORDER BY cal.day;
Agora todos os 30 dias aparecem, e os dias sem pedidos relatam honestamente um 0.
Fusos horarios: a grande pegadinha
O DATE_TRUNC(campo, ts) de dois argumentos nao calcula em UTC. Quando ts e um timestamptz, o PostgreSQL trunca conforme o ajuste TimeZone da sessao, entao a mesma consulta pode colocar um pedido em dias diferentes do calendario dependendo de quem a executa. Um pedido as 2026-06-01 01:30 UTC ja pertence ao dia 1 de junho para uma sessao em Sao Paulo, mas para uma em UTC-5 ainda e 31 de maio. Isso desloca seus baldes mensais e diarios bem na fronteira do dia.
Por isso vale sempre fixar o fuso em vez de confiar no ajuste da sessao. Converta com AT TIME ZONE primeiro e so depois trunque:
SELECT
DATE_TRUNC('day', created_at AT TIME ZONE 'America/Sao_Paulo') AS day_br,
COUNT(*)
FROM orders
GROUP BY DATE_TRUNC('day', created_at AT TIME ZONE 'America/Sao_Paulo');
O PostgreSQL moderno (14+) tambem oferece uma forma de tres argumentos, DATE_TRUNC(campo, ts, fuso), que trunca um timestamptz no fuso que voce indicar e elimina qualquer duvida sobre o ajuste da sessao:
SELECT
DATE_TRUNC('day', created_at, 'America/Sao_Paulo') AS day_br,
COUNT(*)
FROM orders
GROUP BY DATE_TRUNC('day', created_at, 'America/Sao_Paulo');
Para um passo arbitrario (digamos 15 minutos) use date_bin('15 minutes', created_at, TIMESTAMP '2026-01-01').
Equivalentes em MySQL e ClickHouse
DATE_TRUNC nao e universal, e a sintaxe varia.
- MySQL nao tem
DATE_TRUNC. Trunque para mes com DATE_FORMAT e para dia com DATE().
SELECT
DATE_FORMAT(created_at, '%Y-%m-01') AS month,
COUNT(*) AS signups
FROM users
GROUP BY DATE_FORMAT(created_at, '%Y-%m-01');
- ClickHouse traz funcoes dedicadas:
toStartOfMonth, toStartOfDay, toStartOfHour, e toStartOfInterval para um passo arbitrario.
SELECT
toStartOfMonth(created_at) AS month,
count() AS signups
FROM users
GROUP BY month
ORDER BY month;
Lembre-se de uma coisa: a intencao e identica em todo lugar, zerar a cauda do timestamp para que as linhas se encaixem em baldes. So muda o nome da funcao.
DATE_TRUNCdescarta qualquer parte de um timestamp mais fina que o periodo que voce pedir e devolve o inicio desse periodo. E o cavalo de batalha de toda analise temporal: linhas com precisao de milissegundos se agrupam em baldes limpos por hora, dia, semana, mes ou ano.O que o DATE_TRUNC faz de fato
O primeiro argumento e o campo (
'hour','day','week','month','year', etc.); o segundo e umtimestampou umadate. O resultado e arredondado para baixo ate o inicio do periodo, zerando as unidades menores.SELECT DATE_TRUNC('month', TIMESTAMP '2026-06-17 14:32:09') AS month_start, DATE_TRUNC('day', TIMESTAMP '2026-06-17 14:32:09') AS day_start, DATE_TRUNC('hour', TIMESTAMP '2026-06-17 14:32:09') AS hour_start; -- 2026-06-01 00:00:00 | 2026-06-17 00:00:00 | 2026-06-17 14:00:00Propriedades principais:
14:32com'hour'da14:00.timestamp, entao fica facil comparar e agrupar.'week'no PostgreSQL comeca na segunda-feira (ISO), uma surpresa comum se voce esperava domingo.Agrupando uma serie temporal em baldes
O caso de uso principal e calcular uma metrica por periodo. Quantos usuarios se cadastraram a cada mes?
SELECT DATE_TRUNC('month', created_at) AS month, COUNT(*) AS signups FROM users GROUP BY DATE_TRUNC('month', created_at) ORDER BY month;A mesma ideia para receita diaria, agora a partir de
orderse filtrando por status:SELECT DATE_TRUNC('day', created_at) AS day, SUM(amount) AS revenue FROM orders WHERE status = 'paid' GROUP BY DATE_TRUNC('day', created_at) ORDER BY day;Agrupe pela expressao
DATE_TRUNC(...), e nao pelocreated_atcru, senao cada segundo distinto vira uma linha propria.Preenchendo lacunas com generate_series
GROUP BYso retorna periodos que tem dados. Se um dia nao teve pedidos, a linha simplesmente desaparece e seu grafico mostra uma linha quebrada. Para ter um eixo continuo, gere um calendario e faca umLEFT JOINcontra ele.SELECT cal.day, COALESCE(SUM(o.amount), 0) AS revenue FROM generate_series( DATE_TRUNC('day', DATE '2026-06-01'), DATE_TRUNC('day', DATE '2026-06-30'), INTERVAL '1 day' ) AS cal(day) LEFT JOIN orders o ON DATE_TRUNC('day', o.created_at) = cal.day AND o.status = 'paid' GROUP BY cal.day ORDER BY cal.day;Agora todos os 30 dias aparecem, e os dias sem pedidos relatam honestamente um
0.Fusos horarios: a grande pegadinha
O
DATE_TRUNC(campo, ts)de dois argumentos nao calcula em UTC. Quandotse umtimestamptz, o PostgreSQL trunca conforme o ajusteTimeZoneda sessao, entao a mesma consulta pode colocar um pedido em dias diferentes do calendario dependendo de quem a executa. Um pedido as2026-06-01 01:30 UTCja pertence ao dia 1 de junho para uma sessao em Sao Paulo, mas para uma em UTC-5 ainda e 31 de maio. Isso desloca seus baldes mensais e diarios bem na fronteira do dia.Por isso vale sempre fixar o fuso em vez de confiar no ajuste da sessao. Converta com
AT TIME ZONEprimeiro e so depois trunque:SELECT DATE_TRUNC('day', created_at AT TIME ZONE 'America/Sao_Paulo') AS day_br, COUNT(*) FROM orders GROUP BY DATE_TRUNC('day', created_at AT TIME ZONE 'America/Sao_Paulo');O PostgreSQL moderno (14+) tambem oferece uma forma de tres argumentos,
DATE_TRUNC(campo, ts, fuso), que trunca umtimestamptzno fuso que voce indicar e elimina qualquer duvida sobre o ajuste da sessao:SELECT DATE_TRUNC('day', created_at, 'America/Sao_Paulo') AS day_br, COUNT(*) FROM orders GROUP BY DATE_TRUNC('day', created_at, 'America/Sao_Paulo');Para um passo arbitrario (digamos 15 minutos) use
date_bin('15 minutes', created_at, TIMESTAMP '2026-01-01').Equivalentes em MySQL e ClickHouse
DATE_TRUNCnao e universal, e a sintaxe varia.DATE_TRUNC. Trunque para mes comDATE_FORMATe para dia comDATE().SELECT DATE_FORMAT(created_at, '%Y-%m-01') AS month, COUNT(*) AS signups FROM users GROUP BY DATE_FORMAT(created_at, '%Y-%m-01');toStartOfMonth,toStartOfDay,toStartOfHour, etoStartOfIntervalpara um passo arbitrario.SELECT toStartOfMonth(created_at) AS month, count() AS signups FROM users GROUP BY month ORDER BY month;Lembre-se de uma coisa: a intencao e identica em todo lugar, zerar a cauda do timestamp para que as linhas se encaixem em baldes. So muda o nome da funcao.