Em primeiro lugar, o tratamento de tempo e aritmética do PostgreSQL são fantásticos e a Opção 3 é boa no caso geral. É, no entanto, uma visão incompleta de tempo e fusos horários e pode ser complementada:
- Armazenar o nome do fuso horário de um usuário como preferência do usuário (por exemplo,
America/Los_Angeles
, não-0700
). - Ter dados de eventos/hora do usuário enviados localmente para seu quadro de referência (provavelmente um deslocamento do UTC, como
-0700
). - No aplicativo, converta a hora para
UTC
e armazenado usando umTIMESTAMP WITH TIME ZONE
coluna. - Retornar solicitações de horário locais para o fuso horário de um usuário (ou seja, converter de
UTC
paraAmerica/Los_Angeles
). - Defina o
timezone
do seu banco de dados paraUTC
.
Essa opção nem sempre funciona porque pode ser difícil obter o fuso horário de um usuário e, portanto, o conselho de hedge para usar
TIMESTAMP WITH TIME ZONE
para aplicações leves. Dito isto, deixe-me explicar alguns aspectos de fundo desta Opção 4 com mais detalhes. Como a Opção 3, o motivo do
WITH TIME ZONE
é porque o momento em que algo aconteceu é um absoluto momento no tempo. WITHOUT TIME ZONE
produz um parente fuso horário. Nunca, nunca, nunca misture TIMESTAMPs absolutos e relativos. De uma perspectiva programática e de consistência, certifique-se de que todos os cálculos sejam feitos usando UTC como fuso horário. Este não é um requisito do PostgreSQL, mas ajuda na integração com outras linguagens ou ambientes de programação. Configurando um
CHECK
na coluna para garantir que a gravação na coluna de carimbo de hora tenha um deslocamento de fuso horário de 0
é uma posição defensiva que evita algumas classes de bugs (por exemplo, um script despeja dados em um arquivo e outra coisa classifica os dados de tempo usando uma classificação léxica). Novamente, o PostgreSQL não precisa disso para fazer cálculos de data corretamente ou converter entre fusos horários (ou seja, o PostgreSQL é muito hábil em converter horários entre dois fusos horários arbitrários). Para garantir que os dados que entram no banco de dados sejam armazenados com um deslocamento de zero:CREATE TABLE my_tbl (
my_timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
CHECK(EXTRACT(TIMEZONE FROM my_timestamp) = '0')
);
test=> SET timezone = 'America/Los_Angeles';
SET
test=> INSERT INTO my_tbl (my_timestamp) VALUES (NOW());
ERROR: new row for relation "my_tbl" violates check constraint "my_tbl_my_timestamp_check"
test=> SET timezone = 'UTC';
SET
test=> INSERT INTO my_tbl (my_timestamp) VALUES (NOW());
INSERT 0 1
Não é 100% perfeito, mas fornece uma medida anti-footshooting forte o suficiente para garantir que os dados já sejam convertidos para UTC. Existem muitas opiniões sobre como fazer isso, mas isso parece ser o melhor na prática da minha experiência.
Críticas ao tratamento de fuso horário do banco de dados são amplamente justificadas (há muitos bancos de dados que lidam com isso com grande incompetência), no entanto, o tratamento de timestamps e fusos horários do PostgreSQL é bastante impressionante (apesar de alguns "recursos" aqui e ali). Por exemplo, um desses recursos:
-- Make sure we're all working off of the same local time zone
test=> SET timezone = 'America/Los_Angeles';
SET
test=> SELECT NOW();
now
-------------------------------
2011-05-27 15:47:58.138995-07
(1 row)
test=> SELECT NOW() AT TIME ZONE 'UTC';
timezone
----------------------------
2011-05-27 22:48:02.235541
(1 row)
Observe que
AT TIME ZONE 'UTC'
remove as informações do fuso horário e cria um TIMESTAMP WITHOUT TIME ZONE
relativo usando o quadro de referência do seu destino (UTC
). Ao converter de um
TIMESTAMP WITHOUT TIME ZONE
incompleto para um TIMESTAMP WITH TIME ZONE
, o fuso horário ausente é herdado da sua conexão:test=> SET timezone = 'America/Los_Angeles';
SET
test=> SELECT EXTRACT(TIMEZONE_HOUR FROM NOW());
date_part
-----------
-7
(1 row)
test=> SELECT EXTRACT(TIMEZONE_HOUR FROM TIMESTAMP WITH TIME ZONE '2011-05-27 22:48:02.235541');
date_part
-----------
-7
(1 row)
-- Now change to UTC
test=> SET timezone = 'UTC';
SET
-- Create an absolute time with timezone offset:
test=> SELECT NOW();
now
-------------------------------
2011-05-27 22:48:40.540119+00
(1 row)
-- Creates a relative time in a given frame of reference (i.e. no offset)
test=> SELECT NOW() AT TIME ZONE 'UTC';
timezone
----------------------------
2011-05-27 22:48:49.444446
(1 row)
test=> SELECT EXTRACT(TIMEZONE_HOUR FROM NOW());
date_part
-----------
0
(1 row)
test=> SELECT EXTRACT(TIMEZONE_HOUR FROM TIMESTAMP WITH TIME ZONE '2011-05-27 22:48:02.235541');
date_part
-----------
0
(1 row)
A linha inferior:
- armazenar o fuso horário de um usuário como um rótulo nomeado (por exemplo,
America/Los_Angeles
) e não um deslocamento do UTC (por exemplo,-0700
) - use UTC para tudo, a menos que haja um motivo convincente para armazenar um deslocamento diferente de zero
- trate todos os horários UTC diferentes de zero como um erro de entrada
- nunca misture e combine carimbos de data/hora relativos e absolutos
- também use
UTC
como otimezone
no banco de dados, se possível
Nota de linguagem de programação aleatória:
datetime
do Python tipo de dados é muito bom em manter a distinção entre tempos absolutos e relativos (embora frustrante no início até que você o complemente com uma biblioteca como o PyTZ). EDITAR
Deixe-me explicar um pouco mais a diferença entre relativo e absoluto.
O tempo absoluto é usado para registrar um evento. Exemplos:"Usuário 123 logado" ou "cerimônias de formatura começam em 28/05/2011 às 14h PST". Independentemente do seu fuso horário local, se você pudesse se teletransportar para onde o evento ocorreu, poderia testemunhar o evento acontecendo. A maioria dos dados de tempo em um banco de dados é absoluto (e, portanto, deve ser
TIMESTAMP WITH TIME ZONE
, de preferência com um deslocamento +0 e um rótulo textual representando as regras que regem o fuso horário específico - não um deslocamento). Um evento relativo seria registrar ou agendar a hora de algo a partir da perspectiva de um fuso horário ainda a ser determinado. Exemplos:"as portas da nossa empresa abrem às 8h e fecham às 21h", "vamos nos encontrar todas as segundas-feiras às 7h para uma reunião semanal de café da manhã" ou "todo Halloween às 20h". Em geral, o tempo relativo é usado em um modelo ou fábrica para eventos, e o tempo absoluto é usado para quase todo o resto. Há uma rara exceção que vale a pena apontar que deve ilustrar o valor dos tempos relativos. Para eventos futuros que estão longe o suficiente no futuro, onde pode haver incerteza sobre o tempo absoluto em que algo pode ocorrer, use um registro de data e hora relativo. Aqui está um exemplo do mundo real:
Suponha que seja o ano de 2004 e você precise agendar uma entrega em 31 de outubro de 2008 às 13h na costa oeste dos EUA (ou seja,
America/Los_Angeles
/PST8PDT
). Se você armazenou isso usando o tempo absoluto usando ’2008-10-31 21:00:00.000000+00’::TIMESTAMP WITH TIME ZONE
, a entrega teria aparecido às 14h porque o governo dos EUA aprovou a Lei de Política Energética de 2005 que mudou as regras que regem o horário de verão. Em 2004, quando a entrega foi agendada, a data 10-31-2008
seria o horário padrão do Pacífico (+8000
), mas a partir de 2005+ os bancos de dados de fuso horário reconheceram que 10-31-2008
seria o horário de verão do Pacífico (+0700
). Armazenar um carimbo de data/hora relativo com o fuso horário resultaria em uma programação de entrega correta porque um carimbo de data/hora relativo é imune a adulterações mal informadas do Congresso. Onde o ponto de corte entre o uso de tempos relativos vs absolutos para agendar coisas é uma linha difusa, mas minha regra geral é que o agendamento para qualquer coisa no futuro além de 3-6 meses deve fazer uso de timestamps relativos (agendado =absoluto vs planejado =relativo ???). O outro/último tipo de tempo relativo é o
INTERVAL
. Exemplo:"a sessão expirará 20 minutos após o login do usuário". Um INTERVAL
pode ser usado correctamente com carimbos de hora absolutos (TIMESTAMP WITH TIME ZONE
) ou carimbos de data/hora relativos (TIMESTAMP WITHOUT TIME ZONE
). É igualmente correto dizer que "uma sessão de usuário expira 20 minutos após um login bem-sucedido (login_utc + session_duration)" ou "nossa reunião matinal de café da manhã pode durar apenas 60 minutos (recurring_start_time + meeting_length)". Últimos pedaços de confusão:
DATE
, TIME
, TIME WITHOUT TIME ZONE
e TIME WITH TIME ZONE
são todos tipos de dados relativos. Por exemplo:'2011-05-28'::DATE
representa uma data relativa, pois você não tem informações de fuso horário que possam ser usadas para identificar a meia-noite. Da mesma forma, '23:23:59'::TIME
é relativo porque você não sabe o fuso horário nem o DATE
representado pelo tempo. Mesmo com '23:59:59-07'::TIME WITH TIME ZONE
, você não sabe qual é a DATE
seria. E por último, DATE
com um fuso horário não é de fato um DATE
, é um TIMESTAMP WITH TIME ZONE
:test=> SET timezone = 'America/Los_Angeles';
SET
test=> SELECT '2011-05-11'::DATE AT TIME ZONE 'UTC';
timezone
---------------------
2011-05-11 07:00:00
(1 row)
test=> SET timezone = 'UTC';
SET
test=> SELECT '2011-05-11'::DATE AT TIME ZONE 'UTC';
timezone
---------------------
2011-05-11 00:00:00
(1 row)
Colocar datas e fusos horários em bancos de dados é uma coisa boa, mas é fácil obter resultados sutilmente incorretos. É necessário um esforço adicional mínimo para armazenar informações de tempo correta e completamente, mas isso não significa que o esforço extra seja sempre necessário.