PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Ignorer complètement les fuseaux horaires dans Rails et PostgreSQL

Postgres a deux types de données d'horodatage différents :

  • timestamp with time zone , nom court :timestamptz
  • timestamp without time zone , nom court :timestamp

timestamptz est le préféré tapez la famille date/heure, littéralement. Il a typispreferred défini dans pg_type , qui peut être pertinent :

  • Générer des séries temporelles entre deux dates dans PostgreSQL

Stockage interne et époque

En interne, les horodatages occupent 8 octets de stockage sur disque et en RAM. Il s'agit d'une valeur entière représentant le nombre de microsecondes de l'époque Postgres, 2000-01-01 00:00:00 UTC.

Postgres a également une connaissance intégrée de l'heure UNIX couramment utilisée pour compter les secondes de l'époque UNIX, 1970-01-01 00:00:00 UTC, et l'utilise dans les fonctions to_timestamp(double precision) ou EXTRACT(EPOCH FROM timestamptz) .

Le code source :

* Timestamps, as well as the h/m/s fields of intervals, are stored as
* int64 values with units of microseconds.  (Once upon a time they were  
* double values with units of seconds.)

Et :

/* Julian-date equivalents of Day 0 in Unix and Postgres reckoning */  
#define UNIX_EPOCH_JDATE        2440588 /* == date2j(1970, 1, 1) */  
#define POSTGRES_EPOCH_JDATE    2451545 /* == date2j(2000, 1, 1) */  

La résolution en microsecondes se traduit par un maximum de 6 chiffres fractionnaires pour les secondes.

timestamp

Pour timestamp aucun fuseau horaire n'est fourni explicitement. Postgres ignore tout modificateur de fuseau horaire ajouté au littéral d'entrée par erreur !

Aucune heure n'est décalée pour l'affichage. Avec tout ce qui se passe dans le même fuseau horaire, c'est bien. Pour un fuseau horaire différent, la signification change, mais valeur et afficher reste le même.

timestamptz

Gestion de timestamptz est subtilement différent. Je cite le manuel ici :

Pour timestamp with time zone , la valeur stockée en interne est toujours en UTC (Temps Universel Coordonné ...)

Bold emphase mienne. Le fuseau horaire lui-même n'est jamais stocké . Il s'agit d'un modificateur d'entrée utilisé pour calculer l'horodatage UTC correspondant, qui est stocké - ou et le décorateur de sortie utilisé pour calculer l'heure locale pour l'affichage - avec un décalage de fuseau horaire ajouté. Si vous n'ajoutez pas de décalage pour timestamptz à l'entrée, le paramètre de fuseau horaire actuel de la session est supposé. Tous les calculs sont effectués avec des valeurs d'horodatage UTC. Si vous devez (peut-être) avoir à gérer plus d'un fuseau horaire, utilisez timestamptz . En d'autres termes :en cas de doute ou d'incompréhension concernant le fuseau horaire supposé, utilisez timestamptz . S'applique dans la plupart des cas d'utilisation.

Les clients comme psql ou pgAdmin ou toute application communiquant via libpq (comme Ruby avec le gem pg) sont présentés avec l'horodatage plus le décalage pour le fuseau horaire actuel ou selon une demande fuseau horaire (voir ci-dessous). C'est toujours le même moment , seul le format d'affichage varie. Ou, comme le dit le manuel :

Toutes les dates et heures sensibles au fuseau horaire sont stockées en interne en UTC. Ils sont convertis en heure locale dans le fuseau spécifié par le TimeZone paramètre de configuration avant d'être affiché au client.

Exemple en psql :

db=# SELECT timestamptz '2012-03-05 20:00+03';
      timestamptz
------------------------
 2012-03-05 18:00:00+01

Que s'est-il passé ici ?
J'ai choisi un décalage horaire arbitraire +3 pour le littéral d'entrée. Pour Postgres, ce n'est qu'une des nombreuses façons de saisir l'horodatage UTC 2012-03-05 17:00:00 . Le résultat de la requête est affiché pour le fuseau horaire actuel Vienne/Autriche dans mon test, qui a un décalage +1 en hiver et +2 pendant l'heure d'été ("heure d'été", DST). Alors 2012-03-05 18:00:00+01 car l'heure d'été n'intervient que plus tard.

Postgres oublie immédiatement le littéral d'entrée. Tout ce qu'il mémorise est la valeur du type de données. Comme avec un nombre décimal. numeric '003.4' ou numeric '+3.4' - les deux donnent exactement la même valeur interne.

AT TIME ZONE

Il ne manque plus qu'un outil pour interpréter ou représenter les littéraux d'horodatage en fonction d'un fuseau horaire spécifique. C'est là que le AT TIME ZONE construct entre en jeu. Il existe deux cas d'utilisation différents. timestamptz est converti en timestamp et vice versa.

Pour entrer l'UTC timestamptz 2012-03-05 17:00:00+0 :

SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC'

... qui équivaut à :

SELECT timestamptz '2012-03-05 17:00:00 UTC'

Pour afficher le même point dans le temps que EST timestamp (Heure normale de l'Est) :

SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC' AT TIME ZONE 'EST'

C'est exact, AT TIME ZONE 'UTC' deux fois . Le premier interprète le timestamp valeur sous forme d'horodatage UTC (donné) renvoyant le type timestamptz . La seconde convertit le timestamptz à l'timestamp dans le fuseau horaire donné 'EST' - ce qu'une horloge murale affiche dans le fuseau horaire EST à ce moment précis.

Exemples

SELECT ts AT TIME ZONE 'UTC'
FROM  (
   VALUES
      (1, timestamptz '2012-03-05 17:00:00+0')
    , (2, timestamptz '2012-03-05 18:00:00+1')
    , (3, timestamptz '2012-03-05 17:00:00 UTC')
    , (4, timestamp   '2012-03-05 11:00:00'  AT TIME ZONE '+6') 
    , (5, timestamp   '2012-03-05 17:00:00'  AT TIME ZONE 'UTC') 
    , (6, timestamp   '2012-03-05 07:00:00'  AT TIME ZONE 'US/Hawaii')  -- ①
    , (7, timestamptz '2012-03-05 07:00:00 US/Hawaii')                  -- ①
    , (8, timestamp   '2012-03-05 07:00:00'  AT TIME ZONE 'HST')        -- ①
    , (9, timestamp   '2012-03-05 18:00:00+1')  -- ② loaded footgun!
      ) t(id, ts);

Renvoie 8 (ou 9) identiques lignes avec un horodatagetz colonnes contenant le même horodatage UTC 2012-03-05 17:00:00 . La 9e rangée fonctionne en quelque sorte dans mon fuseau horaire, mais c'est un piège diabolique. Voir ci-dessous.

① Lignes 6 à 8 avec le fuseau horaire nom et fuseau horaire abréviation pour l'heure d'Hawaï sont soumis à l'heure d'été (heure d'été) et peuvent différer, mais pas actuellement. Un nom de fuseau horaire comme 'US/Hawaii' est automatiquement au courant des règles DST et de tous les changements historiques, tandis qu'une abréviation telle que HST est juste un code muet pour un décalage fixe. Vous devrez peut-être ajouter une abréviation différente pour l'heure d'été / standard. Le nom interprète correctement any horodatage au fuseau horaire donné. Une abréviation est bon marché, mais doit être le bon pour l'horodatage donné :

  • Les noms de fuseaux horaires avec des propriétés identiques donnent des résultats différents lorsqu'ils sont appliqués à l'horodatage

L'heure d'été ne fait pas partie des idées les plus brillantes que l'humanité ait jamais eues.

② Ligne 9, marquée comme arme à pied chargée fonctionne pour moi , mais seulement par hasard. Si vous convertissez explicitement un littéral en timestamp [without time zone] , tout décalage de fuseau horaire est ignoré ! Seul l'horodatage nu est utilisé. La valeur est alors automatiquement convertie en timestamptz dans l'exemple pour correspondre au type de colonne. Pour cette étape, le timezone le réglage de la session en cours est supposé, qui se trouve être le même fuseau horaire +1 dans mon cas (Europe/Vienne). Mais probablement pas dans votre cas - ce qui entraînera une valeur différente. En bref :ne diffusez pas timestamptz littéraux en timestamp ou vous perdez le décalage horaire.

Vos questions

L'utilisateur stocke une heure, disons le 17 mars 2012, 19 h 00. Je ne veux pas que les conversions de fuseau horaire ou le fuseau horaire soient stockés.

Le fuseau horaire lui-même n'est jamais stocké. Utilisez l'une des méthodes ci-dessus pour saisir un horodatage UTC.

J'utilise uniquement le fuseau horaire spécifié par l'utilisateur pour obtenir des enregistrements "avant" ou "après" l'heure actuelle dans le fuseau horaire local de l'utilisateur.

Vous pouvez utiliser une requête pour tous les clients dans différents fuseaux horaires.
Pour l'heure globale absolue :

SELECT * FROM tbl WHERE time_col > (now() AT TIME ZONE 'UTC')::time

Pour l'heure selon l'horloge locale :

SELECT * FROM tbl WHERE time_col > now()::time

Vous n'êtes pas encore fatigué des informations de fond ? Il y a plus dans le manuel.