Primeiro, a manipulação de tempo e a aritmética do PostgreSQL são fantásticas e a Opção 3 é ótima no caso geral. É, no entanto, uma visão incompleta do tempo e dos fusos horários e pode ser complementada:
- Armazene o nome do fuso horário do usuário como uma preferência do usuário (por exemplo
America/Los_Angeles
, não -0700
).
- Faça com que os dados de eventos / hora do usuário sejam enviados localmente ao seu quadro de referência (provavelmente um deslocamento do UTC, como
-0700
).
- No aplicativo, converta a hora em
UTC
e armazenada usando uma TIMESTAMP 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
para America/Los_Angeles
).
- Defina seu banco de dados
timezone
como UTC
.
Essa opção nem sempre funciona porque pode ser difícil obter o fuso horário do usuário e, portanto, os conselhos de hedge TIMESTAMP WITH TIME ZONE
para aplicativos leves. Dito isto, deixe-me explicar alguns aspectos básicos desta opção 4 em mais detalhes.
Como a opção 3, o motivo WITH TIME ZONE
é que o momento em que algo aconteceu é um momento absoluto no tempo. WITHOUT TIME ZONE
gera um fuso horário relativo . Nunca, nunca, nunca misture TIMESTAMPs absolutos e relativos.
De uma perspectiva programática e de consistência, verifique se todos os cálculos são feitos usando o 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. Definir um CHECK
na coluna para garantir que a gravação na coluna de carimbo de data / hora tenha um deslocamento de fuso horário 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 hora usando um tipo lexical). Novamente, o PostgreSQL não precisa disso para fazer cálculos de datas corretamente ou para converter entre fusos horários (isto é, 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-pegada forte o suficiente para garantir que os dados já sejam convertidos em UTC. Existem muitas opiniões sobre como fazer isso, mas isso parece ser o melhor na prática da minha experiência.
As críticas ao manuseio de fuso horário do banco de dados são amplamente justificadas (existem muitos bancos de dados que lidam com isso com grande incompetência), no entanto, o manuseio de carimbos de data e hora e fusos horários do PostgreSQL é 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'
retira as informações do fuso horário e cria um parente TIMESTAMP WITHOUT TIME ZONE
usando o quadro de referência do seu destino ( UTC
).
Ao converter de um incompleto TIMESTAMP WITHOUT TIME ZONE
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:
- armazene 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
- tratar todos os horários UTC diferentes de zero como um erro de entrada
- nunca misture e combine timestamps relativos e absolutos
- use também
UTC
como timezone
no banco de dados, se possível
Nota da linguagem de programação aleatória: O datetime
tipo de dados do Python é muito bom em manter a distinção entre tempos absolutos e tempos relativos (embora frustrante a princípio até você complementá-lo com uma biblioteca como PyTZ ).
EDITAR
Deixe-me explicar um pouco mais a diferença entre relativo x absoluto.
O tempo absoluto é usado para gravar um evento. Exemplos: "Usuário 123 conectado" ou "uma cerimônia de formatura começa em 28-05-2011, 14h PST". Independentemente do seu fuso horário local, se você puder se teletransportar para onde o evento ocorreu, poderá testemunhar o evento. A maioria dos dados de tempo em um banco de dados é absoluta (e, portanto, deve ser TIMESTAMP WITH TIME ZONE
, idealmente com um deslocamento de +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 o horário de algo da perspectiva de um fuso horário ainda a ser determinado. Exemplos: "as portas de nossa empresa abrem às 8h e fecham às 21h", "nos encontramos todas as segundas-feiras às 7h para uma reunião semanal do 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. Vale ressaltar uma exceção rara que deve ilustrar o valor dos tempos relativos. Para eventos futuros suficientemente distantes no futuro, onde possa haver incerteza sobre o tempo absoluto no qual algo pode ocorrer, use um carimbo de data e hora relativo. Aqui está um exemplo do mundo real:
Suponha que seja o ano de 2004 e você precisa agendar uma entrega em 31 de outubro de 2008 às 13h na costa oeste dos EUA (ou seja, America/Los_Angeles
/ PST8PDT
). Se você armazenasse isso usando o tempo absoluto ’2008-10-31 21:00:00.000000+00’::TIMESTAMP WITH TIME ZONE
, a entrega seria exibida às 14h porque o governo dos EUA aprovou a Lei de Política Energética de 2005 que alterou as regras que regem o horário de verão. Em 2004, quando a entrega foi agendada, a data 10-31-2008
seria a Hora Padrão do Pacífico ( +8000
), mas a partir de 2005, mais de um banco de dados de fuso horário reconheceu que 10-31-2008
seria a hora de verão do Pacífico (+0700
) Armazenar um carimbo de data e hora relativo com o fuso horário resultaria em um cronograma de entrega correto, porque um carimbo de data e hora relativo é imune à adulteração mal informada do Congresso. Onde o ponto de corte entre o uso de tempos relativos e absolutos para agendar as coisas é, é uma linha difusa, mas minha regra geral é que a programação para qualquer coisa no futuro além de 3-6mo deve fazer uso de registros de data e hora relativos (agendado = absoluto x planejado = relativo ???).
O outro / último tipo de tempo relativo é o INTERVAL
. Exemplo: "a sessão atingirá o tempo limite 20 minutos após o login de um usuário". Um INTERVAL
pode ser usado corretamente com registros de data e hora absolutos ( TIMESTAMP WITH TIME ZONE
) ou registros de data e hora relativos ( TIMESTAMP WITHOUT TIME ZONE
). É igualmente correto dizer: "uma sessão do 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 os tipos de dados relativos. Por exemplo: '2011-05-28'::DATE
representa uma data relativa, pois você não possui 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 conhece o fuso horário ou o DATE
representado pelo horário. Mesmo com '23:59:59-07'::TIME WITH TIME ZONE
, você não sabe o que DATE
seria. E, finalmente, 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 nos 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, no entanto, isso não significa que o esforço extra seja sempre necessário.