Que tipo de carimbo de data / hora devo escolher em um banco de dados PostgreSQL?


119

Gostaria de definir uma prática recomendada para armazenar registros de data e hora no meu banco de dados do Postgres no contexto de um projeto de fuso horário múltiplo.

eu posso

  1. escolha TIMESTAMP WITHOUT TIME ZONEe lembre-se de que fuso horário foi usado no momento da inserção desse campo
  2. escolha TIMESTAMP WITHOUT TIME ZONEe adicione outro campo que contenha o nome do fuso horário usado no momento da inserção
  3. escolha TIMESTAMP WITH TIME ZONEe insira os registros de data e hora de acordo

Tenho uma ligeira preferência pela opção 3 (carimbo de data / hora com fuso horário), mas gostaria de ter uma opinião fundamentada sobre o assunto.

Respostas:


142

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:

  1. 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).
  2. 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).
  3. No aplicativo, converta a hora em UTCe armazenada usando uma TIMESTAMP WITH TIME ZONEcoluna.
  4. Retornar solicitações de horário locais para o fuso horário de um usuário (ou seja, converter de UTCpara America/Los_Angeles).
  5. Defina seu banco de dados timezonecomo 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 ZONEpara 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 ZONEgera 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 CHECKna 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 ZONEusando o quadro de referência do seu destino ( UTC).

Ao converter de um incompleto TIMESTAMP WITHOUT TIME ZONEpara 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 UTCcomo timezoneno banco de dados, se possível

Nota da linguagem de programação aleatória: O datetimetipo 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-2008seria 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-2008seria 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 INTERVALpode 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 ZONEe TIME WITH TIME ZONEsão todos os tipos de dados relativos. Por exemplo: '2011-05-28'::DATErepresenta 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 DATErepresentado pelo horário. Mesmo com '23:59:59-07'::TIME WITH TIME ZONE, você não sabe o que DATEseria. E, finalmente, DATEcom 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.


2
Se você informar com precisão ao postgresql o fuso horário correto em que o registro de data e hora do usuário está, o postgresql fará o trabalho pesado nos bastidores. Convertê-lo sozinho é apenas um problema emprestado.
Seth Robertson

1
@Sean - com sua restrição de cheques, como você nunca insere um carimbo de data / hora sem set timezone to 'UTC'? Você sabe que todas as datas com reconhecimento de fuso horário são armazenadas internamente no UTC ?

2
O objetivo da verificação é garantir que os dados sejam armazenados com deslocamento zero do UTC. A classificação e recuperação de informações e a comparação de tempos com compensações diferentes de zero são propensas a erros. Ao impor um deslocamento UTC zero, você pode interagir consistentemente com os dados de uma única perspectiva, de uma maneira quase sem risco que se comporta de maneira previsível em todos os cenários. Se fosse prático que os carimbos de data e hora suportassem representações textuais de fusos horários, meus pensamentos sobre o assunto seriam diferentes. : ~]
Sean

6
@Sean: Mas, como Jack indica, todos os timestamps com reconhecimento de fuso horário fundamentalmente são armazenados internamente no UTC e são convertidos em seu fuso horário local quando usados; efetivamente, extrair (fuso horário de ...) sempre retornará qualquer que seja o fuso horário local da conexão: não tem relação com o modo como o carimbo de data / hora foi "armazenado". Em outras palavras, o fuso horário não faz parte do tipo e não pode ser armazenado: o "com fuso horário" é apenas uma propriedade de como os dados serão convertidos ao interagir com outros tipos. Os dados, portanto, não têm representação de fusos horários, textuais ou outros.
precisa saber é o seguinte

@ JayFreeman-saurik-: você está absolutamente correto. O '' CHECK () '' existe como uma medida anti-pegada para proteger contra código possivelmente desonesto. Garantir que os dados sejam UTC na gravação fornece uma garantia modesta de que o código foi pensado ou que o ambiente de execução está configurado corretamente.
Sean

58

A resposta de Sean é excessivamente complexa e enganosa.

O fato é que "WITH TIME ZONE" e "WITHOUT TIME ZONE" armazenam o valor como um registro de data e hora UTC absoluto semelhante ao unix. A diferença está na maneira como o carimbo de data / hora é exibido. Quando "WITH fuso horário", o valor exibido é o valor armazenado UTC convertido para o fuso do usuário. Quando "SEM Fuso Horário", o valor armazenado no UTC é torcido para mostrar o mesmo relógio, independentemente da zona que o usuário definiu ".

A única situação em que um "SEM fuso horário" é utilizável é quando um valor nominal do relógio é aplicável independentemente do fuso horário real. Por exemplo, quando um registro de data e hora indica quando as cabines de votação podem fechar (ou seja, elas fecham às 20:00, independentemente do fuso horário de uma pessoa).

Use a opção 3. Sempre use "WITH fuso horário", a menos que haja um motivo muito específico para não fazê-lo.


10
David E. Wheeler, um dos principais especialistas do Postgres, concordaria com sua avaliação de acordo com a publicação dele, Sempre use TIMESTAMP WITH TIME ZONE .
Basil Bourque

2
E se o navegador converter o carimbo de hora UTC em fuso horário local? Portanto, o banco de dados nunca fará a conversão e conterá apenas UTC. "SEM fuso horário" seria aceitável?
dman

5

Minha preferência é pela opção 3, pois o Postgres pode fazer todo o trabalho recalculando os carimbos de hora em relação ao fuso horário para você, enquanto nos outros dois você terá que fazer isso sozinho. A sobrecarga de armazenamento extra de armazenar o carimbo de data / hora com um fuso horário é realmente insignificante, a menos que você esteja falando de milhões de registros; nesse caso, você provavelmente já possui requisitos de armazenamento bastante escassos.


19
Incorreta. Não há custos indiretos ... O Postgres não armazena o fuso horário ('deslocamento' é o termo correto, não o fuso horário, a propósito). O TIMESTAMP WITH TIME ZONEnome é enganoso. Significa realmente "preste atenção a qualquer deslocamento especificado ao inserir / atualizar e use esse deslocamento para ajustar a data e hora para UTC". O TIMESTAMP WITHOUT TIME ZONEnome significa "ignorar qualquer deslocamento que possa estar presente durante a inserção / atualização, considere as partes de data e hora como estando no UTC sem necessidade de ajuste". Leia o documento com atenção.
Basil Bourque

1
@BasilBourque obrigado por esta informação. Incrivelmente útil. Para outros que estão lendo isso, a linha do documento diz: "Em um literal que foi determinado como carimbo de data / hora sem fuso horário, o PostgreSQL ignorará silenciosamente qualquer indicação de fuso horário. Ou seja, o valor resultante é derivado dos campos de data / hora em o valor de entrada, e não é ajustada para o fuso horário ".
Aidan Rosswood
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.