¿Qué tipo de marca de tiempo debo elegir en una base de datos PostgreSQL?

119

Me gustaría definir una mejor práctica para almacenar marcas de tiempo en mi base de datos de Postgres en el contexto de un proyecto de múltiples zonas horarias.

puedo

  1. elija TIMESTAMP WITHOUT TIME ZONEy recuerde qué zona horaria se usó en el momento de la inserción para este campo
  2. elija TIMESTAMP WITHOUT TIME ZONEy agregue otro campo que contendrá el nombre de la zona horaria que se utilizó en el momento de la inserción
  3. elija TIMESTAMP WITH TIME ZONEe inserte las marcas de tiempo en consecuencia

Tengo una ligera preferencia por la opción 3 (marca de tiempo con zona horaria) pero me gustaría tener una opinión informada sobre el asunto.

Jerome WAGNER
fuente

Respuestas:

142

En primer lugar, el manejo del tiempo y la aritmética de PostgreSQL es fantástico y la Opción 3 está bien en el caso general. Sin embargo, es una vista incompleta de la hora y las zonas horarias y se puede complementar:

  1. Almacene el nombre de la zona horaria de un usuario como una preferencia del usuario (por ejemplo America/Los_Angeles, no -0700).
  2. Haga que los datos de eventos / tiempo de los usuarios se envíen localmente a su marco de referencia (probablemente un desplazamiento de UTC, como -0700).
  3. En la aplicación, convierta el tiempo en UTCuna TIMESTAMP WITH TIME ZONEcolumna y lo almacene mediante .
  4. Devolver las solicitudes de hora local a la zona horaria de un usuario (es decir, convertir de UTCa America/Los_Angeles).
  5. Configure su base de datos timezoneen UTC.

Esta opción no siempre funciona porque puede ser difícil obtener la zona horaria de un usuario y, por lo tanto, el consejo de cobertura TIMESTAMP WITH TIME ZONEpara aplicaciones livianas. Dicho esto, permítanme explicar algunos aspectos de fondo de esta Opción 4 con más detalle.

Al igual que la Opción 3, la razón WITH TIME ZONEes porque el momento en que sucedió algo es un momento absoluto en el tiempo. WITHOUT TIME ZONEproduce una zona horaria relativa . Nunca, nunca, nunca mezcle TIMESTAMPs absolutos y relativos.

Desde una perspectiva programática y de coherencia, asegúrese de que todos los cálculos se realicen utilizando UTC como zona horaria. Este no es un requisito de PostgreSQL, pero ayuda cuando se integra con otros lenguajes o entornos de programación. Establecer un CHECKen la columna para asegurarse de que la escritura en la columna de marca de tiempo tenga un desplazamiento de zona horaria de 0es una posición defensiva que evita algunas clases de errores (por ejemplo, un script vuelca datos en un archivo y algo más ordena los datos de tiempo usando un tipo léxico). Una vez más, PostgreSQL no necesita esto para hacer cálculos de fecha correctamente o para convertir entre zonas horarias (es decir, PostgreSQL es muy hábil para convertir tiempos entre dos zonas horarias arbitrarias). Para garantizar que los datos que ingresan a la base de datos se almacenen con un desplazamiento de cero:

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

No es 100% perfecto, pero proporciona una medida anti-disparo lo suficientemente fuerte que asegura que los datos ya estén convertidos a UTC. Hay muchas opiniones sobre cómo hacer esto, pero esta parece ser la mejor en la práctica según mi experiencia.

Las críticas al manejo de la zona horaria de la base de datos están en gran parte justificadas (hay muchas bases de datos que manejan esto con gran incompetencia), sin embargo, el manejo de PostgreSQL de marcas de tiempo y zonas horarias es bastante impresionante (a pesar de algunas "características" aquí y allá). Por ejemplo, una de esas características:

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

Tenga en cuenta que AT TIME ZONE 'UTC'elimina la información de la zona horaria y crea un pariente TIMESTAMP WITHOUT TIME ZONEutilizando el marco de referencia de su objetivo ( UTC).

Al convertir de incompleto TIMESTAMP WITHOUT TIME ZONEa incompleto TIMESTAMP WITH TIME ZONE, la zona horaria faltante se hereda de su conexión:

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)

La línea de fondo:

  • almacenar la zona horaria de un usuario como una etiqueta con nombre (p America/Los_Angeles. ej. ) y no un desplazamiento de UTC (p -0700. ej. )
  • use UTC para todo a menos que haya una razón convincente para almacenar un desplazamiento distinto de cero
  • tratar todas las horas UTC distintas de cero como un error de entrada
  • nunca mezcle y combine marcas de tiempo relativas y absolutas
  • también use UTCcomo timezoneen la base de datos si es posible

Nota sobre el lenguaje de programación aleatorio: el datetimetipo de datos de Python es muy bueno para mantener la distinción entre tiempos absolutos y relativos (aunque frustrante al principio hasta que lo complementa con una biblioteca como PyTZ ).


EDITAR

Permítanme explicar un poco más la diferencia entre relativo y absoluto.

El tiempo absoluto se utiliza para registrar un evento. Ejemplos: "El usuario 123 inició sesión" o "una ceremonia de graduación comienza en 2011-05-28 2pm PST". Independientemente de su zona horaria local, si pudiera teletransportarse al lugar donde ocurrió el evento, podría presenciar el evento. La mayoría de los datos de tiempo en una base de datos son absolutos (y por lo tanto deberían ser TIMESTAMP WITH TIME ZONE, idealmente con un desplazamiento +0 y una etiqueta textual que represente las reglas que gobiernan la zona horaria en particular, no un desplazamiento).

Un evento relativo sería registrar o programar la hora de algo desde la perspectiva de una zona horaria aún por determinar. Ejemplos: "las puertas de nuestra empresa abren a las 8 a. M. Y cierran a las 9 p. M.", "Nos reunimos todos los lunes a las 7 a. M. Para un desayuno semanal" o "cada Halloween a las 8 p. M.". En general, el tiempo relativo se usa en una plantilla o fábrica para eventos, y el tiempo absoluto se usa para casi todo lo demás. Hay una rara excepción que vale la pena señalar y que debería ilustrar el valor de los tiempos relativos. Para eventos futuros que son lo suficientemente lejanos en el futuro donde podría haber incertidumbre sobre el tiempo absoluto en el que podría ocurrir algo, use una marca de tiempo relativa. Aquí tienes un ejemplo del mundo real:

Suponga que es el año 2004 y necesita programar una entrega el 31 de octubre de 2008 a la 1 pm en la costa oeste de los EE. UU. (Es decir, America/Los_Angeles/ PST8PDT). Si lo almacenó usando el tiempo absoluto ’2008-10-31 21:00:00.000000+00’::TIMESTAMP WITH TIME ZONE, la entrega habría aparecido a las 2 pm porque el gobierno de los EE. UU. Aprobó la Ley de Política Energética de 2005 que cambió las reglas que rigen el horario de verano. En 2004, cuando se programó la entrega, la fecha 10-31-2008habría sido la hora estándar del Pacífico ( +8000), pero a partir del año 2005, las bases de datos de zonas horarias reconocieron que 10-31-2008habría sido el horario de verano del Pacífico (+0700). El almacenamiento de una marca de tiempo relativa con la zona horaria habría resultado en un programa de entrega correcto porque una marca de tiempo relativa es inmune a la manipulación mal informada del Congreso. Donde está el límite entre el uso de tiempos relativos y absolutos para programar cosas, es una línea difusa, pero mi regla general es que la programación para cualquier cosa en el futuro más allá de 3-6 meses debería hacer uso de marcas de tiempo relativas (programado = absoluto vs planeado = relativo ???).

El otro / último tipo de tiempo relativo es el INTERVAL. Ejemplo: "la sesión expirará 20 minutos después de que un usuario inicie sesión". Se INTERVALpuede usar correctamente con marcas de tiempo absolutas ( TIMESTAMP WITH TIME ZONE) o marcas de tiempo relativas ( TIMESTAMP WITHOUT TIME ZONE). Es igualmente correcto decir, "una sesión de usuario expira 20 minutos después de un inicio de sesión exitoso (login_utc + session_duration)" o "nuestra reunión de desayuno de la mañana solo puede durar 60 minutos (recurring_start_time + meeting_length)".

Últimos restos de confusión: DATE, TIME, TIME WITHOUT TIME ZONEy TIME WITH TIME ZONEson todos los tipos de datos relativos. Por ejemplo: '2011-05-28'::DATErepresenta una fecha relativa ya que no tiene información de zona horaria que pueda usarse para identificar la medianoche. Del mismo modo, '23:23:59'::TIMEes relativo porque no conoce ni la zona horaria ni la DATErepresentada por la hora. Incluso con '23:59:59-07'::TIME WITH TIME ZONE, no sabes cuál DATEsería. Y, por último, DATEcon una zona horaria no es de hecho una DATE, es una 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)

Poner fechas y zonas horarias en las bases de datos es algo bueno, pero es fácil obtener resultados sutilmente incorrectos. Se requiere un esfuerzo adicional mínimo para almacenar la información de tiempo de manera correcta y completa, sin embargo, eso no significa que siempre se requiera un esfuerzo adicional.

Sean
fuente
2
Si le dice con precisión a postgresql la zona horaria correcta en la que se encuentra la marca de tiempo del usuario, postgresql hará el trabajo pesado detrás de escena. Convertirlo usted mismo es solo pedir prestado un problema.
Seth Robertson
1
@Sean: con su restricción de cheque, ¿cómo puede insertar una marca de tiempo sin ella set timezone to 'UTC'? ¿Sabe que todas las fechas que reconocen la zona horaria se almacenan internamente en UTC ?
2
El objetivo de la verificación es asegurarse de que los datos se almacenen con compensación cero de UTC. La clasificación y recuperación de información y la comparación de tiempos con compensaciones distintas de cero es propensa a errores. Al hacer cumplir una compensación de UTC cero, puede interactuar de manera constante con los datos desde una única perspectiva de una manera de riesgo casi cero que se comporta de manera predecible en todos los escenarios. Si fuera práctico que las marcas de tiempo admitieran representaciones textuales de zonas horarias, mis pensamientos sobre el tema serían diferentes. : ~]
Sean
6
@Sean: Pero, como indica Jack, todas las marcas de tiempo que reconocen la zona horaria se almacenan fundamentalmente internamente en UTC y se convierten a su zona horaria local cuando se utilizan; efectivamente, extraer (zona horaria de ...) siempre devolverá la zona horaria local de la conexión: no tiene relación con cómo se "almacenó" la marca de tiempo. Dicho de otra manera, la zona horaria no es parte del tipo en absoluto y no se puede almacenar: el "con zona horaria" es solo una propiedad de cómo se convertirán los datos al interactuar con otros tipos. Por lo tanto, los datos no tienen ninguna representación de zonas horarias, textuales o de otro tipo.
Jay Freeman -saurik-
@ JayFreeman-saurik-: tienes toda la razón. El '' CHECK () '' está ahí como una medida anti-pisada para proteger contra un código posiblemente dudoso. Asegurarse de que los datos sean UTC en escritura proporciona una modesta garantía de que el código fue pensado o que el entorno de ejecución está configurado correctamente.
Sean
59

La respuesta de Sean es demasiado compleja y engañosa.

El hecho es que tanto "CON ZONA HORARIA" como "SIN ZONA HORARIA" almacenan el valor como una marca de tiempo UTC absoluta similar a Unix. La diferencia radica en cómo se muestra la marca de tiempo. Cuando está "CON zona horaria", el valor mostrado es el valor almacenado UTC traducido a la zona del usuario. Cuando "SIN zona horaria", el valor almacenado en UTC se tuerce para mostrar el mismo reloj sin importar la zona que haya configurado el usuario ".

La única situación en la que se puede usar "SIN zona horaria" es cuando se aplica un valor de esfera de reloj independientemente de la zona real. Por ejemplo, cuando una marca de tiempo indica cuándo pueden cerrar las cabinas de votación (es decir, cierran a las 20:00, independientemente de la zona horaria de una persona).

Utilice la opción 3. Utilice siempre "CON zona horaria" a menos que exista una razón muy específica para no hacerlo.

Arrendajo
fuente
10
David E. Wheeler, un importante experto de Postgres, estaría de acuerdo con su evaluación de acuerdo con su publicación, Use siempre TIMESTAMP WITH TIME ZONE .
Basil Bourque
2
¿Qué sucede si el navegador convierte la marca de tiempo UTC en la zona horaria local? Entonces, la base de datos nunca hará la conversión y solo contendrá UTC. ¿Sería aceptable "SIN zona horaria"?
dman
5

Mi preferencia es hacia la opción 3, ya que Postgres puede hacer una gran parte del trabajo recalculando las marcas de tiempo relativas a la zona horaria por usted, mientras que con las otras dos tendrá que hacerlo usted mismo. La sobrecarga de almacenamiento adicional de almacenar la marca de tiempo con una zona horaria es realmente insignificante a menos que esté hablando de millones de registros, en cuyo caso probablemente ya tenga requisitos de almacenamiento bastante sustanciosos de todos modos.

GordonM
fuente
19
Incorrecto. No hay gastos generales ... Postgres no almacena la zona horaria (por cierto, 'desplazamiento' es el término correcto, no la zona horaria). El TIMESTAMP WITH TIME ZONEnombre es engañoso. Realmente significa "prestar atención a cualquier desplazamiento especificado al insertar / actualizar y utilizar ese desplazamiento para ajustar la fecha y hora a UTC". El TIMESTAMP WITHOUT TIME ZONEnombre significa "ignore cualquier desplazamiento que pueda estar presente durante la inserción / actualización, considere las porciones de fecha y hora como en UTC sin necesidad de ajuste". Lea el documento con atención.
Basil Bourque
1
@BasilBourque gracias por esta información. Increíblemente útil. Para otros que lean esto, la línea del documento dice: "En un literal que se ha determinado que es una marca de tiempo sin zona horaria, PostgreSQL ignorará silenciosamente cualquier indicación de zona horaria. Es decir, el valor resultante se deriva de los campos de fecha / hora en el valor de entrada y no se ajusta para la zona horaria ".
Aidan Rosswood