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:
- Almacene el nombre de la zona horaria de un usuario como una preferencia del usuario (por ejemplo
America/Los_Angeles
, no -0700
).
- 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
).
- En la aplicación, convierta el tiempo en
UTC
una TIMESTAMP WITH TIME ZONE
columna y lo almacene mediante .
- Devolver las solicitudes de hora local a la zona horaria de un usuario (es decir, convertir de
UTC
a America/Los_Angeles
).
- Configure su base de datos
timezone
en 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 ZONE
para 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 ZONE
es porque el momento en que sucedió algo es un momento absoluto en el tiempo. WITHOUT TIME ZONE
produce 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 CHECK
en la columna para asegurarse de que la escritura en la columna de marca de tiempo tenga un desplazamiento de zona horaria de 0
es 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 ZONE
utilizando el marco de referencia de su objetivo ( UTC
).
Al convertir de incompleto TIMESTAMP WITHOUT TIME ZONE
a 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
UTC
como timezone
en la base de datos si es posible
Nota sobre el lenguaje de programación aleatorio: el datetime
tipo 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-2008
habrí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-2008
habrí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 INTERVAL
puede 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 ZONE
y TIME WITH TIME ZONE
son todos los tipos de datos relativos. Por ejemplo: '2011-05-28'::DATE
representa 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'::TIME
es relativo porque no conoce ni la zona horaria ni la DATE
representada por la hora. Incluso con '23:59:59-07'::TIME WITH TIME ZONE
, no sabes cuál DATE
sería. Y, por último, DATE
con 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.