AT TIME ZONE e uma ferramenta com duas faces. O que ela faz depende inteiramente do tipo da entrada: para um timestamptz ela representa o momento como o horario de parede na zona escolhida, ja para um timestamp naive faz o oposto, declara que o valor ja estava naquela zona e devolve o instante absoluto.
Dois modos, um operador
A regra e curta: AT TIME ZONE sempre inverte o tipo para o oposto.
timestamptz AT TIME ZONE 'zona' -> timestamp (horario de parede naquela zona).
timestamp AT TIME ZONE 'zona' -> timestamptz (interpreta o valor naive como hora local daquela zona).
SELECT TIMESTAMPTZ '2026-06-17 12:00:00+00' AT TIME ZONE 'Europe/Moscow';
SELECT TIMESTAMP '2026-06-17 15:00:00' AT TIME ZONE 'Europe/Moscow';
As duas expressoes sao espelho uma da outra e fazem ida e volta sem perda. A primeira responde "que horas sao em Moscou neste momento"; a segunda responde "a qual instante absoluto correspondem as 15:00 de Moscou".
Convertendo UTC armazenado para a zona do usuario
O padrao canonico: voce armazena created_at como timestamptz (que internamente e UTC) e quer exibi-lo na zona de um usuario especifico.
SELECT
o.id,
o.amount,
o.created_at,
o.created_at AT TIME ZONE u.tz AS local_created_at
FROM orders o
JOIN users u ON u.id = o.user_id;
Aqui u.tz e uma coluna de texto com um nome de zona como 'America/Sao_Paulo'. Use sempre zonas IANA nomeadas, nunca deslocamentos fixos como '+03': um nome de zona conhece todo o historico de transicoes de horario de verao, um deslocamento cru nao.
Agrupar pedidos pelo "dia local" do usuario e igualmente direto, primeiro converter, depois truncar:
SELECT
DATE_TRUNC('day', o.created_at AT TIME ZONE u.tz) AS local_day,
SUM(o.amount) AS revenue
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.status = 'paid'
GROUP BY 1
ORDER BY 1;
Horario de verao, onde realmente morde
A principal razao para nao somar deslocamentos na mao e o DST. Pegue Nova York em torno da transicao de primavera de 2026, quando os relogios pulam das 02:00 direto para as 03:00.
SELECT TIMESTAMPTZ '2026-03-08 06:30:00+00' AT TIME ZONE 'America/New_York' AS before_dst,
TIMESTAMPTZ '2026-03-08 07:30:00+00' AT TIME ZONE 'America/New_York' AS after_dst;
Passou exatamente uma hora de tempo absoluto entre os dois momentos, mas o relogio de parede pulou de 01:30 para 03:30, a hora 02:xx simplesmente nao existe naquela noite. Um deslocamento fixo de -05 daria a resposta errada na segunda linha.
Pegadinhas
- Pegadinha: confusao de modos. Aplicar
AT TIME ZONE a um valor que ja e uma coluna timestamp naive nao o "converte", declara que ele estava naquela zona. Verifique primeiro o tipo da coluna.
- Aplicar duas vezes faz ida e volta.
(ts AT TIME ZONE 'Europe/Moscow') da um timestamp naive; aplique AT TIME ZONE de novo e voce recupera um timestamptz, as vezes util para "remudar de zona", mas com mais frequencia um bug.
- Use nomes IANA. Zonas como
'Europe/Moscow' sobrevivem a mudancas nas regras de DST; deslocamentos como '+03' nao.
MySQL e ClickHouse
Nenhum dos dois motores tem a sintaxe AT TIME ZONE. No MySQL use CONVERT_TZ(ts, 'UTC', 'Europe/Moscow') (as tabelas de zonas nomeadas precisam estar carregadas). No ClickHouse use toTimeZone(ts, 'Europe/Moscow') para um DateTime, lembrando que ali a zona e apenas um atributo de exibicao sobre um timestamp baseado em UTC.
AT TIME ZONEe uma ferramenta com duas faces. O que ela faz depende inteiramente do tipo da entrada: para umtimestamptzela representa o momento como o horario de parede na zona escolhida, ja para umtimestampnaive faz o oposto, declara que o valor ja estava naquela zona e devolve o instante absoluto.Dois modos, um operador
A regra e curta:
AT TIME ZONEsempre inverte o tipo para o oposto.timestamptz AT TIME ZONE 'zona'->timestamp(horario de parede naquela zona).timestamp AT TIME ZONE 'zona'->timestamptz(interpreta o valor naive como hora local daquela zona).-- timestamptz -> timestamp: what the clock on the wall in Moscow shows SELECT TIMESTAMPTZ '2026-06-17 12:00:00+00' AT TIME ZONE 'Europe/Moscow'; -- 2026-06-17 15:00:00 -- timestamp -> timestamptz: this naive value WAS Moscow local time SELECT TIMESTAMP '2026-06-17 15:00:00' AT TIME ZONE 'Europe/Moscow'; -- 2026-06-17 12:00:00+00As duas expressoes sao espelho uma da outra e fazem ida e volta sem perda. A primeira responde "que horas sao em Moscou neste momento"; a segunda responde "a qual instante absoluto correspondem as 15:00 de Moscou".
Convertendo UTC armazenado para a zona do usuario
O padrao canonico: voce armazena
created_atcomotimestamptz(que internamente e UTC) e quer exibi-lo na zona de um usuario especifico.SELECT o.id, o.amount, o.created_at, -- absolute moment (UTC) o.created_at AT TIME ZONE u.tz AS local_created_at -- wall time for the user FROM orders o JOIN users u ON u.id = o.user_id;Aqui
u.tze uma coluna de texto com um nome de zona como'America/Sao_Paulo'. Use sempre zonas IANA nomeadas, nunca deslocamentos fixos como'+03': um nome de zona conhece todo o historico de transicoes de horario de verao, um deslocamento cru nao.Agrupar pedidos pelo "dia local" do usuario e igualmente direto, primeiro converter, depois truncar:
SELECT DATE_TRUNC('day', o.created_at AT TIME ZONE u.tz) AS local_day, SUM(o.amount) AS revenue FROM orders o JOIN users u ON u.id = o.user_id WHERE o.status = 'paid' GROUP BY 1 ORDER BY 1;Horario de verao, onde realmente morde
A principal razao para nao somar deslocamentos na mao e o DST. Pegue Nova York em torno da transicao de primavera de 2026, quando os relogios pulam das 02:00 direto para as 03:00.
SELECT TIMESTAMPTZ '2026-03-08 06:30:00+00' AT TIME ZONE 'America/New_York' AS before_dst, TIMESTAMPTZ '2026-03-08 07:30:00+00' AT TIME ZONE 'America/New_York' AS after_dst; -- before_dst = 2026-03-08 01:30:00 (offset -05) -- after_dst = 2026-03-08 03:30:00 (offset -04, the 02:00 hour never exists)Passou exatamente uma hora de tempo absoluto entre os dois momentos, mas o relogio de parede pulou de 01:30 para 03:30, a hora 02:xx simplesmente nao existe naquela noite. Um deslocamento fixo de
-05daria a resposta errada na segunda linha.Pegadinhas
AT TIME ZONEa um valor que ja e uma colunatimestampnaive nao o "converte", declara que ele estava naquela zona. Verifique primeiro o tipo da coluna.(ts AT TIME ZONE 'Europe/Moscow')da umtimestampnaive; apliqueAT TIME ZONEde novo e voce recupera umtimestamptz, as vezes util para "remudar de zona", mas com mais frequencia um bug.'Europe/Moscow'sobrevivem a mudancas nas regras de DST; deslocamentos como'+03'nao.MySQL e ClickHouse
Nenhum dos dois motores tem a sintaxe
AT TIME ZONE. No MySQL useCONVERT_TZ(ts, 'UTC', 'Europe/Moscow')(as tabelas de zonas nomeadas precisam estar carregadas). No ClickHouse usetoTimeZone(ts, 'Europe/Moscow')para umDateTime, lembrando que ali a zona e apenas um atributo de exibicao sobre um timestamp baseado em UTC.