Três formas de armazenar um timestamp
A maioria dos bancos oferece pelo menos três opções: um tipo datetime nativo (TIMESTAMP, DATETIME, TIMESTAMPTZ), um inteiro simples (INT ou BIGINT) ou uma string (VARCHAR). Cada um tem compromissos diferentes em tamanho de armazenamento, ergonomia de consulta, tratamento de fuso e preparação para o futuro. Para a maioria dos bancos de produto, uma coluna datetime nativa é o melhor padrão porque o banco pode comparar, indexar, truncar, agrupar e formatar o valor como tempo em vez de como um número anônimo.
- Tipo datetime nativo — o melhor para aritmética de datas, conversão de fuso e legibilidade
- Inteiro BIGINT — bom para inserções de alto throughput e consultas numéricas de intervalo simples
- String VARCHAR — quase sempre errado: a comparação de strings de datas só funciona com formato ISO 8601 estrito
- Inteiro INT — evite para timestamps futuros a menos que tenha verificado a fundo o limite do ano 2038
MySQL: TIMESTAMP vs DATETIME vs INT
O MySQL tem dois tipos de data-hora que parecem semelhantes mas se comportam de modo muito diferente — e um deles tem uma data de expiração rígida. TIMESTAMP é conveniente quando você quer conversão automática entre UTC e o fuso de sessão, mas seu intervalo histórico de 32 bits o torna arriscado para dados de produto voltados ao futuro. DATETIME armazena a data e hora literais que você fornece, o que costuma ser mais claro quando a aplicação padroniza em UTC antes de gravar.
- TIMESTAMP: armazenado internamente como segundos Unix de 32 bits — limitado de 1970-01-01 a 2038-01-19
- TIMESTAMP: auto-converte entre UTC e o fuso de sessão ao inserir/ler
- DATETIME: armazena a data-hora literal, sem fuso. Intervalo 1000-01-01 a 9999-12-31. Não afetado pelo Y2038.
- DATETIME: não converte fusos — você controla UTC no nível da aplicação
- Recomendação: use DATETIME com valores UTC explícitos para tabelas novas e evite o limite de 2038
PostgreSQL: TIMESTAMPTZ é a escolha certa
O TIMESTAMP WITH TIME ZONE (TIMESTAMPTZ) do PostgreSQL armazena os timestamps como microssegundos UTC internamente e os converte para o fuso de sessão na saída. É a opção mais segura e correta para a maioria dos casos porque representa um instante real no tempo. O nome pode enganar: TIMESTAMPTZ não armazena o rótulo de fuso original como America/New_York. Ele armazena o instante e então o exibe conforme o fuso de sessão atual.
- TIMESTAMPTZ: armazena UTC, converte para o fuso de sessão na saída — portátil e seguro quanto ao horário de verão
- TIMESTAMP (sem fuso): armazena o valor literal sem conversão — use apenas para dados sem fuso
- EXTRACT(EPOCH FROM col): retorna segundos Unix como float de qualquer coluna TIMESTAMP
- TO_TIMESTAMP(epoch): converte segundos Unix de volta para um TIMESTAMPTZ
Indexação e desempenho de consultas
Para tabelas de aplicação normais, a diferença de desempenho entre colunas datetime nativas e colunas epoch BIGINT raramente é o fator decisivo. A forma da consulta, o design do índice, o particionamento e o número de linhas importam mais. Escolha primeiro o tipo que mantém o significado correto, depois indexe-o para as consultas de intervalo que sua aplicação realmente executa.
- Os três tipos suportam índices B-tree e consultas de intervalo eficientes
- Inteiros BIGINT são marginalmente mais rápidos para varreduras de igualdade e intervalo em tabelas de altíssimo volume
- Tipos datetime nativos permitem consultas indexadas por parte da data: WHERE created_at::date = '2024-01-01'
- Timestamps VARCHAR são os piores para desempenho — a comparação de strings não entende datas
Quando o armazenamento epoch em BIGINT faz sentido
BIGINT é razoável quando os dados são do tipo evento, com muitas inserções, e já produzidos como tempo Unix por outro sistema. Pipelines de analytics, fluxos de telemetria, filas e protocolos binários compactos costumam usar milissegundos epoch porque valores numéricos são rápidos de comparar e neutros à linguagem. O compromisso é a legibilidade: humanos precisam de um conversor e a aritmética de datas em SQL fica mais verbosa.
- Use BIGINT para milissegundos Unix se clientes JavaScript produzem os eventos diretamente
- Use BIGINT para segundos Unix se o sistema de origem é estilo Unix e a precisão de segundos basta
- Documente a unidade no nome da coluna: created_at_ms é mais claro que created_at_epoch
- Adicione uma coluna datetime gerada se os analistas precisam de consultas SQL legíveis
- Evite INT para timestamps modernos voltados ao futuro por causa dos limites de intervalo de 32 bits
Padrões de esquema recomendados
Para a maioria das aplicações web, armazene um instante em UTC e armazene o fuso preferido do usuário separadamente apenas quando precisar reconstruir a intenção de relógio local. Uma reunião marcada para 9:00 America/New_York é diferente de um log de eventos criado em um instante UTC preciso; modele esses casos de forma diferente.
- Logs de eventos: created_at TIMESTAMPTZ no PostgreSQL, ou created_at DATETIME em UTC para MySQL
- Ingestão de eventos JavaScript: created_at_ms BIGINT mais documentação clara da API
- Agendamentos locais recorrentes: local_date, local_time e timezone_id, e então calcule o próximo instante
- Timestamps de expiração: expires_at como datetime nativo ou expires_at_seconds com segundos Unix explícitos
- Tabelas de auditoria: mantenha created_at e updated_at como colunas datetime nativas para depuração legível
FAQ sobre timestamps em bancos de dados
MongoDB stores dates natively as the BSON Date type — a signed 64-bit integer of milliseconds since the Unix epoch. The driver maps it to a Date object in JavaScript, datetime in Python, ISODate in the mongo shell.
- BSON Date: 8 bytes, signed 64-bit, milliseconds since 1970 UTC
- Indexes natively — range queries on Date are fast and idiomatic
- Use ISODate("2024-01-01T00:00:00Z") in the shell to create comparable values
- For multi-precision needs, use Decimal128 or a sub-document with seconds + nanos; Date itself is millisecond precision
- TTL indexes operate on Date fields specifically — integer epoch fields will not work with expireAfterSeconds
DynamoDB: epoch attributes and TTL
DynamoDB has no dedicated date type. Two patterns dominate: ISO 8601 strings for human readability, or epoch numbers for compactness and TTL support.
- TTL requires the attribute to be a Number representing Unix seconds (not milliseconds)
- Strings store ISO 8601 with full timezone info but cost more bytes and sort lexicographically (so include the Z suffix consistently)
- Range queries on a sort-key epoch number are extremely cheap and align with DynamoDB’s query model
- Avoid mixing seconds and milliseconds across tables — DynamoDB will not protect you from the unit drift
- DynamoDB Streams emit an ApproximateCreationDateTime in ISO 8601 regardless of how you stored the original value
FAQ
- Should I store UTC or local time in a database?
- Store UTC for event timestamps and convert to local time when displaying. Store a timezone identifier separately when the user's local wall-clock intent matters, such as recurring meetings or business hours.
- Is BIGINT better than TIMESTAMP?
- Not generally. BIGINT is useful for numeric epoch pipelines, but native datetime types are easier for SQL date arithmetic, readable debugging, and timezone-aware output.
- Should MySQL use TIMESTAMP or DATETIME?
- For new application tables, DATETIME with UTC values is often safer because it avoids the 2038 range limit and does not silently depend on session timezone conversion.
- Should I store timestamps as UTC or with a timezone?
- Store the instant in UTC (TIMESTAMPTZ in PostgreSQL, or DATETIME with UTC values in MySQL) and convert to local time on display. Keep a separate IANA timezone column only when you must reconstruct a user's local wall-clock intent, such as recurring meetings.