Zeitzonen in Rails und PostgreSQL insgesamt ignorieren

164

Ich beschäftige mich mit Datums- und Uhrzeitangaben in Rails und Postgres und stoße auf dieses Problem:

Die Datenbank ist in UTC.

Der Benutzer legt in der Rails-App eine Zeitzone fest, die jedoch nur verwendet werden soll, wenn die Ortszeit des Benutzers zum Vergleichen der Zeiten abgerufen wird.

Der Benutzer speichert eine Uhrzeit, z. B. 17. März 2012, 19 Uhr. Ich möchte nicht, dass Zeitzonen-Konvertierungen oder die Zeitzone gespeichert werden. Ich möchte nur, dass Datum und Uhrzeit gespeichert werden. Wenn der Benutzer seine Zeitzone ändert, wird auf diese Weise immer noch der 17. März 2012, 19 Uhr angezeigt.

Ich verwende nur die vom Benutzer angegebene Zeitzone, um Datensätze "vor" oder "nach" der aktuellen Zeit in der lokalen Zeitzone des Benutzers abzurufen.

Ich verwende derzeit "Zeitstempel ohne Zeitzone", aber wenn ich die Datensätze abrufe, konvertiert Rails (?) Sie in die Zeitzone in der App, die ich nicht möchte.

Appointment.first.time
 => Fri, 02 Mar 2012 19:00:00 UTC +00:00 

Da die Datensätze in der Datenbank als UTC zu erscheinen scheinen, besteht mein Hack darin, die aktuelle Zeit zu erfassen, die Zeitzone mit 'Date.strptime (str, "% m /% d /% Y")' zu entfernen und dann meine auszuführen Abfrage damit:

.where("time >= ?", date_start)

Es scheint eine einfachere Möglichkeit zu geben, Zeitzonen überall einfach zu ignorieren. Irgendwelche Ideen?

99 Meilen
quelle

Antworten:

347

Der Datentyp timestampist der Kurzname für timestamp without time zone.
Die andere Option timestamptzist die Abkürzung für timestamp with time zone.

timestamptzist buchstäblich der bevorzugte Typ in der Datums- / Zeitfamilie. Es hat typispreferredeingesetzt pg_type, was relevant sein kann:

Interner Speicher und Epoche

Intern belegen Zeitstempel 8 Byte Speicherplatz auf der Festplatte und im RAM. Dies ist ein ganzzahliger Wert, der die Anzahl der Mikrosekunden aus der Postgres-Epoche 2000-01-01 00:00:00 UTC darstellt.

Postgres verfügt außerdem über integrierte Kenntnisse über die häufig verwendete UNIX- Zeitzählung von Sekunden aus der UNIX-Epoche 1970-01-01 00:00:00 UTC und verwendet diese in Funktionen to_timestamp(double precision)oder EXTRACT(EPOCH FROM timestamptz).

Der Quellcode:

* Zeitstempel sowie die Intervallfelder h / m / s werden als gespeichert
* int64-Werte mit Einheiten von Mikrosekunden. (Es war einmal  
* doppelte Werte mit Einheiten von Sekunden.)

Und:

/ * Julianische Datumsäquivalente von Tag 0 in Unix- und Postgres-Abrechnung * /  
#define UNIX_EPOCH_JDATE 2440588 / * == date2j (1970, 1, 1) * /  
#define POSTGRES_EPOCH_JDATE 2451545 / * == date2j (2000, 1, 1) * /  

Die Mikrosekundenauflösung entspricht maximal 6 Bruchstellen für Sekunden.

timestamp

Ein als eingegebener Wert teilt Postgres mit, dass keine Zeitzone explizit angegeben wird. Die aktuelle Zeitzone wird angenommen. Postgres ignoriert jeden versehentlich hinzugefügten Zeitzonenmodifikator!timestamp [without time zone]

Für die Anzeige werden keine Stunden verschoben. Bei gleicher Zeitzoneneinstellung ist alles in Ordnung. Bei einer anderen Zeitzoneneinstellung ändert sich die Bedeutung, aber Wert und Anzeige bleiben gleich.

timestamptz

Der Umgang mit timestamp with time zoneist subtil anders. Ich zitiere das Handbuch hier :

Denn timestamp with time zoneder intern gespeicherte Wert ist immer in UTC (Universal Coordinated Time ...)

Meine kühne Betonung. Die Zeitzone selbst wird niemals gespeichert . Es ist ein Eingabemodifikator, der zur Berechnung des entsprechenden UTC-Zeitstempels verwendet wird, der gespeichert wird - oder ein Ausgabemodifikator, der zur Berechnung der lokalen Anzeigezeit verwendet wird - mit angefügtem Zeitzonenversatz. Wenn Sie timestamptzbei der Eingabe keinen Versatz anhängen , wird die aktuelle Zeitzoneneinstellung der Sitzung angenommen. Alle Berechnungen werden mit UTC-Zeitstempelwerten durchgeführt. Wenn Sie mit mehr als einer Zeitzone arbeiten müssen (oder müssen), verwenden Sie timestamptz.

Kunden wie psql oder pgAdmin oder jede Anwendung kommuniziert über libpq (wie Ruby mit dem gem pg) mit dem Zeitstempel dargestellt und für die Offset aktuellen Zeitzone oder entsprechend eine angeforderten Zeitzone (siehe unten). Es ist immer der gleiche Zeitpunkt , nur das Anzeigeformat variiert. Oder, wie es im Handbuch heißt :

Alle zeitzonenbezogenen Daten und Zeiten werden intern in UTC gespeichert. Sie werden in der durch den TimeZone- Konfigurationsparameter angegebenen Zone in die Ortszeit konvertiert, bevor sie dem Client angezeigt werden.

Betrachten Sie dieses einfache Beispiel (in psql):

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

Meine kühne Betonung. Was ist hier passiert?
Ich habe einen beliebigen Zeitzonenversatz +3für das Eingabeliteral gewählt. Für Postgres ist dies nur eine von vielen Möglichkeiten, den UTC-Zeitstempel einzugeben 2012-03-05 17:00:00. Das Ergebnis der Abfrage wird für die aktuelle Zeitzoneneinstellung Wien / Österreich in meinem Test angezeigt , die im Winter und im Sommer versetzt ist : , weil sie in die Winterzeit fällt.+1+22012-03-05 18:00:00+01

Postgres hat bereits vergessen, wie dieser Wert eingegeben wurde. Alles, woran es sich erinnert, ist der Wert und der Datentyp. Genau wie bei einer Dezimalzahl. numeric '003.4', numeric '3.40'oder numeric '+3.4'- alle ergeben genau den gleichen internen Wert.

AT TIME ZONE

Sobald Sie diese Logik verstanden haben, können Sie alles tun, was Sie wollen. Jetzt fehlt nur noch ein Tool zum Interpretieren oder Darstellen von Zeitstempelliteralen für eine bestimmte Zeitzone. Hier kommt das AT TIME ZONEKonstrukt ins Spiel. Es gibt zwei verschiedene Anwendungsfälle. timestamptzwird konvertiert timestampund umgekehrt.

So geben Sie die UTC ein timestamptz 2012-03-05 17:00:00+0:

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

... was entspricht:

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

So zeigen Sie denselben Zeitpunkt wie EST timestamp(Eastern Standard Time) an:

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

Das stimmt AT TIME ZONE 'UTC' zweimal . Der erste interpretiert den timestampWert als (gegebenen) UTC-Zeitstempel, der den Typ zurückgibt timestamptz. Die zweite konvertiert die timestamptzin die timestampin der angegebenen Zeitzone 'EST' - was für eine Uhr in der Zeitzone EST zu diesem eindeutigen Zeitpunkt anzeigt.

Beispiele

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);

Gibt 8 (oder 9) identische Zeilen mit einer Zeitstempelspalte zurück, die denselben UTC-Zeitstempel enthält 2012-03-05 17:00:00. Die 9. Reihe funktioniert zufällig in meiner Zeitzone, ist aber eine böse Falle. Siehe unten.

① Zeilen 6 bis 8 mit Zeitzonennamen und Zeitzone Abkürzung für Hawaii Zeit unterliegen DST (Sommerzeit) und können sich unterscheiden, wenn auch nicht gerade. Ein Zeitzonenname wie 'US/Hawaii'kennt die Sommerzeitregeln und alle historischen Verschiebungen automatisch, während eine Abkürzung wie HSTnur ein dummer Code für einen festen Versatz ist. Möglicherweise müssen Sie eine andere Abkürzung für Sommer / Standardzeit anhängen. Der Name interpretiert jeden Zeitstempel in der angegebenen Zeitzone korrekt . Eine Abkürzung ist billig, muss aber für den angegebenen Zeitstempel die richtige sein:

Die Sommerzeit gehört nicht zu den besten Ideen, die die Menschheit jemals entwickelt hat.

② Zeile 9, die als geladene Fußwaffe markiert ist , funktioniert für mich , aber nur durch Zufall. Wenn Sie explizit ein Literal in umwandeln, timestamp [without time zone]wird jeder Zeitzonenversatz ignoriert ! Es wird nur der bloße Zeitstempel verwendet. Der Wert wird dann timestamptzim Beispiel automatisch gezwungen, dem Spaltentyp zu entsprechen . Für diesen Schritt wird die timezoneEinstellung der aktuellen Sitzung angenommen, die +1in meinem Fall dieselbe Zeitzone ist (Europa / Wien). Aber wahrscheinlich nicht in Ihrem Fall - was zu einem anderen Wert führt. Kurz gesagt: Wirf keine timestamptzLiterale in timestampoder du verlierst den Zeitzonenversatz.

Deine Fragen

Der Benutzer speichert eine Uhrzeit, z. B. 17. März 2012, 19 Uhr. Ich möchte nicht, dass Zeitzonen-Konvertierungen oder die Zeitzone gespeichert werden.

Die Zeitzone selbst wird niemals gespeichert. Verwenden Sie eine der oben genannten Methoden, um einen UTC-Zeitstempel einzugeben.

Ich verwende nur die vom Benutzer angegebene Zeitzone, um Datensätze "vor" oder "nach" der aktuellen Zeit in der lokalen Zeitzone des Benutzers abzurufen.

Sie können eine Abfrage für alle Clients in verschiedenen Zeitzonen verwenden.
Für die absolute globale Zeit:

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

Für die Zeit nach der lokalen Uhr:

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

Noch nicht müde von Hintergrundinformationen? Das Handbuch enthält mehr.

Erwin Brandstetter
quelle
2
Kleinere Details, aber ich denke, Zeitstempel werden intern als Anzahl der Mikrosekunden seit dem 01.01.2000 gespeichert - siehe Abschnitt Datums- / Uhrzeit-Datentyp im Handbuch. Meine eigenen Inspektionen der Quelle scheinen dies zu bestätigen. Seltsam, einen anderen Ursprung für die Epoche zu verwenden!
Harmic
2
@harmic Wie für verschiedene Epochen ... Eigentlich nicht so seltsam. Diese Wikipedia-Seite listet zwei Dutzend Epochen auf, die von verschiedenen Computersystemen verwendet werden. Die Unix-Epoche ist zwar weit verbreitet, aber nicht die einzige.
Basil Bourque
4
@ErwinBrandstetter Dies ist eine großartige Antwort, bis auf einen schwerwiegenden Fehler. Wie harmic kommentiert, ist Postgres nicht Unix Zeit. Laut Dokument : (a) Die Epoche ist 2001-01-01 und nicht Unix '1970-01-01, und (b) Während die Unix-Zeit eine Auflösung von ganzen Sekunden hat, behält Postgres Bruchteile von Sekunden bei. Die Anzahl der gebrochenen Ziffern hängt von der Option für die Kompilierungszeit ab: 0 bis 6, wenn ein 8-Byte-Ganzzahlspeicher (Standard) verwendet wird, oder 0 bis 10, wenn ein Gleitkommaspeicher (veraltet) verwendet wird.
Basil Bourque
2
@BasilBourque: Ich bin mir dieses unglücklichen Fehlers bewusst. Wenn Sie nichts dagegen haben, können Sie es gerne bearbeiten. Ich habe einige Ihrer Antworten in der Vergangenheit gesehen und Sie sind gut darin. Eine weitere Bearbeitung von mir würde dies in das Community-Wiki zwingen - im Laufe der Zeit habe ich große Anstrengungen (und Änderungen) unternommen, um es klar und umfassend zu machen.
Erwin Brandstetter
2
KORREKTUR: In meinem früheren Kommentar habe ich die Postgres-Epoche fälschlicherweise als 2001 zitiert. Eigentlich ist es 2000 .
Basil Bourque
1

Wenn Sie standardmäßig in UTC handeln möchten:

In config/application.rbhinzufügen:

config.time_zone = 'UTC'

Wenn Sie dann den aktuellen Zeitzonennamen des Benutzers speichern current_user.timezone, können Sie sagen.

post.created_at.in_time_zone(current_user.timezone)

current_user.timezonesollte ein gültiger Zeitzonenname sein, sonst erhalten Sie ArgumentError: Invalid Timezone, siehe vollständige Liste .

Dorian
quelle