PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Ignorando completamente os fusos horários no Rails e no PostgreSQL


O Postgres tem dois tipos de dados de timestamp diferentes:
  • timestamp with time zone , nome curto:timestamptz
  • timestamp without time zone , nome curto:timestamp

timestamptz é o preferido digite a família de data/hora, literalmente. Tem typispreferred definido em pg_type , que pode ser relevante:
  • Gerando séries temporais entre duas datas no PostgreSQL

Armazenamento interno e época


Internamente, os carimbos de data/hora ocupam 8 bytes de armazenamento em disco e em RAM. É um valor inteiro que representa a contagem de microssegundos da época do Postgres, 2000-01-01 00:00:00 UTC.

O Postgres também possui conhecimento interno do tempo UNIX comumente usado contando segundos da época UNIX, 1970-01-01 00:00:00 UTC, e usa isso nas funções to_timestamp(double precision) ou EXTRACT(EPOCH FROM timestamptz) .

O código fonte:
* Timestamps, as well as the h/m/s fields of intervals, are stored as
* int64 values with units of microseconds.  (Once upon a time they were  
* double values with units of seconds.)

E:
/* Julian-date equivalents of Day 0 in Unix and Postgres reckoning */  
#define UNIX_EPOCH_JDATE        2440588 /* == date2j(1970, 1, 1) */  
#define POSTGRES_EPOCH_JDATE    2451545 /* == date2j(2000, 1, 1) */  

A resolução de microssegundos se traduz em um máximo de 6 dígitos fracionários por segundos.

timestamp


Para timestamp nenhum fuso horário é fornecido explicitamente. Postgres ignora qualquer modificador de fuso horário adicionado ao literal de entrada por engano!

Nenhuma hora é deslocada para exibição. Com tudo acontecendo no mesmo fuso horário, tudo bem. Para um fuso horário diferente, o significado muda, mas valor e exibir continue o mesmo.

timestamptz


Manipulação de timestamptz é sutilmente diferente. Cito o manual aqui:

Para timestamp with time zone , o valor armazenado internamente é sempre em UTC (Tempo Universal Coordenado...)

Minha ênfase em negrito. O fuso horário em si nunca é armazenado . É um modificador de entrada usado para calcular o timestamp UTC correspondente, que é armazenado - ou um decorador de saída usado para calcular a hora local para exibição - com deslocamento de fuso horário anexado. Se você não anexar um deslocamento para timestamptz na entrada, a configuração de fuso horário atual da sessão é assumida. Todos os cálculos são feitos com valores de carimbo de data/hora UTC. Se você (talvez) tiver que lidar com mais de um fuso horário, use timestamptz . Em outras palavras:se houver alguma dúvida ou mal-entendido sobre o fuso horário assumido, use timestamptz . Aplica-se na maioria dos casos de uso.

Clientes como psql ou pgAdmin ou qualquer aplicativo se comunicando via libpq (como Ruby com a gem pg) são apresentados com o timestamp mais o deslocamento para o fuso horário atual ou de acordo com um solicitado fuso horário (veja abaixo). É sempre o mesmo momento , apenas o formato de exibição varia. Ou, como diz o manual:

Todas as datas e horas com reconhecimento de fuso horário são armazenadas internamente em UTC. Eles são convertidos para a hora local na zona especificada pelo TimeZone parâmetro de configuração antes de ser exibido ao cliente.

Exemplo no psql:
db=# SELECT timestamptz '2012-03-05 20:00+03';
      timestamptz
------------------------
 2012-03-05 18:00:00+01

O que aconteceu aqui?
Eu escolhi um deslocamento de fuso horário arbitrário +3 para o literal de entrada. Para o Postgres, esta é apenas uma das muitas maneiras de inserir o timestamp UTC 2012-03-05 17:00:00 . O resultado da consulta é exibido para a configuração de fuso horário atual Viena/Áustria no meu teste, que tem um deslocamento +1 durante o inverno e +2 durante o horário de verão ("horário de verão", DST). Então 2012-03-05 18:00:00+01 pois o horário de verão só entra em ação mais tarde.

O Postgres esquece o literal de entrada imediatamente. Tudo o que ele lembra é o valor para o tipo de dados. Assim como com um número decimal. numeric '003.4' ou numeric '+3.4' - ambos resultam exatamente no mesmo valor interno.

AT TIME ZONE


Tudo o que falta agora é uma ferramenta para interpretar ou representar literais de timestamp de acordo com um fuso horário específico. É aí que o AT TIME ZONE construto entra em cena. Existem dois casos de uso diferentes. timestamptz é convertido para timestamp e vice versa.

Para inserir o UTC timestamptz 2012-03-05 17:00:00+0 :
SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC'

... que é equivalente a:
SELECT timestamptz '2012-03-05 17:00:00 UTC'

Para exibir o mesmo momento que EST timestamp (Horário Padrão do Leste):
SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC' AT TIME ZONE 'EST'

Isso mesmo, AT TIME ZONE 'UTC' duas vezes . O primeiro interpreta o timestamp valor como (dado) timestamp UTC retornando o tipo timestamptz . O segundo converte o timestamptz para o timestamp no fuso horário 'EST' - o que um relógio de parede exibe no fuso horário EST neste momento.

Exemplos

SELECT ts AT TIME ZONE 'UTC'
FROM  (
   VALUES
      (1, timestamptz '2012-03-05 17:00:00+0')
    , (2, timestamptz '2012-03-05 18:00:00+1')
    , (3, timestamptz '2012-03-05 17:00:00 UTC')
    , (4, timestamp   '2012-03-05 11:00:00'  AT TIME ZONE '+6') 
    , (5, timestamp   '2012-03-05 17:00:00'  AT TIME ZONE 'UTC') 
    , (6, timestamp   '2012-03-05 07:00:00'  AT TIME ZONE 'US/Hawaii')  -- ①
    , (7, timestamptz '2012-03-05 07:00:00 US/Hawaii')                  -- ①
    , (8, timestamp   '2012-03-05 07:00:00'  AT TIME ZONE 'HST')        -- ①
    , (9, timestamp   '2012-03-05 18:00:00+1')  -- ② loaded footgun!
      ) t(id, ts);

Retorna 8 (ou 9) idênticos linhas com colunas de timestamptz com o mesmo timestamp UTC 2012-03-05 17:00:00 . A 9ª linha funciona no meu fuso horário, mas é uma armadilha do mal. Ver abaixo.

① Linhas 6 a 8 com fuso horário nome e fuso horário abreviatura para o horário do Havaí estão sujeitos ao DST (horário de verão) e podem ser diferentes, embora não atualmente. Um nome de fuso horário como 'US/Hawaii' está ciente das regras de horário de verão e todas as mudanças históricas automaticamente, enquanto uma abreviação como HST é apenas um código idiota para um deslocamento fixo. Pode ser necessário anexar uma abreviação diferente para o horário de verão/padrão. O nome interpreta corretamente qualquer timestamp no fuso horário especificado. Uma abreviatura é barato, mas precisa ser o correto para o timestamp fornecido:
  • Nomes de fuso horário com propriedades idênticas geram resultados diferentes quando aplicados ao carimbo de data/hora

O horário de verão não está entre as ideias mais brilhantes que a humanidade já teve.

② Linha 9, marcada como espingarda carregada funciona para mim , mas apenas por coincidência. Se você converter explicitamente um literal para timestamp [without time zone] , qualquer deslocamento de fuso horário é ignorado! Somente o timestamp simples é usado. O valor é então automaticamente coagido para timestamptz no exemplo para corresponder ao tipo de coluna. Para esta etapa, o timezone a configuração da sessão atual é assumida, que por acaso é o mesmo fuso horário +1 no meu caso (Europa/Viena). Mas provavelmente não no seu caso - o que resultará em um valor diferente. Resumindo:não converta timestamptz literais para timestamp ou você perde o deslocamento de fuso horário.

Suas perguntas


O usuário armazena um horário, digamos, 17 de março de 2012, 19h. Não quero que as conversões de fuso horário ou o fuso horário sejam armazenados.

O fuso horário em si nunca é armazenado. Use um dos métodos acima para inserir um carimbo de data/hora UTC.

Eu só uso o fuso horário especificado pelos usuários para obter registros 'antes' ou 'depois' da hora atual no fuso horário local dos usuários.

Você pode usar uma consulta para todos os clientes em fusos horários diferentes.
Para tempo global absoluto:
SELECT * FROM tbl WHERE time_col > (now() AT TIME ZONE 'UTC')::time

Para hora de acordo com o relógio local:
SELECT * FROM tbl WHERE time_col > now()::time

Ainda não está cansado de informações básicas? Tem mais no manual.