Quel type d'horodatage dois-je choisir dans une base de données PostgreSQL?

119

Je souhaite définir une bonne pratique pour stocker les horodatages dans ma base de données Postgres dans le cadre d'un projet multi-fuseaux horaires.

je peux

  1. choisir TIMESTAMP WITHOUT TIME ZONEet mémoriser le fuseau horaire utilisé au moment de l'insertion pour ce champ
  2. choisissez TIMESTAMP WITHOUT TIME ZONEet ajoutez un autre champ qui contiendra le nom du fuseau horaire utilisé au moment de l'insertion
  3. choisissez TIMESTAMP WITH TIME ZONEet insérez les horodatages en conséquence

J'ai une légère préférence pour l'option 3 (horodatage avec fuseau horaire) mais j'aimerais avoir une opinion éclairée sur le sujet.

Jérôme WAGNER
la source

Réponses:

142

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:

  1. 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).
  2. 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).
  3. Dans l'application, convertissez l'heure en UTCet stockée à l'aide d'une TIMESTAMP WITH TIME ZONEcolonne.
  4. Les demandes d'heure de retour sont locales au fuseau horaire d'un utilisateur (c.-à-d. Convertir de UTCen America/Los_Angeles).
  5. Définissez votre base de données timezonesur 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 ZONEpour 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 ZONEest que le moment où quelque chose s'est produit est un moment absolu dans le temps. WITHOUT TIME ZONEdonne 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 CHECKsur la colonne pour s'assurer que l'écriture dans la colonne d'horodatage a un décalage de fuseau horaire de 0est 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 ZONEutilisant le cadre de référence de votre cible ( UTC).

Lors de la conversion d'un incomplet TIMESTAMP WITHOUT TIME ZONEen 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 UTCcomme timezonedans la base de données si possible

Remarque sur le langage de programmation aléatoire: le datetimetype 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-2008aurait é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-2008heure 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 INTERVALpeut ê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 ZONEet TIME WITH TIME ZONEsont tous les types de données relatives. Par exemple: '2011-05-28'::DATErepré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'::TIMEest relatif car vous ne connaissez ni le fuseau horaire ni celui DATEreprésenté par l'heure. Même avec '23:59:59-07'::TIME WITH TIME ZONE, vous ne savez pas ce que ce DATEserait. Et enfin, DATEavec 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.

Sean
la source
2
Si vous indiquez avec précision à postgresql le fuseau horaire correct dans lequel se trouve l'horodatage de l'utilisateur, postgresql fera le gros du travail dans les coulisses. Le convertir vous-même n'est qu'un emprunt.
Seth Robertson
1
@Sean - avec votre contrainte de vérification, comment pouvez-vous insérer un horodatage sans set timezone to 'UTC'? Vous savez que toutes les dates tenant compte du fuseau horaire sont stockées en interne en UTC ?
2
Le but de la vérification est de s'assurer que les données sont stockées avec un décalage nul par rapport à UTC. Le tri et la récupération des informations et la comparaison des temps avec des décalages non nuls sont sujets aux erreurs. En appliquant un décalage UTC nul, vous pouvez interagir de manière cohérente avec les données à partir d'une perspective unique d'une manière presque sans risque qui se comporte de manière prévisible dans tous les scénarios. S'il était pratique que les horodatages prennent en charge les représentations textuelles des fuseaux horaires, mes pensées sur le sujet seraient différentes. : ~]
Sean
6
@Sean: Mais, comme Jack l'indique, tous les horodatages sensibles au fuseau horaire sont fondamentalement stockés en interne en UTC et sont convertis dans votre fuseau horaire local lorsqu'ils sont utilisés; effectivement, extraire (fuseau horaire de ...) retournera alors toujours quel que soit le fuseau horaire local de la connexion: il n'a aucun rapport avec la façon dont l'horodatage a été "stocké". En d'autres termes, le fuseau horaire ne fait pas du tout partie du type et ne peut pas être stocké: le "avec fuseau horaire" est simplement une propriété de la façon dont les données seront converties lors de l'interaction avec d'autres types. Les données n'ont donc aucune représentation des fuseaux horaires, textuels ou autres.
Jay Freeman -saurik-
@ JayFreeman-saurik-: vous avez tout à fait raison. Le '' CHECK () '' est là comme une mesure anti-footshooting pour se protéger contre un code potentiellement douteux. S'assurer que les données sont en UTC lors de l'écriture offre une modeste garantie que le code a été pensé ou que l'environnement d'exécution est correctement configuré.
Sean
59

La réponse de Sean est trop complexe et trompeuse.

Le fait est que "AVEC FUSEAU HORAIRE" et "SANS ZONE HORAIRE" stockent la valeur sous la forme d'un horodatage UTC absolu de type Unix. La différence réside dans la façon dont l'horodatage est affiché. Lorsque "AVEC fuseau horaire", la valeur affichée est la valeur stockée UTC traduite dans la zone de l'utilisateur. Lorsque "SANS fuseau horaire", la valeur enregistrée UTC est tordue de manière à afficher le même cadran d'horloge quelle que soit la zone définie par l'utilisateur ".

La seule situation où un "SANS fuseau horaire" est utilisable est lorsqu'une valeur de cadran d'horloge est applicable indépendamment de la zone réelle. Par exemple, lorsqu'un horodatage indique à quel moment les isoloirs peuvent fermer (c'est-à-dire qu'ils ferment à 20h00 quel que soit le fuseau horaire d'une personne).

Utilisez le choix 3. Utilisez toujours "AVEC fuseau horaire" sauf s'il y a une raison très spécifique de ne pas le faire.

Geai
la source
10
David E. Wheeler, un expert majeur de Postgres, serait d'accord avec votre évaluation selon sa publication, Toujours utiliser HORAIRE AVEC FUSEAU HORAIRE .
Basil Bourque
2
Et si le navigateur convertissait l'horodatage UTC en fuseau horaire local? Ainsi, la base de données ne fera jamais la conversion et ne contiendra que UTC. Est-ce que "SANS fuseau horaire" serait acceptable?
dman
5

Ma préférence va à l'option 3, car Postgres peut alors effectuer une grande partie du travail de recalcul des horodatages par rapport au fuseau horaire pour vous, alors qu'avec les deux autres, vous devrez le faire vous-même. La surcharge de stockage supplémentaire liée au stockage de l'horodatage avec un fuseau horaire est vraiment négligeable à moins que vous ne parliez de millions d'enregistrements, auquel cas vous avez probablement déjà des besoins de stockage assez importants de toute façon.

GordonM
la source
19
Incorrect. Il n'y a pas de surcharge… Postgres ne stocke pas le fuseau horaire («offset» est le terme correct, pas le fuseau horaire, d'ailleurs). Le TIMESTAMP WITH TIME ZONEnom est trompeur. Cela signifie vraiment "faites attention à tout décalage spécifié lors de l'insertion / mise à jour et utilisez ce décalage pour ajuster la date-heure à UTC". Le TIMESTAMP WITHOUT TIME ZONEnom signifie "ignorer tout décalage qui peut être présent pendant l'insertion / la mise à jour, considérer les parties de date et d'heure comme étant en UTC sans besoin d'ajustement". Lisez attentivement la documentation .
Basil Bourque
1
@BasilBourque merci pour cette information. Incroyablement utile. Pour les autres lisant ceci, la ligne du document dit: "Dans un littéral qui a été déterminé comme étant un horodatage sans fuseau horaire, PostgreSQL ignorera silencieusement toute indication de fuseau horaire. Autrement dit, la valeur résultante est dérivée des champs date / heure dans la valeur d'entrée et n'est pas ajustée pour le fuseau horaire. »
Aidan Rosswood