Tout d'abord, la gestion du temps et l'arithmétique de PostgreSQL sont fantastiques et l'option 3 convient dans le cas général. Il s'agit cependant d'une vue incomplète de l'heure et des fuseaux horaires et peut être complétée:
- Stockez le nom du fuseau horaire d'un utilisateur en tant que préférence de l'utilisateur (par exemple
America/Los_Angeles
, non -0700
).
- Faites soumettre les données d'événements / d'heure de l'utilisateur localement à leur cadre de référence (probablement un décalage par rapport à UTC, par exemple
-0700
).
- Dans l'application, convertissez l'heure en
UTC
et stockée à l'aide d'une TIMESTAMP WITH TIME ZONE
colonne.
- Les demandes d'heure de retour sont locales au fuseau horaire d'un utilisateur (c.-à-d. Convertir de
UTC
en America/Los_Angeles
).
- Définissez votre base de données
timezone
sur UTC
.
Cette option ne fonctionne pas toujours car il peut être difficile d'obtenir le fuseau horaire d'un utilisateur et donc les conseils de couverture à utiliser TIMESTAMP WITH TIME ZONE
pour les applications légères. Cela dit, permettez-moi d'expliquer plus en détail certains aspects de base de cette option 4.
Comme pour l'option 3, la raison en WITH TIME ZONE
est que le moment où quelque chose s'est produit est un moment absolu dans le temps. WITHOUT TIME ZONE
donne un fuseau horaire relatif . Ne mélangez jamais, jamais, jamais des TIMESTAMP absolus et relatifs.
Du point de vue de la programmation et de la cohérence, assurez-vous que tous les calculs sont effectués en utilisant UTC comme fuseau horaire. Ce n'est pas une exigence PostgreSQL, mais cela aide lors de l'intégration avec d'autres langages de programmation ou environnements. Définir un CHECK
sur la colonne pour s'assurer que l'écriture dans la colonne d'horodatage a un décalage de fuseau horaire de 0
est une position défensive qui empêche quelques classes de bogues (par exemple, un script vide les données dans un fichier et quelque chose d'autre trie les données de temps à l'aide d'un tri lexical). Encore une fois, PostgreSQL n'en a pas besoin pour faire des calculs de date correctement ou pour effectuer une conversion entre les fuseaux horaires (c'est-à-dire que PostgreSQL est très habile à convertir les heures entre deux fuseaux horaires arbitraires). Pour garantir que les données entrant dans la base de données sont stockées avec un décalage de zéro:
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
Ce n'est pas parfait à 100%, mais cela fournit une mesure anti-footshoot suffisamment puissante qui garantit que les données sont déjà converties en UTC. Il y a beaucoup d'opinions sur la façon de faire cela, mais cela semble être la meilleure pratique de mon expérience.
Les critiques sur la gestion des fuseaux horaires des bases de données sont largement justifiées (il y a beaucoup de bases de données qui gèrent cela avec une grande incompétence), cependant la gestion par PostgreSQL des horodatages et des fuseaux horaires est assez impressionnante (malgré quelques «fonctionnalités» ici et là). Par exemple, une de ces fonctionnalités:
-- 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)
Notez que AT TIME ZONE 'UTC'
supprime les informations de fuseau horaire et crée un parent en TIMESTAMP WITHOUT TIME ZONE
utilisant le cadre de référence de votre cible ( UTC
).
Lors de la conversion d'un incomplet TIMESTAMP WITHOUT TIME ZONE
en un TIMESTAMP WITH TIME ZONE
, le fuseau horaire manquant est hérité de votre connexion:
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 ligne du bas:
- stocker le fuseau horaire d'un utilisateur sous la forme d'une étiquette nommée (par exemple
America/Los_Angeles
) et non d'un décalage par rapport à UTC (par exemple -0700
)
- utiliser UTC pour tout sauf s'il y a une raison impérieuse de stocker un décalage non nul
- traiter toutes les heures UTC non nulles comme une erreur d'entrée
- ne jamais mélanger et faire correspondre les horodatages relatifs et absolus
- aussi utiliser
UTC
comme timezone
dans la base de données si possible
Remarque sur le langage de programmation aléatoire: le datetime
type de données de Python est très efficace pour maintenir la distinction entre les temps absolus et relatifs (bien que frustrant au début jusqu'à ce que vous le complétiez avec une bibliothèque comme PyTZ ).
ÉDITER
Permettez-moi d'expliquer un peu plus la différence entre relatif et absolu.
Le temps absolu est utilisé pour enregistrer un événement. Exemples: «Utilisateur 123 connecté» ou «une cérémonie de remise des diplômes commence le 28/05/2011 à 14 h PST». Quel que soit votre fuseau horaire local, si vous pouviez vous téléporter là où l'événement s'est produit, vous pourriez être témoin de l'événement. La plupart des données de temps dans une base de données sont absolues (et devraient donc être TIMESTAMP WITH TIME ZONE
, idéalement, avec un décalage +0 et une étiquette textuelle représentant les règles régissant le fuseau horaire particulier - pas un décalage).
Un événement relatif serait d'enregistrer ou de planifier l'heure de quelque chose du point de vue d'un fuseau horaire encore à déterminer. Exemples: «les portes de notre entreprise ouvrent à 8h et ferment à 21h», «nous réunissons tous les lundis à 7h pour un petit-déjeuner hebdomadaire» ou «chaque Halloween à 20h». En général, le temps relatif est utilisé dans un modèle ou une fabrique pour les événements, et le temps absolu est utilisé pour presque tout le reste. Il existe une rare exception qui mérite d'être soulignée et qui devrait illustrer la valeur des temps relatifs. Pour les événements futurs qui sont suffisamment éloignés dans le futur où il pourrait y avoir une incertitude quant à l'heure absolue à laquelle quelque chose pourrait se produire, utilisez un horodatage relatif. Voici un exemple du monde réel:
Supposons que ce soit l'année 2004 et que vous deviez planifier une livraison le 31 octobre 2008 à 13 heures sur la côte ouest des États-Unis (c'est America/Los_Angeles
-à- dire / PST8PDT
). Si vous avez stocké cela en utilisant l'heure absolue ’2008-10-31 21:00:00.000000+00’::TIMESTAMP WITH TIME ZONE
, la livraison se serait affichée à 14 heures, car le gouvernement américain a adopté la loi de 2005 sur la politique énergétique qui a modifié les règles régissant l'heure d'été. En 2004, lorsque la livraison était prévue, la date 10-31-2008
aurait été l'heure normale du Pacifique ( +8000
), mais à partir de l'année 2005 et plus, les bases de données de fuseaux horaires ont reconnu qu'il s'agissait de l' 10-31-2008
heure d'été du Pacifique (+0700
). Le stockage d'un horodatage relatif avec le fuseau horaire aurait abouti à un calendrier de livraison correct, car un horodatage relatif est à l'abri de la falsification mal informée du Congrès. La limite entre l'utilisation des temps relatifs et absolus pour la planification des choses est une ligne floue, mais ma règle de base est que la planification de tout ce qui se trouve dans le futur au-delà de 3-6 mois devrait utiliser des horodatages relatifs (planifié = absolu vs planifié = relative ???).
L'autre / dernier type d'heure relative est le INTERVAL
. Exemple: "la session expirera 20 minutes après la connexion d'un utilisateur". Un INTERVAL
peut être utilisé correctement avec des horodatages absolus ( TIMESTAMP WITH TIME ZONE
) ou des horodatages relatifs ( TIMESTAMP WITHOUT TIME ZONE
). Il est tout aussi correct de dire, "une session utilisateur expire 20 minutes après une connexion réussie (login_utc + session_duration)" ou "notre petit-déjeuner du matin ne peut durer que 60 minutes (récurrent_start_time + meeting_length)".
Derniers bits de confusion: DATE
, TIME
, TIME WITHOUT TIME ZONE
et TIME WITH TIME ZONE
sont tous les types de données relatives. Par exemple: '2011-05-28'::DATE
représente une date relative puisque vous n'avez aucune information de fuseau horaire qui pourrait être utilisée pour identifier minuit. De même, '23:23:59'::TIME
est relatif car vous ne connaissez ni le fuseau horaire ni celui DATE
représenté par l'heure. Même avec '23:59:59-07'::TIME WITH TIME ZONE
, vous ne savez pas ce que ce DATE
serait. Et enfin, DATE
avec un fuseau horaire n'est pas en fait un DATE
, c'est un 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)
Mettre les dates et les fuseaux horaires dans les bases de données est une bonne chose, mais il est facile d'obtenir des résultats subtilement incorrects. Un minimum d'effort supplémentaire est nécessaire pour stocker correctement et complètement les informations de temps, mais cela ne signifie pas qu'un effort supplémentaire est toujours nécessaire.