sql >> Base de Datos >  >> RDS >> PostgreSQL

Ignorando las zonas horarias por completo en Rails y PostgreSQL

Postgres tiene dos tipos de datos de marca de tiempo diferentes:

  • timestamp with time zone , nombre corto:timestamptz
  • timestamp without time zone , nombre corto:timestamp

timestamptz es el preferido escriba la familia de fecha/hora, literalmente. Tiene typispreferred establecido en pg_type , que puede ser relevante:

  • Generando series de tiempo entre dos fechas en PostgreSQL

Almacenamiento interno y época

Internamente, las marcas de tiempo ocupan 8 bytes de almacenamiento en disco y en RAM. Es un valor entero que representa el conteo de microsegundos desde la época de Postgres, 2000-01-01 00:00:00 UTC.

Postgres también tiene conocimiento integrado de los segundos de conteo de tiempo de UNIX de uso común desde la época de UNIX, 1970-01-01 00:00:00 UTC, y lo usa en funciones to_timestamp(double precision) o EXTRACT(EPOCH FROM timestamptz) .

El código fuente:

* 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.)

Y:

/* 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) */  

La resolución de microsegundos se traduce a un máximo de 6 dígitos fraccionarios por segundos.

timestamp

Para timestamp no se proporciona ninguna zona horaria explícitamente. Postgres ignora cualquier modificador de zona horaria agregado al literal de entrada por error!

No se cambian las horas para la visualización. Con todo lo que sucede en la misma zona horaria, está bien. Para una zona horaria diferente, el significado cambia, pero valor y mostrar sigue igual.

timestamptz

Manejo de timestamptz es sutilmente diferente. Cito el manual aquí:

Para timestamp with time zone , el valor almacenado internamente es siempre en UTC (Tiempo Universal Coordinado...)

Énfasis en negrita mío. La zona horaria en sí nunca se almacena . Es un modificador de entrada que se usa para calcular la marca de tiempo UTC correspondiente, que se almacena, o un decorador de salida que se usa para calcular la hora local para mostrar, con el desplazamiento de zona horaria adjunto. Si no agrega un desplazamiento para timestamptz en la entrada, se asume la configuración de zona horaria actual de la sesión. Todos los cálculos se realizan con valores de marca de tiempo UTC. Si (puede) tener que lidiar con más de una zona horaria, use timestamptz . En otras palabras:si puede haber alguna duda o malentendido sobre la zona horaria asumida, vaya con timestamptz . Se aplica en la mayoría de los casos de uso.

Los clientes como psql o pgAdmin o cualquier aplicación que se comunique a través de libpq (como Ruby con pg gem) se presentan con la marca de tiempo más el desplazamiento para la zona horaria actual o según un solicitado zona horaria (ver abajo). Siempre es el mismo punto en el tiempo , solo varía el formato de visualización. O, como dice el manual:

Todas las fechas y horas con reconocimiento de zona horaria se almacenan internamente en UTC. Se convierten a la hora local en la zona especificada por TimeZone parámetro de configuración antes de mostrarse al cliente.

Ejemplo en psql:

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

¿Qué pasó aquí?
Elegí una diferencia de zona horaria arbitraria +3 para el literal de entrada. Para Postgres, esta es solo una de las muchas formas de ingresar la marca de tiempo UTC 2012-03-05 17:00:00 . El resultado de la consulta se muestra para la configuración de la zona horaria actual Vienna/Austria en mi prueba, que tiene un desplazamiento +1 durante el invierno y +2 durante el horario de verano ("horario de verano", DST). Así que 2012-03-05 18:00:00+01 ya que el horario de verano solo se activa más tarde.

Postgres olvida el literal de entrada inmediatamente. Todo lo que recuerda es el valor para el tipo de datos. Al igual que con un número decimal. numeric '003.4' o numeric '+3.4' - ambos dan como resultado exactamente el mismo valor interno.

AT TIME ZONE

Todo lo que falta ahora es una herramienta para interpretar o representar literales de marca de tiempo según una zona horaria específica. Ahí es donde el AT TIME ZONE entra en juego la construcción. Hay dos casos de uso diferentes. timestamptz se convierte a timestamp y viceversa.

Para ingresar el UTC timestamptz 2012-03-05 17:00:00+0 :

SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC'

... que es equivalente a:

SELECT timestamptz '2012-03-05 17:00:00 UTC'

Para mostrar el mismo punto en el tiempo que EST timestamp (hora estándar del este):

SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC' AT TIME ZONE 'EST'

Así es, AT TIME ZONE 'UTC' dos veces . El primero interpreta el timestamp valor como (dado) marca de tiempo UTC que devuelve el tipo timestamptz . El segundo convierte el timestamptz a la timestamp en la zona horaria dada 'EST':lo que muestra un reloj de pared en la zona horaria EST en este momento.

Ejemplos

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);

Devuelve 8 (o 9) idéntico filas con una marca de tiempo tz columnas con la misma marca de tiempo UTC 2012-03-05 17:00:00 . La novena fila funciona en mi zona horaria, pero es una trampa maligna. Ver más abajo.

① Filas 6 - 8 con zona horaria nombre y zona horaria abreviatura para la hora de Hawái están sujetos al DST (horario de verano) y pueden diferir, aunque actualmente no. Un nombre de zona horaria como 'US/Hawaii' conoce las reglas del horario de verano y todos los turnos históricos automáticamente, mientras que una abreviatura como HST es solo un código tonto para un desplazamiento fijo. Es posible que deba agregar una abreviatura diferente para el horario de verano/estándar. El nombre interpreta correctamente any marca de tiempo en la zona horaria dada. Una abreviatura es barato, pero debe ser el correcto para la marca de tiempo dada:

  • Los nombres de zona horaria con propiedades idénticas arrojan resultados diferentes cuando se aplican a la marca de tiempo

El horario de verano no se encuentra entre las ideas más brillantes que se le ocurrieron a la humanidad.

② Fila 9, marcada como pistola cargada funciona para mí , pero sólo por coincidencia. Si envía explícitamente un literal a timestamp [without time zone] , ¡se ignora cualquier compensación de zona horaria! Solo se utiliza la marca de tiempo desnuda. El valor se coacciona automáticamente a timestamptz en el ejemplo para que coincida con el tipo de columna. Para este paso, la timezone se asume la configuración de la sesión actual, que resulta ser la misma zona horaria +1 en mi caso (Europa/Viena). Pero probablemente no en su caso, lo que dará como resultado un valor diferente. En resumen:no emita timestamptz literales a timestamp o pierde la compensación de la zona horaria.

Tus preguntas

El usuario almacena una hora, digamos el 17 de marzo de 2012 a las 7 p. m. No quiero que se almacenen las conversiones de zona horaria o la zona horaria.

La zona horaria en sí nunca se almacena. Utilice uno de los métodos anteriores para ingresar una marca de tiempo UTC.

Solo uso la zona horaria especificada por los usuarios para obtener registros "antes" o "después" de la hora actual en la zona horaria local de los usuarios.

Puede utilizar una consulta para todos los clientes en diferentes zonas horarias.
Para la hora global absoluta:

SELECT * FROM tbl WHERE time_col > (now() AT TIME ZONE 'UTC')::time

Para la hora según el reloj local:

SELECT * FROM tbl WHERE time_col > now()::time

¿Aún no estás cansado de la información de fondo? Hay más en el manual.