21 de jan. de 2026·7 min de leitura

TIMESTAMPTZ vs TIMESTAMP: dashboards e APIs no PostgreSQL

TIMESTAMPTZ vs TIMESTAMP no PostgreSQL: como o tipo escolhido afeta dashboards, respostas de API, conversões de fuso horário e bugs de horário de verão.

TIMESTAMPTZ vs TIMESTAMP: dashboards e APIs no PostgreSQL

O problema real: um evento, muitas interpretações

Um evento acontece uma vez, mas é reportado de uma dúzia de maneiras diferentes. O banco de dados armazena um valor, uma API o serializa, um dashboard o agrupa e cada pessoa o vê no seu próprio fuso horário. Se qualquer camada fizer uma suposição diferente, a mesma linha pode parecer dois momentos distintos.

Por isso TIMESTAMPTZ vs TIMESTAMP não é apenas uma preferência de tipo de dado. Isso decide se um valor armazenado representa um instante específico no tempo ou um horário de relógio que só faz sentido em um lugar específico.

O que costuma quebrar primeiro é simples: um dashboard de vendas mostra totais diários diferentes em Nova York e Berlim. Um gráfico horário tem uma hora faltando ou uma hora duplicada durante mudanças de horário de verão (DST). Um log de auditoria parece fora de ordem porque dois sistemas “concordam” na data, mas não no instante real.

Um modelo simples evita problemas:

  • Storage: o que você salva no PostgreSQL e o que isso representa.
  • Display: como você formata isso em uma interface, exportação ou relatório.
  • User locale: o fuso horário e as regras de calendário do visualizador, incluindo DST.

Misture essas coisas e você terá bugs silenciosos de relatório. Um time de suporte exporta “tickets criados ontem” de um dashboard e compara com um relatório da API. Ambos parecem razoáveis, mas um usou o limite de meia-noite local do visualizador enquanto o outro usou UTC.

O objetivo é simples: para cada valor de tempo, tome duas decisões claras. Decida o que você armazena e decida o que você mostra. Essa mesma clareza precisa atravessar seu modelo de dados, respostas de API e dashboards para que todos vejam a mesma linha do tempo.

O que TIMESTAMP e TIMESTAMPTZ realmente significam

No PostgreSQL, os nomes são enganosos. Eles parecem descrever o que é armazenado, mas na prática descrevem como o PostgreSQL interpreta a entrada e formata a saída.

TIMESTAMP (também timestamp without time zone) é apenas uma data do calendário e hora do relógio, como 2026-01-29 09:00:00. Nenhum fuso horário está anexado. PostgreSQL não converte isso para você. Duas pessoas em fusos horários diferentes podem ler o mesmo TIMESTAMP e assumir momentos reais diferentes.

TIMESTAMPTZ (também timestamp with time zone) representa um ponto real no tempo. Pense nele como um instante. PostgreSQL o normaliza internamente (efetivamente para UTC) e então o exibe no fuso horário que sua sessão está usando.

O comportamento por trás da maioria das surpresas é:

  • On input: PostgreSQL converte valores TIMESTAMPTZ para um único instante comparável.
  • On output: PostgreSQL formata esse instante usando o fuso horário da sessão atual.
  • For TIMESTAMP: nenhuma conversão automática acontece na entrada ou na saída.

Um pequeno exemplo mostra a diferença. Suponha que seu app receba 2026-03-08 02:30 de um usuário. Se você inserir isso em uma coluna TIMESTAMP, o PostgreSQL armazena exatamente esse valor de relógio. Se esse horário local não existe por causa de um salto de DST, você pode não notar até que os relatórios quebrem.

Se você inserir em TIMESTAMPTZ, o PostgreSQL precisa de um fuso horário para interpretar o valor. Se você fornecer 2026-03-08 02:30 America/New_York, o PostgreSQL converte para um instante (ou lança um erro dependendo das regras e do valor exato). Depois, um dashboard em Londres mostrará um horário de relógio local diferente, mas será o mesmo instante.

Uma concepção errada comum: as pessoas veem “with time zone” e esperam que o PostgreSQL armazene o rótulo do fuso original. Não armazena. O PostgreSQL guarda o momento, não o rótulo. Se você precisa do fuso original do usuário para exibição (por exemplo, “mostrar no horário local do cliente”), armazene a zona separadamente como um campo de texto.

Fuso horário da sessão: a configuração oculta por trás de muitas surpresas

PostgreSQL tem uma configuração que muda silenciosamente o que você vê: o fuso horário da sessão. Duas pessoas podem executar a mesma consulta nos mesmos dados e obter horários de relógio diferentes porque as sessões usam fusos distintos.

Isso afeta principalmente TIMESTAMPTZ. PostgreSQL armazena um momento absoluto e então o exibe no fuso horário da sessão. Com TIMESTAMP (sem fuso), PostgreSQL trata o valor como tempo de calendário simples. Ele não o desloca para exibição, mas o fuso horário da sessão ainda pode atrapalhar quando você o converte para TIMESTAMPTZ ou o compara com valores sensíveis a fuso.

Fusos de sessão frequentemente são definidos sem você notar: configuração na inicialização do app, parâmetros do driver, pools de conexão reutilizando sessões antigas, ferramentas de BI com seus próprios padrões, jobs de ETL herdando configurações de locale do servidor ou consoles SQL manuais usando as preferências do seu laptop.

É assim que times acabam discutindo. Suponha que um evento esteja armazenado como 2026-03-08 01:30:00+00 em uma coluna TIMESTAMPTZ. Uma sessão de dashboard em America/Los_Angeles vai exibí-lo como o horário local da noite anterior, enquanto uma sessão de API em UTC mostra um horário de relógio diferente. Se um gráfico agrupa por dia usando o dia local da sessão, você pode obter totais diários diferentes.

-- Make your output consistent for a reporting job
SET TIME ZONE 'UTC';

SELECT created_at, date_trunc('day', created_at) AS day_bucket
FROM events;

Para qualquer coisa que produza relatórios ou respostas de API, torne o fuso horário explícito. Defina-o na conexão (ou execute SET TIME ZONE primeiro), escolha um padrão para saídas de máquina (frequentemente UTC) e para relatórios em “horário comercial local” defina a zona do negócio dentro do job, não no laptop de alguém. Se usar conexões em pool, resete configurações de sessão quando uma conexão for retirada do pool.

Como dashboards quebram: agrupamentos, buckets e lacunas do DST

Dashboards parecem simples: contar pedidos por dia, mostrar cadastros por hora, comparar semana a semana. Os problemas começam quando o banco armazena um “momento” mas o gráfico o transforma em muitos “dias”, dependendo de quem está olhando.

Se você agrupar por dia usando o fuso horário local do usuário, duas pessoas podem ver datas diferentes para o mesmo evento. Um pedido feito às 23:30 em Los Angeles já é “amanhã” em Berlim. E se seu SQL agrupa por DATE(created_at) em um TIMESTAMP simples, você não está agrupando por um momento real. Está agrupando por uma leitura de relógio sem fuso horário.

Gráficos horários ficam mais complicados ao redor do DST. Na primavera, uma hora local nunca acontece, então gráficos podem mostrar uma lacuna. No outono, uma hora local acontece duas vezes, então você pode obter um pico ou buckets duplicados se sua consulta e o dashboard discordarem sobre qual 01:30 você quer dizer.

Uma pergunta prática ajuda: você está plotando momentos reais (seguros para converter) ou um horário de agendamento local (não deve ser convertido)? Dashboards quase sempre querem momentos reais.

Quando agrupar por UTC vs por fuso horário do negócio

Escolha uma regra de agrupamento e aplique-a em todos os lugares (SQL, API, ferramenta de BI), caso contrário os totais divergem.

Agrupe por UTC quando quiser uma série global e consistente (saúde do sistema, tráfego da API, cadastros globais). Agrupe por um fuso horário do negócio quando “o dia” tiver um significado legal ou operacional (dia da loja, SLAs de suporte, fechamento financeiro). Agrupe pelo fuso horário do visualizador apenas quando a personalização for mais importante que a comparabilidade (feeds de atividade pessoal).

Aqui está o padrão para um agrupamento consistente por “dia do negócio":

SELECT date_trunc('day', created_at AT TIME ZONE 'America/New_York') AS business_day,
       count(*)
FROM orders
GROUP BY 1
ORDER BY 1;

Rótulos que evitam desconfiança

As pessoas param de confiar em gráficos quando números pulam e ninguém consegue explicar por quê. Rotule a regra diretamente na UI: “Daily orders (America/New_York)” ou “Hourly events (UTC)”. Use a mesma regra em exportações e APIs.

Um conjunto simples de regras para relatórios e APIs

Show the right local time
Build web and mobile interfaces that display times in the user’s locale correctly.
Create App

Decida se você está armazenando um instante ou uma leitura do relógio local. Misturar esses dois é onde dashboards e APIs começam a discordar.

Um conjunto de regras que mantém relatórios previsíveis:

  • Armazene eventos do mundo real como instantes usando TIMESTAMPTZ, e trate UTC como a fonte da verdade.
  • Armazene conceitos de negócio como “billing day” separadamente como DATE (ou um campo de horário local se realmente precisar do horário de parede).
  • Em APIs, retorne timestamps em ISO 8601 e seja consistente: sempre inclua um offset (como +02:00) ou sempre use Z para UTC.
  • Converta nas bordas (UI e camada de relatórios). Evite converter para frente e para trás dentro de lógica de banco e jobs em background.

Por que isso dá certo: dashboards fazem buckets e comparam intervalos. Se você armazenar instantes (TIMESTAMPTZ), PostgreSQL pode ordenar e filtrar eventos de forma confiável mesmo quando há mudanças de DST. Então você decide como exibi-los ou agrupá-los. Se você armazenar um horário de relógio local (TIMESTAMP) sem fuso, PostgreSQL não pode saber o que isso significa, então o agrupamento pode mudar quando o fuso da sessão mudar.

Mantenha “datas comerciais locais” separadas porque elas não são instantes. “Entregar em 2026-03-08” é uma decisão de data, não um momento. Se você forçá-la em um timestamp, dias com DST podem criar horas locais faltando ou duplicadas, que depois aparecem como lacunas ou picos.

Passo a passo: escolher o tipo certo para cada valor de tempo

Control reporting time zones
Run jobs and reports with an explicit time zone to avoid hidden session settings.
Set UTC

Escolher entre TIMESTAMPTZ vs TIMESTAMP começa com uma pergunta: esse valor descreve um momento real que aconteceu, ou um horário local que você quer manter exatamente como escrito?

1) Separe eventos reais de horários agendados locais

Faça um inventário rápido de suas colunas.

Eventos reais (cliques, pagamentos, logins, envios, leituras de sensores, mensagens de suporte) geralmente devem ser armazenados como TIMESTAMPTZ. Você quer um instante sem ambiguidade, mesmo se as pessoas o virem de fusos horários diferentes.

Horários agendados locais são diferentes: “Loja abre às 09:00”, “Janela de pickup é das 16:00 às 18:00”, “Faturamento roda no dia 1 às 10:00 horário local”. Esses geralmente ficam melhores como TIMESTAMP mais um campo separado de fuso horário, porque a intenção está ligada ao relógio do local.

2) Escolha um padrão e documente

Para a maioria dos produtos, um bom padrão é: armazene tempos de eventos em UTC, apresente-os no fuso do usuário. Documente isso em lugares que as pessoas realmente leem: notas do esquema, docs da API e descrições dos dashboards. Também defina o que “dia do negócio” significa (dia UTC, dia do fuso do negócio ou dia local do visualizador), porque essa escolha direciona relatórios diários.

Uma checklist curta que funciona na prática:

  • Marque cada coluna de tempo como “evento instantâneo” ou “agendamento local”.
  • Padronize instantes de evento para TIMESTAMPTZ armazenados em UTC.
  • Ao mudar esquemas, faça backfill com cuidado e valide linhas amostrais manualmente.
  • Padronize formatos de API (sempre inclua Z ou um offset para instantes).
  • Defina o fuso da sessão explicitamente em jobs de ETL, conectores de BI e workers em background.

Tenha cuidado com trabalhos de “converter e backfill”. Mudar o tipo de coluna pode mudar silenciosamente o significado se valores antigos foram interpretados sob um fuso de sessão diferente.

Erros comuns que causam bugs de dia-a-mais ou DST

A maioria dos bugs de tempo não é “PostgreSQL sendo estranho”. Eles vêm de armazenar um valor que parece certo, mas com o significado errado, e então deixar diferentes camadas adivinharem o contexto ausente.

Erro 1: Salvar um horário de relógio como se fosse absoluto

Uma armadilha comum é armazenar horários locais (como “2026-03-29 09:00” em Berlim) em um TIMESTAMPTZ. PostgreSQL trata isso como um instante e o converte com base no fuso da sessão atual. Se a intenção era “sempre 9h horário local”, você acabou perdendo essa intenção. Ver a mesma linha sob outro fuso de sessão desloca a hora exibida.

Para compromissos, armazene o horário local como TIMESTAMP mais um campo separado de fuso (ou localização). Para eventos que ocorreram em um momento (pagamentos, logins), armazene o instante como TIMESTAMPTZ.

Erro 2: Ambientes diferentes, suposições diferentes

Seu laptop, staging e produção podem não compartilhar o mesmo fuso horário. Um ambiente roda em UTC, outro em horário local, e relatórios “group by day” começam a discordar. Os dados não mudaram, a configuração da sessão mudou.

Erro 3: Usar funções de tempo sem saber o que prometem

now() e current_timestamp são estáveis dentro de uma transação. clock_timestamp() muda a cada chamada. Se você gerar timestamps em vários pontos de uma transação e misturar essas funções, ordenação e durações podem parecer estranhas.

Erro 4: Converter duas vezes (ou zero vezes)

Um bug frequente de API: o app converte um horário local para UTC, envia como uma string ingênua e então a sessão do banco converte de novo porque assume que a entrada era local. O oposto também acontece: o app envia um horário local mas o rotula com Z (UTC), deslocando-o quando renderizado.

Erro 5: Agrupar por data sem declarar o fuso horário pretendido

“Totais diários” depende de qual limite de dia você quer. Se você agrupa com date(created_at) em um TIMESTAMPTZ, o resultado segue o fuso horário da sessão. Eventos de fim de noite podem mudar para o dia anterior ou seguinte.

Antes de liberar um dashboard ou API, faça uma verificação básica: escolha um fuso de relatório por gráfico e aplique-o consistentemente, inclua offsets (ou Z) em payloads de API, alinhe staging e produção na política de fuso horário e seja explícito sobre qual fuso você quer ao agrupar.

Verificações rápidas antes de liberar um dashboard ou API

Own your generated source code
Get production-ready Go, Vue3, and native mobile code you can deploy or self-host.
Generate Code

Bugs de tempo raramente vêm de uma única consulta ruim. Eles acontecem porque armazenamento, relatório e API cada um faz uma suposição ligeiramente diferente.

Use uma checklist curta antes do lançamento:

  • Para eventos do mundo real (cadastros, pagamentos, pings de sensor), armazene o instante como TIMESTAMPTZ.
  • Para conceitos locais do negócio (dia de faturamento, data de relatório), armazene um DATE ou TIME, não um timestamp que você planeja “converter depois”.
  • Em jobs agendados e executores de relatórios, defina o fuso da sessão de propósito.
  • Em respostas de API, inclua um offset ou Z, e confirme que o cliente interpreta como horário com fuso.
  • Teste a semana de transição de DST para pelo menos um fuso alvo.

Uma validação rápida fim-a-fim: escolha um evento conhecido de borda (por exemplo, 2026-03-08 01:30 em uma zona que observa DST) e siga-o pelo armazenamento, saída da consulta, JSON da API e o rótulo final no gráfico. Se o gráfico mostrar o dia certo mas a tooltip a hora errada (ou vice-versa), você tem uma incompatibilidade de conversão.

Exemplo: por que duas equipes discordam sobre os números do mesmo dia

Prototype a reporting-safe app
Turn your time contract into a working app fast, then adjust without technical debt.
Prototype Now

Um time de suporte em Nova York e um time financeiro em Berlim olham o mesmo dashboard. O servidor do banco roda em UTC. Todo mundo insiste que seus números estão certos, mas “ontem” é diferente dependendo de quem pergunta.

Aqui está o evento: um ticket de cliente é criado às 23:30 em Nova York no dia 10 de março. Isso é 04:30 UTC no dia 11 de março, e 05:30 em Berlim. Um momento real, três datas de calendário diferentes.

Se o horário de criação do ticket estiver armazenado como TIMESTAMP (sem fuso) e seu app assumir que é “local”, você pode reescrever a história silenciosamente. Nova York pode tratar 2026-03-10 23:30 como horário de Nova York, enquanto Berlim interpreta esse mesmo valor armazenado como horário de Berlim. A mesma linha cai em dias diferentes para visualizadores distintos.

Se estiver armazenado como TIMESTAMPTZ, PostgreSQL guarda o instante consistentemente e só o converte quando alguém o vê ou formata. Por isso TIMESTAMPTZ vs TIMESTAMP muda o que “um dia” significa em relatórios.

A correção é separar duas ideias: o instante em que o evento aconteceu e a data de relatório que você quer usar.

Um padrão prático:

  1. Armazene o tempo do evento como TIMESTAMPTZ.
  2. Decida a regra de relatório: local do visualizador (dashboards pessoais) ou um único fuso do negócio (financeiro da empresa).
  3. Calcule a data de relatório na hora da consulta usando essa regra: converta o instante para a zona escolhida e então pegue a date.

Próximos passos: padronize o tratamento de tempo em toda a stack

Se o tratamento de tempo não estiver documentado, todo novo relatório vira um jogo de adivinhação. Mire em um comportamento de tempo que seja entediante e previsível entre banco, APIs e dashboards.

Escreva um pequeno “contrato de tempo” que responda três perguntas:

  • Event time standard: armazene instantes de evento como TIMESTAMPTZ (tipicamente em UTC) a menos que haja uma razão forte para não fazê-lo.
  • Business time zone: escolha uma zona para relatórios e use-a consistentemente quando definir “dia”, “semana” e “mês”.
  • API format: sempre envie timestamps com um offset (ISO 8601 com Z ou +/-HH:MM) e documente se os campos significam “instante” ou “horário de parede local”.

Adicione pequenos testes ao redor do início e fim do DST. Eles pegam bugs caros cedo. Por exemplo, valide que uma query de “total diário” é estável para uma zona comercial fixa durante uma mudança de DST, e que entradas de API como 2026-11-01T01:30:00-04:00 e 2026-11-01T01:30:00-05:00 são tratadas como dois instantes diferentes.

Planeje migrações com cuidado. Mudar tipos e suposições no lugar pode reescrever silenciosamente a história em gráficos. Uma abordagem mais segura é adicionar uma nova coluna (por exemplo, created_at_utc TIMESTAMPTZ), preencher com uma conversão revisada, atualizar leituras para usar a nova coluna e então atualizar gravações. Mantenha relatórios antigos e novos lado a lado brevemente para que mudanças nos totais diários fiquem óbvias.

Se você quiser um lugar para aplicar esse “contrato de tempo” em modelos de dados, APIs e telas, uma configuração unificada de build ajuda. AppMaster (appmaster.io) gera backend, app web e APIs a partir de um único projeto, o que facilita manter regras de armazenamento e exibição de timestamps consistentes à medida que sua aplicação cresce.

FAQ

When should I use TIMESTAMPTZ instead of TIMESTAMP?

Use TIMESTAMPTZ for anything that happened at a real moment (signups, payments, logins, messages, sensor pings). It stores one unambiguous instant and can be safely sorted, filtered, and compared across systems. Use plain TIMESTAMP only when the value is meant to be a wall-clock time that should stay exactly as written, usually paired with a separate time zone or location field.

What’s the real difference between TIMESTAMP and TIMESTAMPTZ in PostgreSQL?

TIMESTAMPTZ represents a real instant in time; PostgreSQL normalizes it internally and then displays it in your session time zone. TIMESTAMP is just a date and clock time with no zone attached, so PostgreSQL won’t shift it automatically. The key difference is meaning: instant versus local wall time.

Why do I see different times for the same row depending on who runs the query?

Because the session time zone controls how TIMESTAMPTZ is formatted on output and how some inputs are interpreted. Two tools can query the same row and show different clock times if one session is set to UTC and another to America/Los_Angeles. For reports and APIs, set the session time zone explicitly so results don’t depend on hidden defaults.

Why do daily totals change between New York and Berlin?

Because “a day” depends on a time zone boundary. If one dashboard groups by viewer-local time while another groups by UTC (or a business zone), late-night events can fall on different dates and change daily totals. Fix it by picking one grouping rule per chart (UTC or a specific business zone) and using it consistently in SQL, BI, and exports.

How do I avoid DST bugs like missing or duplicated hours in hourly charts?

DST creates missing or duplicated local hours, which can produce gaps or double-counted buckets when grouping by local time. If your data represents real moments, store it as TIMESTAMPTZ and choose a clear chart time zone for bucketing. Also test the DST transition week for your target zones to catch surprises early.

Does TIMESTAMPTZ store the user’s time zone?

No, PostgreSQL does not preserve the original time zone label with TIMESTAMPTZ; it stores the instant. When you query it, PostgreSQL displays it in the session time zone, which may differ from the user’s original zone. If you need “show it in the customer’s time zone,” store that zone separately in another column.

What should my API return for timestamps to avoid confusion?

Return ISO 8601 timestamps that include an offset, and be consistent. A simple default is to always return UTC with Z for event instants, then let clients convert for display. Avoid sending “naive” strings like 2026-03-10 23:30:00 because clients will guess the zone differently.

Where should time zone conversion happen: database, API, or UI?

Convert at the edges: store event instants as TIMESTAMPTZ, then convert to the desired zone when you display or bucket for reporting. Avoid converting back and forth inside triggers, background jobs, and ETL unless you have a clear contract. Most reporting problems come from double conversion or from mixing naive and time-zone-aware values.

How should I store business days and schedules like “run at 10:00 local time”?

Use DATE for business concepts that are truly dates, like “billing day,” “reporting date,” or “delivery date.” Use TIME (or TIMESTAMP plus a separate time zone) for schedules like “opens at 09:00 local time.” Don’t force these into TIMESTAMPTZ unless you really mean a single instant, because DST and zone changes can shift the intended meaning.

How can I migrate from TIMESTAMP to TIMESTAMPTZ without breaking reports?

First, decide whether it’s an instant (TIMESTAMPTZ) or a local wall time (TIMESTAMP plus zone), then add a new column instead of rewriting in place. Backfill with a reviewed conversion under a known session time zone, and validate sample rows around midnight and DST boundaries. Run old and new reports side by side briefly so any shifts in totals are obvious before you remove the old column.

Fácil de começar
Criar algo espantoso

Experimente o AppMaster com plano gratuito.
Quando estiver pronto, você poderá escolher a assinatura adequada.

Comece