DATETIME vs TIMESTAMP no Banco de Dados: Guia Completo
Escolher entre DATETIME e TIMESTAMP parece simples, mas a decisão errada gera bugs sutis em sistemas multi-região, mudanças de servidor e migrações. Veja a comparação técnica detalhada para MySQL e PostgreSQL, o problema do ano 2038, TIMESTAMPTZ e quando cada tipo brilha.
Por Vitor Morais
Fundador do MochaLabz ·
Converta timestamps Unix online
De segundos desde 1970 para data legível em qualquer fuso — 100% no navegador.
Usar conversor →DATETIME e TIMESTAMP parecem fazer a mesma coisa — guardar data e hora — mas têm comportamentos fundamentalmente diferentes em relação a fuso horário, armazenamento e migração. A escolha errada gera bugs sutis que aparecem só em produção, semanas depois do deploy: horários que mudam ao trocar servidor, eventos que aparecem em datas erradas para usuários em outros TZs, ano 2038 retornando NULL. Este guia cobre a comparação técnica para MySQL e PostgreSQL, com TIMESTAMPTZ, migração e quando usar cada um.
A diferença essencial em uma frase
- DATETIME = “calendário na parede”. Armazena exatamente o que você escreveu, sem TZ.
- TIMESTAMP = “instante no tempo”. Armazena um momento absoluto (UTC internamente) e converte para o TZ do servidor na leitura.
Mudou o servidor de TZ?
DATETIME: valores não mudam. 14:30 continua 14:30. TIMESTAMP: valores aparecem em outro horário (mas o instante absoluto é o mesmo). Esse é o ponto-chave para escolher.
DATETIME no MySQL: data e hora literais
DATETIME armazena ano, mês, dia, hora, minuto, segundo e (em MySQL 5.6.4+) frações de segundo. Sem TZ, sem conversão. O que você inseriu é o que você lê:
-- MySQL: DATETIME
CREATE TABLE eventos (
id INT PRIMARY KEY AUTO_INCREMENT,
titulo VARCHAR(200),
data_hora DATETIME -- "2026-04-18 14:30:00"
);
INSERT INTO eventos (titulo, data_hora)
VALUES ('Conferência', '2026-04-18 14:30:00');
-- Características:
-- Range: '1000-01-01 00:00:00' até '9999-12-31 23:59:59'
-- Tamanho: 8 bytes (5 base + 3 fração de segundos opcional)
-- TZ: não armazenada, não convertida
-- Mudança de TZ no servidor: SEM IMPACTO no valor armazenadoTIMESTAMP no MySQL: instante absoluto
TIMESTAMP armazena segundos desde a Unix epoch (1970-01-01 00:00:00 UTC). MySQL converte automaticamente para UTC na escrita e para o TZ da sessão na leitura:
-- MySQL: TIMESTAMP com auto update
CREATE TABLE pedidos (
id INT PRIMARY KEY AUTO_INCREMENT,
valor DECIMAL(10,2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP
);
-- Características:
-- Range: '1970-01-01 00:00:01' até '2038-01-19 03:14:07' UTC
-- Tamanho: 4 bytes (problema do ano 2038)
-- TZ: sempre UTC internamente, converte na leitura
-- Mudança de TZ: exibição muda, instante real é o mesmo
-- Demonstração da conversão automática:
SET time_zone = '+00:00'; -- UTC
SELECT created_at FROM pedidos WHERE id = 1;
-- '2026-04-18 14:30:00'
SET time_zone = '-03:00'; -- BRT
SELECT created_at FROM pedidos WHERE id = 1;
-- '2026-04-18 11:30:00' -- mesmo instante, exibição localComparativo MySQL — DATETIME vs TIMESTAMP
| Critério | DATETIME | TIMESTAMP |
|---|---|---|
| Tamanho em disco | 8 bytes | 4 bytes (32 bits) |
| Range | 1000-01-01 a 9999-12-31 | 1970-01-01 a 2038-01-19 |
| Armazena timezone? | Não | Sim (sempre UTC interno) |
| Converte na leitura? | Não | Sim — para TZ da sessão |
| Mudança de TZ no servidor | Sem efeito | Muda exibição, mantém instante |
| Auto update | Sim (5.6.5+) | Sim (clássico) |
| DEFAULT NOW() | Sim | Sim |
| Suporta ano > 2038 | Sim | Não (Y2K38) |
| Caso de uso ideal | Eventos locais, datas calendário | created_at, updated_at, logs, instantes |
O problema do ano 2038 (Y2K38)
TIMESTAMP do MySQL é armazenado em 4 bytes assinados — o que limita a contagem em 2³¹ - 1 segundos desde a epoch, batendo em 03:14:07 UTC de 19 de janeiro de 2038. Depois disso, overflow.
Y2K38 não é teórico
Sistemas que armazenam datas futuras (assinaturas vitalícias, apólices de seguro, garantias estendidas) já podem encontrar o bug HOJE quando calculam datas de 2038+. Para qualquer campo que possa ter ano > 2038, use DATETIME ou MariaDB 10.5+ com TIMESTAMP de 64 bits.
PostgreSQL: TIMESTAMP vs TIMESTAMPTZ
PostgreSQL tem dois tipos quase idênticos no nome mas com comportamento muito diferente:
-- PostgreSQL: TIMESTAMP (sem TZ)
-- Equivalente ao DATETIME do MySQL.
-- "Calendário na parede". Não converte nada.
data_evento TIMESTAMP NOT NULL
-- PostgreSQL: TIMESTAMPTZ (TIMESTAMP WITH TIME ZONE)
-- Equivalente ao TIMESTAMP do MySQL — mas SEM problema de 2038.
-- Armazena 8 bytes; range vai até ~290.000 d.C.
-- Armazena UTC internamente, converte para TZ da sessão na leitura.
created_at TIMESTAMPTZ DEFAULT NOW()
-- DATE puro (sem horário)
nascimento DATE NOT NULLRegra simples no PostgreSQL
Use TIMESTAMPTZ para tudo, exceto quando você realmente precisa de “data e hora locais sem timezone” (raro). TIMESTAMPTZ tem 8 bytes, sem problema de 2038, e o comportamento é o que 99% dos desenvolvedores intuitivamente esperam.
Comparativo PostgreSQL — TIMESTAMP vs TIMESTAMPTZ
| Critério | TIMESTAMP | TIMESTAMPTZ |
|---|---|---|
| Tamanho | 8 bytes | 8 bytes |
| Range | 4713 a.C. a 294276 d.C. | Idem |
| Armazena TZ? | Não | Sim (UTC internamente) |
| Conversão na leitura | Nenhuma | Para TZ da sessão |
| Equivalente MySQL | DATETIME | TIMESTAMP (sem Y2K38) |
| Recomendação 2026 | Casos raros (datas locais) | Padrão para tudo |
Configurando timezone da sessão (PostgreSQL)
-- Padrão: TZ do cliente ou do servidor (UTC se bem configurado)
SHOW timezone; -- "Etc/UTC"
-- Mudar para a sessão
SET timezone = 'America/Sao_Paulo';
SELECT NOW(); -- mostra em BRT
-- Inserção (qualquer formato com TZ é aceito)
INSERT INTO eventos (data_hora) VALUES
('2026-04-18 14:30:00'), -- assume TZ da sessão
('2026-04-18 14:30:00+00'), -- UTC explícito
('2026-04-18T14:30:00-03:00'); -- BRT explícito
-- Internamente todos viram UTC. Na leitura, voltam em TZ da sessão.Datas sem horário: use DATE
Para campos como aniversário, data de nascimento, dia de cobrança, deadline calendário — qualquer coisa onde o horário não importa — use o tipo DATE puro. Existe em MySQL e PostgreSQL:
CREATE TABLE usuarios (
id INT PRIMARY KEY AUTO_INCREMENT,
nome VARCHAR(200),
nascimento DATE, -- '2026-04-18' (3 bytes em MySQL)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Por que NÃO usar TIMESTAMP para nascimento?
-- Bug clássico: alguém nascido em 1990-04-18 às 00:00:00 BRT
-- é armazenado como '1990-04-18 03:00:00' UTC.
-- Em outro TZ aparece como dia 17 ou 19 dependendo da diferença.
-- DATE elimina o problema completamente.Comportamento em DST (horário de verão)
O Brasil aboliu DST em 2019, mas se sua aplicação atende usuários em países com DST ativo (EUA, Europa), o comportamento dos tipos importa:
| Critério | O que acontece |
|---|---|
| TIMESTAMPTZ (Postgres) / TIMESTAMP (MySQL) | Conversão automática correta — mantém o instante absoluto |
| TIMESTAMP (Postgres) / DATETIME (MySQL) | Sem conversão — pode pular ou repetir hora durante DST |
| DATE | Imune — não tem horário |
| Cálculo manual de offset (-3h) | Quebra em transições de DST de outros países |
Migrando DATETIME → TIMESTAMPTZ
Cenário comum em projetos legados que começaram com DATETIME (ou TIMESTAMP curto) e precisam mudar para tipo com TZ. O processo seguro:
-- PostgreSQL — migração de TIMESTAMP para TIMESTAMPTZ
-- (assumindo que valores antigos representam horário em BRT)
-- 1. Adiciona nova coluna
ALTER TABLE eventos ADD COLUMN data_hora_tz TIMESTAMPTZ;
-- 2. Popula com a coluna antiga, assumindo TZ original
UPDATE eventos
SET data_hora_tz = data_hora AT TIME ZONE 'America/Sao_Paulo';
-- 3. Verifica em duas zonas
SELECT data_hora,
data_hora_tz AT TIME ZONE 'UTC' AS em_utc,
data_hora_tz AT TIME ZONE 'America/Sao_Paulo' AS em_brt
FROM eventos LIMIT 5;
-- 4. Quando confirmar, drop coluna antiga e renomeia
ALTER TABLE eventos DROP COLUMN data_hora;
ALTER TABLE eventos RENAME COLUMN data_hora_tz TO data_hora;
-- MySQL — abordagem similar via STR_TO_DATE + CONVERT_TZQuando usar cada tipo (decision tree)
A coluna representa um INSTANTE no tempo
(quando algo aconteceu / vai acontecer)?
├── SIM → TIMESTAMP/TIMESTAMPTZ
│ (created_at, updated_at, logs, audit, eventos absolutos)
│
│ Mas tem datas > 2038?
│ ├── Sim → MySQL DATETIME ou Postgres TIMESTAMPTZ (8 bytes)
│ └── Não → TIMESTAMP do MySQL OK
│
└── NÃO → A coluna representa "data e hora locais" (sem TZ)?
├── Não tem horário → DATE
│ (nascimento, deadline, aniversário, dia cobrança)
│
└── Tem horário, mas é "horário local literal"
(ex: "todo dia às 8h locais, em qualquer fuso")
→ DATETIME (MySQL) ou TIMESTAMP sem TZ (Postgres)Padrão recomendado em 2026
Configuração de servidor
Servidor sempre em UTC. Mesmo em servidor no Brasil. UTC simplifica debug, evita bugs em DST de outros países e facilita correlação multi-região. Conversão para o timezone do usuário acontece na camada de apresentação (cliente, frontend, view).
- PostgreSQL: TIMESTAMPTZ para tudo que é instante. DATE para datas puras. Evite TIMESTAMP sem TZ.
- MySQL: TIMESTAMP para campos comuns (created_at, updated_at). DATETIME quando precisar de range > 2038 ou armazenar “hora local literal”. DATE para datas puras.
- Servidor: sempre UTC. Configure
default_time_zone = '+00:00'(MySQL) outimezone = 'UTC'(Postgres). - Aplicação: sempre serialize datas como ISO 8601 com Z final (
2026-04-18T14:30:00Z) ao trafegar entre cliente e servidor.
Para o lado do JavaScript, veja o guia completo de fusos horários em JavaScript, e para entender o instante absoluto subjacente, o que é Unix timestamp.
Os 8 erros mais comuns
Lista negra
- Servidor em horário local (BRT): bugs em DST de outros países, dificulta debug.
- Armazenar “horário do usuário” sem TZ: valores ficam ambíguos depois.
- TIMESTAMP em data de nascimento: dia muda em outro TZ. Use DATE.
- TIMESTAMP do MySQL para datas > 2038: overflow silencioso.
- TIMESTAMP sem TZ no Postgres: nome parecido confunde, comportamento é diferente do MySQL.
- Cálculo manual de offset (-3h fixo): quebra em DST.
- Timezone configurado por sessão sem padrão: cada cliente vê valores diferentes, debug horrível.
- Misturar tipos na mesma tabela: created_at TIMESTAMP + scheduled_at DATETIME = comparações inconsistentes.
Checklist da modelagem certa
- ✅ Servidor de banco configurado em UTC.
- ✅ Coluna criada com tipo correto (TIMESTAMPTZ no Postgres, TIMESTAMP no MySQL para campos comuns).
- ✅ Datas sem horário usam DATE puro.
- ✅ Conversão para TZ do usuário só na camada de apresentação.
- ✅ ISO 8601 com Z final entre cliente e API.
- ✅ Sem cálculos manuais de offset (-3h fixo).
- ✅ Datas além de 2038 modeladas em DATETIME (MySQL) ou TIMESTAMPTZ (Postgres).
- ✅ Testes em pelo menos 2 timezones diferentes.
- ✅ Logs em UTC ISO para correlação multi-serviço.
- ✅ Documentação clara de qual tipo cada coluna usa e por quê.
Perguntas frequentes
Qual a diferença básica entre DATETIME e TIMESTAMP?+
DATETIME armazena uma data e hora literais, sem timezone — pense em "calendário na parede". TIMESTAMP armazena um instante absoluto no tempo (segundos desde 1970 UTC) e converte automaticamente para o timezone do servidor ao exibir. Em mudanças de servidor ou de timezone, DATETIME mantém os valores; TIMESTAMP muda a exibição mas mantém o instante.
Devo usar DATETIME ou TIMESTAMP no MySQL?+
Use TIMESTAMP para campos created_at, updated_at, logs, eventos com instante absoluto. Use DATETIME para datas "locais" sem TZ (aniversários, dia de cobrança, agendamentos baseados em horário local específico). Em sistemas multi-região, prefira TIMESTAMP sempre que o instante absoluto importa.
Por que o TIMESTAMP do MySQL termina em 2038?+
Porque é armazenado em 4 bytes (32 bits assinados), o que limita ao range de ~1970 a ~2038 (problema do ano 2038, ou Y2K38). Para datas além disso, MySQL exige DATETIME (8 bytes, range 1000–9999). MariaDB 10.5+ tem TIMESTAMP de 64 bits opcional. PostgreSQL não tem essa limitação — TIMESTAMPTZ usa 8 bytes nativos e cobre milênios.
Qual a diferença entre TIMESTAMP e TIMESTAMPTZ no PostgreSQL?+
TIMESTAMP (sem TIME ZONE) é equivalente ao DATETIME do MySQL — armazena "calendário na parede" sem TZ. TIMESTAMPTZ (TIMESTAMP WITH TIME ZONE) é o equivalente ao TIMESTAMP do MySQL — armazena UTC internamente e converte para o TZ da sessão na leitura. Para projetos novos em Postgres, use SEMPRE TIMESTAMPTZ; o tipo sem TZ é fonte garantida de bugs.
O TIMESTAMP do MySQL armazena UTC mesmo?+
Sim. Independente do timezone do servidor, MySQL converte o valor recebido para UTC na escrita e converte de UTC para o timezone da sessão na leitura. Isso significa que mudar timezone do servidor (ou da sessão) não corrompe os dados — só muda como aparecem. DATETIME, ao contrário, é "surdo" a TZ e não muda nada.
Como armazenar datas sem horário (data de nascimento, prazo)?+
Use o tipo DATE puro (existe em MySQL e PostgreSQL). Tem 3 bytes em MySQL, range 1000–9999, e não envolve horário nem TZ. Armazenar nascimento como TIMESTAMP gera bug clássico: alguém nascido em 1990-04-18 vira 17 de abril em outro TZ. DATE elimina o problema.
Posso configurar o timezone do servidor para evitar conversões?+
Pode, mas geralmente não deve. A boa prática é manter servidor sempre em UTC, armazenar tudo em UTC (TIMESTAMP/TIMESTAMPTZ) e converter para o TZ do usuário só na camada de apresentação. Servidor em horário local (ex: BRT) gera bugs em DST de outras regiões e dificulta debug entre data centers.
TIMESTAMP em MySQL ainda atualiza automaticamente em UPDATE?+
Sim, é o comportamento histórico para o primeiro TIMESTAMP da tabela: TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP. Útil para updated_at automático. DATETIME passou a suportar o mesmo comportamento desde MySQL 5.6.5. Em PostgreSQL, esse comportamento é manual via trigger ou DEFAULT NOW() + UPDATE explícito.
Continue lendo
Unix Timestamp (2026): O Que É, Como Funciona e Como Converter em Qualquer Linguagem
Guia completo do Unix timestamp: origem, cálculo, conversão em JavaScript, Python e SQL, fuso horário, Year 2038 problem e quando usar no lugar de datas formatadas.
UUID como Chave Primária: Vantagens, Problemas e Alternativas (2026)
Tudo sobre usar UUID como chave primária em PostgreSQL e MySQL: por que vale, problema de fragmentação do v4, como UUID v7 resolve, comparativo com BIGINT, ULID, NanoID, CUID2 e Snowflake.
Fusos Horários em JavaScript: Guia Completo (2026) com Date, Intl e Temporal
Como Date funciona internamente, Intl.DateTimeFormat por região, timezones do Brasil, parsing seguro, horário de verão, a nova API Temporal e bibliotecas como date-fns e Luxon.
Performance SQL com Índices: Guia Completo 2026 (PostgreSQL e MySQL)
Aprenda a usar índices em SQL para acelerar queries de segundos para milissegundos. Tipos de índice (B-tree, GIN, GiST, BRIN), índices compostos, parciais, funcionais, EXPLAIN ANALYZE, anti-padrões e checklist de manutenção.