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 :
- Stocker le nom du fuseau horaire d'un utilisateur en tant que préférence utilisateur (par exemple,
America/Los_Angeles
, pas-0700
). - Avoir des événements utilisateur/données temporelles soumis localement à leur cadre de référence (probablement un décalage par rapport à l'UTC, tel que
-0700
). - Dans l'application, convertissez l'heure en
UTC
et stocké à l'aide d'unTIMESTAMP WITH TIME ZONE
colonne. - Renvoyer les demandes d'heure locale au fuseau horaire d'un utilisateur (c'est-à-dire convertir à partir de
UTC
versAmerica/Los_Angeles
). - Définissez le
timezone
de votre base de données àUTC
.
Cette option ne fonctionne pas toujours car il peut être difficile d'obtenir le fuseau horaire d'un utilisateur et donc le conseil d'utiliser TIMESTAMP WITH TIME ZONE
pour les applications légères. Cela dit, permettez-moi d'expliquer plus en détail certains aspects contextuels de cette option 4.
Comme l'option 3, la raison du WITH TIME ZONE
c'est parce que l'heure à laquelle quelque chose s'est passé est un absolu instant dans le temps. WITHOUT TIME ZONE
donne un parent fuseau horaire. Ne mélangez jamais, jamais, jamais les horodatages 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 temporelles à l'aide d'un tri lexical). Encore une fois, PostgreSQL n'a pas besoin de cela pour effectuer correctement les calculs de date ou pour convertir entre les fuseaux horaires (c'est-à-dire que PostgreSQL est très habile pour convertir les heures entre deux fuseaux horaires arbitraires). Pour vous assurer 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 il fournit une mesure anti-footshooting 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 procéder, mais cela semble être la meilleure pratique d'après mon expérience.
Les critiques sur la gestion des fuseaux horaires de la base de données sont largement justifiées (il existe de nombreuses bases de données qui gèrent cela avec une grande incompétence), mais 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 TIMESTAMP WITHOUT TIME ZONE
relatif en utilisant le référentiel de votre cible (UTC
).
Lors de la conversion à partir d'un TIMESTAMP WITHOUT TIME ZONE
incomplet vers 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)
L'essentiel :
- stocker le fuseau horaire d'un utilisateur sous la forme d'un libellé nommé (par exemple,
America/Los_Angeles
) et non un décalage par rapport à UTC (par exemple,-0700
) - utiliser UTC pour tout sauf s'il existe une raison impérieuse de stocker un décalage non nul
- traiter toutes les heures UTC non nulles comme une erreur de saisie
- ne mélangez jamais les horodatages relatifs et absolus
- utiliser également
UTC
commetimezone
dans la base de données si possible
Remarque sur le langage de programmation aléatoire :datetime
de Python Le type de données est très bon 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).
MODIFIER
Laissez-moi vous 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 2011-05-28 2pm 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 temporelles dans une base de données sont absolues (et doivent donc être TIMESTAMP WITH TIME ZONE
, idéalement avec un décalage de +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 programmer 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", "rencontrons-nous 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 y a une rare exception qui mérite d'être soulignée et qui devrait illustrer la valeur des temps relatifs. Pour les événements futurs 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 concret :
Supposons que nous soyons en 2004 et que vous deviez programmer une livraison le 31 octobre 2008 à 13 h 00 sur la côte ouest des États-Unis (c'est-à-dire America/Los_Angeles
/PST8PDT
). Si vous avez stocké cela en utilisant l'heure absolue en utilisant ’2008-10-31 21:00:00.000000+00’::TIMESTAMP WITH TIME ZONE
, la livraison se serait produite à 14 heures parce que le gouvernement américain a adopté l'Energy Policy Act de 2005 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+, les bases de données de fuseaux horaires ont reconnu que 10-31-2008
aurait été l'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. Là où la coupure entre l'utilisation des temps relatifs et absolus pour planifier les choses est une ligne floue, mais ma règle d'or est que la planification pour quoi que ce soit dans le futur au-delà de 3 à 6 mois devrait utiliser des horodatages relatifs (prévu =absolu vs planifié =relatif ???).
L'autre/dernier type de temps relatif 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 également 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 (recurring_start_time + meeting_length)".
Dernière confusion :DATE
, TIME
, TIME WITHOUT TIME ZONE
et TIME WITH TIME ZONE
sont tous des types de données relatifs. 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 la DATE
représenté par le temps. Même avec '23:59:59-07'::TIME WITH TIME ZONE
, vous ne savez pas quelle est la DATE
serait. Et enfin, DATE
avec un fuseau horaire n'est pas en fait une DATE
, il s'agit d'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 des dates et des fuseaux horaires dans des bases de données est une bonne chose, mais il est facile d'obtenir des résultats subtilement incorrects. Un effort supplémentaire minimal est requis pour stocker correctement et complètement les informations de temps, mais cela ne signifie pas que l'effort supplémentaire est toujours nécessaire.