MySQL-Datums- und Uhrzeitfelder und Sommerzeit - wie verweise ich auf die „zusätzliche“ Stunde?

88

Ich benutze die Zeitzone Amerika / New York. Im Herbst "fallen" wir eine Stunde zurück - und "gewinnen" effektiv eine Stunde um 2 Uhr morgens. Am Übergangspunkt geschieht Folgendes:

Es ist 01:59:00 -04: 00 Uhr
und 1 Minute später wird es:
01:00:00 -05: 00 Uhr

Wenn Sie also einfach "1:30 Uhr" sagen, ist es nicht eindeutig, ob Sie sich auf das erste Mal beziehen, wenn 1:30 herumrollt, oder auf das zweite Mal. Ich versuche, Planungsdaten in einer MySQL-Datenbank zu speichern, und kann nicht bestimmen, wie die Zeiten richtig gespeichert werden sollen.

Hier ist das Problem:
"2009-11-01 00:30:00" wird intern als 2009-11-01 00:30:00 -04: 00
gespeichert. "2009-11-01 01:30:00" wird intern als gespeichert 2009-11-01 01:30:00 -05: 00

Das ist in Ordnung und ziemlich zu erwarten. Aber wie speichere ich etwas bis 01:30:00 -04: 00 ? Die Dokumentation enthält keine Unterstützung für die Angabe des Offsets. Wenn ich also versucht habe, den Offset anzugeben, wurde dieser ordnungsgemäß ignoriert.

Die einzigen Lösungen, an die ich gedacht habe, bestehen darin, den Server auf eine Zeitzone einzustellen, in der keine Sommerzeit verwendet wird, und die erforderlichen Transformationen in meinen Skripten durchzuführen (ich verwende dafür PHP). Aber das scheint nicht notwendig zu sein.

Vielen Dank für Anregungen.

Aaron
quelle
Ich weiß nicht genug über MySQL oder PHP, um eine kohärente Antwort zu finden, aber ich wette, es hat etwas mit der Konvertierung von und nach UTC zu tun.
Mark Ransom
2
Intern werden sie alle als UTC gespeichert, oder?
Eli
4
Ich fand web.ivy.net/~carton/rant/MySQL-timezones.txt eine interessante Lektüre zu diesem Thema.
Micahwittman
Guter Link, Micahwittman - sehr hilfreich.
Aaron
tolle Fragen. ein häufiges Problem.
Vardumper

Antworten:

47

Die Datumstypen von MySQL sind offen gesagt fehlerhaft und können nicht alle Zeiten korrekt gespeichert werden, es sei denn, Ihr System ist auf eine konstante Offset-Zeitzone wie UTC oder GMT-5 eingestellt. (Ich benutze MySQL 5.0.45)

Dies liegt daran, dass Sie während der Stunde vor Ende der Sommerzeit keine Zeit speichern können . Unabhängig davon, wie Sie Datumsangaben eingeben, behandelt jede Datumsfunktion diese Uhrzeiten so, als ob sie sich in der Stunde nach dem Wechsel befinden.

Die Zeitzone meines Systems ist America/New_York. Versuchen wir, 1257051600 (So, 01. November 2009, 06:00:00 +0100) zu speichern.

Hier wird die proprietäre INTERVAL-Syntax verwendet:

SELECT UNIX_TIMESTAMP('2009-11-01 00:00:00' + INTERVAL 3599 SECOND); # 1257051599
SELECT UNIX_TIMESTAMP('2009-11-01 00:00:00' + INTERVAL 3600 SECOND); # 1257055200

SELECT UNIX_TIMESTAMP('2009-11-01 01:00:00' - INTERVAL 1 SECOND); # 1257051599
SELECT UNIX_TIMESTAMP('2009-11-01 01:00:00' - INTERVAL 0 SECOND); # 1257055200

Auch FROM_UNIXTIME()wird nicht die genaue Zeit zurückgeben.

SELECT UNIX_TIMESTAMP(FROM_UNIXTIME(1257051599)); # 1257051599
SELECT UNIX_TIMESTAMP(FROM_UNIXTIME(1257051600)); # 1257055200

Seltsamerweise speichert DATETIME immer noch (nur in Zeichenfolgenform!) Zeiten innerhalb der "verlorenen" Stunde, in der die Sommerzeit beginnt (z 2009-03-08 02:59:59. B. ). Die Verwendung dieser Daten in einer MySQL-Funktion ist jedoch riskant:

SELECT UNIX_TIMESTAMP('2009-03-08 01:59:59'); # 1236495599
SELECT UNIX_TIMESTAMP('2009-03-08 02:00:00'); # 1236495600
# ...
SELECT UNIX_TIMESTAMP('2009-03-08 02:59:59'); # 1236495600
SELECT UNIX_TIMESTAMP('2009-03-08 03:00:00'); # 1236495600

Das Mitnehmen: Wenn Sie jedes Mal im Jahr speichern und abrufen müssen , haben Sie einige unerwünschte Optionen:

  1. Stellen Sie die Systemzeitzone auf GMT + einen konstanten Offset ein. ZB UTC
  2. Speichern Sie Daten als INTs (wie Aaron herausfand, ist TIMESTAMP nicht einmal zuverlässig)

  3. Stellen Sie sich vor, der Typ DATETIME hat eine konstante Versatzzeitzone. Wenn Sie beispielsweise in sind America/New_York, konvertieren Sie Ihr Datum außerhalb von MySQL in GMT-5 und speichern Sie es dann als DATETIME (dies stellt sich als wesentlich heraus: siehe Aarons Antwort). Dann müssen Sie bei der Verwendung der Datums- / Zeitfunktionen von MySQL sehr vorsichtig sein, da einige davon ausgehen, dass Ihre Werte der Systemzeitzone entsprechen, andere (insbesondere Zeitarithmetikfunktionen) "zeitzonenunabhängig" sind (sie verhalten sich möglicherweise so, als wären die Zeiten UTC).

Aaron und ich vermuten, dass die automatisch generierenden TIMESTAMP-Spalten ebenfalls fehlerhaft sind. Beide 2009-11-01 01:30 -0400und 2009-11-01 01:30 -0500werden als mehrdeutig gespeichert 2009-11-01 01:30.

Steve Clay
quelle
Vielen Dank für all Ihre Hilfe bei diesem mrclay. Sie haben die Situation hier sehr genau umrissen.
Aaron
Es scheint, dass Option 3 für die Zeitarithmetik tatsächlich sicherer ist, da (anscheinend) die Funktionen implementiert wurden, bevor die Sommerzeitfunktionalität hinzugefügt wurde. Beispielsweise gibt TIMEDIFF ('2009-11-01 02:30:00', '2009-11-01 00:30:00') 2:00 zurück, was für UTC korrekt ist, aber in Amerika / New_York betragen die Zeiten 3 Stunden ein Teil.
Steve Clay
1
-1: Sie haben den Fehler gemacht, dass MySQL-Datums- / Zeitfunktionen mit einem DATETIME-Typ arbeiten, der zeitzonenunabhängig ist. Das Argument, das Sie an UNIX_TIMSTAMP übergeben, ist daher select '2009-11-01 00:00:00' + INTERVAL 3600 SECOND;das folgende '2009-11-01 01:00:00'. UNIX_TIMESTAMP versucht dann einfach, dies im Kontext der Sitzungszeitzone in UTC umzuwandeln - es wird nicht versucht, das Hinzufügen im Kontext der Sommerzeitregeln dieser Zeitzone durchzuführen.
Kbro
@kbro OK, aber das Problem bleibt bestehen. Wenn die Sitzung tz ist America/New_York, sehe ich keine Möglichkeit, 1257051600 zu speichern.
Steve Clay
75

Ich habe es für meine Zwecke herausgefunden. Ich werde zusammenfassen, was ich gelernt habe (Entschuldigung, diese Notizen sind ausführlich; sie sind für meine zukünftige Überweisung genauso wichtig wie alles andere).

Im Gegensatz zu dem, was ich in einem meiner vorherigen Kommentare gesagt habe, verhalten sich die Felder DATETIME und TIMESTAMP unterschiedlich. TIMESTAMP-Felder (wie in den Dokumenten angegeben) nehmen alles, was Sie senden, im Format "JJJJ-MM-TT hh: mm: ss" und konvertieren es von Ihrer aktuellen Zeitzone in die UTC-Zeit. Das Gegenteil geschieht transparent, wenn Sie die Daten abrufen. DATETIME-Felder führen diese Konvertierung nicht durch. Sie nehmen alles, was Sie ihnen senden, und speichern es direkt.

Weder die Feldtypen DATETIME noch TIMESTAMP können Daten in einer Zeitzone genau speichern, in der die Sommerzeit eingehalten wird . Wenn Sie "2009-11-01 01:30:00" speichern, können die Felder nicht unterscheiden, welche Version von 1:30 Uhr Sie möchten - die Version -04: 00 oder -05: 00.

Ok, wir müssen unsere Daten in einer Zeitzone ohne Sommerzeit (z. B. UTC) speichern. TIMESTAMP-Felder können diese Daten aus Gründen, die ich erläutern werde, nicht genau verarbeiten: Wenn Ihr System auf eine DST-Zeitzone eingestellt ist, ist das, was Sie in TIMESTAMP eingeben, möglicherweise nicht das, was Sie zurückbekommen. Selbst wenn Sie Daten senden, die Sie bereits in UTC konvertiert haben, werden die Daten in Ihrer lokalen Zeitzone übernommen und eine weitere Konvertierung in UTC durchgeführt. Diese von TIMESTAMP erzwungene Hin- und Rückfahrt von lokal zu UTC zurück zu lokal ist verlustbehaftet, wenn Ihre lokale Zeitzone die Sommerzeit einhält (seit "2009-11-01 01:30:00" zwei verschiedenen möglichen Zeiten zugeordnet sind).

Mit DATETIME können Sie Ihre Daten in jeder gewünschten Zeitzone speichern und sicher sein, dass Sie alles zurückerhalten, was Sie senden (Sie werden nicht in die verlustbehafteten Roundtrip-Konvertierungen gezwungen, die Ihnen TIMESTAMP-Felder auferlegen). Die Lösung besteht also darin, ein DATETIME-Feld zu verwenden und vor dem Speichern in das Feld von Ihrer Systemzeitzone in eine Nicht-DST-Zone zu konvertieren, in der Sie es speichern möchten (ich denke, UTC ist wahrscheinlich die beste Option). Auf diese Weise können Sie die Konvertierungslogik in Ihre Skriptsprache integrieren, sodass Sie das UTC-Äquivalent von "2009-11-01 01:30:00 -04: 00" oder "" 2009-11-01 01:30: 00 -05: 00 ".

Ein weiterer wichtiger Punkt ist, dass die mathematischen Funktionen von MySQL für Datum und Uhrzeit nicht richtig um die Sommerzeitgrenzen herum funktionieren, wenn Sie Ihre Daten in einer Sommerzeit-TZ speichern. Umso mehr Grund, in UTC zu sparen.

Kurz gesagt, ich mache jetzt Folgendes:

Beim Abrufen der Daten aus der Datenbank:

Interpretieren Sie die Daten aus der Datenbank explizit als UTC außerhalb von MySQL, um einen genauen Unix-Zeitstempel zu erhalten. Ich benutze dafür die Funktion strtotime () von PHP oder die DateTime-Klasse. Dies kann in MySQL mit den Funktionen CONVERT_TZ () oder UNIX_TIMESTAMP () von MySQL nicht zuverlässig durchgeführt werden, da CONVERT_TZ nur einen Wert 'JJJJ-MM-TT hh: mm: ss' ausgibt, der unter Mehrdeutigkeitsproblemen leidet, und UNIX_TIMESTAMP () seinen Wert annimmt Die Eingabe erfolgt in der Systemzeitzone, nicht in der Zeitzone, in der die Daten tatsächlich gespeichert wurden (UTC).

Beim Speichern der Daten in der Datenbank:

Konvertieren Sie Ihr Datum in die genaue UTC-Zeit, die Sie außerhalb von MySQL wünschen. Beispiel: Mit der DateTime-Klasse von PHP können Sie "2009-11-01 1:30:00 EST" deutlich von "2009-11-01 1:30:00 EDT" angeben, diese dann in UTC konvertieren und die richtige UTC-Zeit speichern zu Ihrem DATETIME-Feld.

Puh. Vielen Dank für alle Beiträge und Hilfe. Hoffentlich erspart dies jemand anderem später Kopfschmerzen.

Übrigens sehe ich das auf MySQL 5.0.22 und 5.0.27

Aaron
quelle
13

Ich denke, der Link von micahwittman bietet die beste praktische Lösung für diese MySQL-Einschränkungen: Setzen Sie die Sitzungszeitzone auf UTC, wenn Sie eine Verbindung herstellen:

SET SESSION time_zone = '+0:00'

Dann senden Sie ihm einfach Unix-Zeitstempel und alles sollte in Ordnung sein.

Steve Clay
quelle
Dieser Rat funktioniert gut. Das Problem ist gelöst, nachdem ich alle Verbindungen in meinem Pool mit der angegebenen Anweisung initiiert habe.
Snowindy
4

Dieser Thread hat mich verrückt gemacht, da wir TIMESTAMPSpalten mit On UPDATE CURRENT_TIMESTAMP(dh recordTimestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP:) verwenden, um geänderte Datensätze und ETL in einem Datawarehouse zu verfolgen.

Falls sich jemand wundert, TIMESTAMPverhalten Sie sich in diesem Fall korrekt und Sie können zwischen den beiden ähnlichen Daten unterscheiden, indem Sie den TIMESTAMPZeitstempel in Unix konvertieren :

select TestFact.*, UNIX_TIMESTAMP(recordTimestamp) from TestFact;

id  recordTimestamp         UNIX_TIMESTAMP(recordTimestamp)
1   2012-11-04 01:00:10.0   1352005210
2   2012-11-04 01:00:10.0   1352008810
Manuel Darveau
quelle
3

Aber wie speichere ich etwas bis 01:30:00 -04: 00?

Sie können wie folgt in UTC konvertieren:

SELECT CONVERT_TZ('2009-11-29 01:30:00','-04:00','+00:00');


Noch besser, speichern Sie die Daten als TIMESTAMP- Feld. Das wird immer in UTC gespeichert, und UTC weiß nichts über Sommer- / Winterzeit.

Sie können mit CONVERT_TZ von UTC in Ortszeit konvertieren :

SELECT CONVERT_TZ(UTC_TIMESTAMP(),'+00:00','SYSTEM');

Dabei ist '+00: 00' UTC, die Zeitzone from, und 'SYSTEM' die lokale Zeitzone des Betriebssystems, in dem MySQL ausgeführt wird.

Andomar
quelle
Danke für die Antwort. Wie ich am besten beurteilen kann, verhalten sich die Felder TIMESTAMP und Datetime identisch: Sie speichern ihre Daten in UTC, erwarten jedoch, dass ihre Eingabe in Ortszeit erfolgt, und konvertieren sie automatisch in UTC - wenn ich in konvertiere UTC Zuerst hat die Datenbank keine Ahnung, dass ich das getan habe, und sie verlängert die Zeit um 4 (oder 5, je nachdem, ob wir Sommerzeit sind oder nicht). Das Problem bleibt also bestehen: Wie gebe ich 2009-11-01 01:30:00 -04: 00 als Eingabe an?
Aaron
Nun, ich habe festgestellt, dass der größte Teil meiner Verwirrung darin besteht, dass die Funktion UNIX_TIMESTAMP () ihren Datumsparameter immer relativ zur aktuellen Zeitzone interpretiert, unabhängig davon, ob Sie die Daten aus einem TIMESTAMP- oder einem DATETIME-Feld abrufen oder nicht . Das macht jetzt Sinn, wo ich darüber nachdenke. Ich werde später mehr aktualisieren.
Aaron
2

MySQL löst dieses Problem von Natur aus mithilfe der Tabelle time_zone_name aus der MySQL-Datenbank. Verwenden Sie CONVERT_TZ während CRUD, um die Datums- und Uhrzeitangabe zu aktualisieren, ohne sich um die Sommerzeit kümmern zu müssen.

SELECT
  CONVERT_TZ('2019-04-01 00:00:00','Europe/London','UTC') AS time1,
  CONVERT_TZ('2019-03-01 00:00:00','Europe/London','UTC') AS time2;
Arpan Jain
quelle
1

Ich habe daran gearbeitet, die Anzahl der Besuche von Seiten zu protokollieren und die Anzahl in der Grafik anzuzeigen (mithilfe des Flot jQuery-Plugins). Ich füllte die Tabelle mit Testdaten und alles sah gut aus, aber ich bemerkte, dass am Ende des Diagramms die Punkte laut Beschriftungen auf der x-Achse einen Tag frei waren. Nach der Prüfung stellte ich fest, dass die Anzahl der Aufrufe für den Tag 2015-10-25 zweimal aus der Datenbank abgerufen und an Flot übergeben wurde, sodass jeder Tag nach diesem Datum um einen Tag nach rechts verschoben wurde.
Nachdem ich eine Weile nach einem Fehler in meinem Code gesucht hatte, wurde mir klar, dass an diesem Datum die Sommerzeit stattfindet. Dann kam ich zu dieser SO-Seite ...
... aber die vorgeschlagenen Lösungen waren ein Overkill für das, was ich brauchte, oder sie hatten andere Nachteile. Ich bin nicht sehr besorgt darüber, nicht zwischen mehrdeutigen Zeitstempeln unterscheiden zu können. Ich muss nur die Aufzeichnungen pro Tag zählen und anzeigen.

Zuerst rufe ich den Datumsbereich ab:

SELECT 
    DATE(MIN(created_timestamp)) AS min_date, 
    DATE(MAX(created_timestamp)) AS max_date 
FROM page_display_log
WHERE item_id = :item_id

Dann rufe ich in einer for-Schleife, beginnend mit min_date, endend mit max_dateSchritt eines Tages ( 60*60*24), die Zählungen ab:

for( $day = $min_date_timestamp; $day <= $max_date_timestamp; $day += 60 * 60 * 24 ) {
    $query = "
        SELECT COUNT(*) AS count_per_day
        FROM page_display_log
        WHERE 
            item_id = :item_id AND
            ( 
                created_timestamp BETWEEN 
                '" . date( "Y-m-d 00:00:00", $day ) . "' AND
                '" . date( "Y-m-d 23:59:59", $day ) . "'
            )
    ";
    //execute query and do stuff with the result
}

Meine endgültige und schnelle Lösung für mein Problem war folgende:

$min_date_timestamp += 60 * 60 * 2; // To avoid DST problems
for( $day = $min_date_timestamp; $day <= $max_da.....

Ich starre also nicht am Anfang des Tages, sondern zwei Stunden später auf die Schleife . Der Tag ist immer noch derselbe und ich rufe immer noch die richtigen Zählwerte ab, da ich die Datenbank explizit zwischen 00:00:00 und 23:59:59 des Tages nach Datensätzen frage, unabhängig von der tatsächlichen Zeit des Zeitstempels. Und wenn die Zeit um eine Stunde springt, bin ich immer noch am richtigen Tag.

Hinweis: Ich weiß, dass dies ein 5 Jahre alter Thread ist, und ich weiß, dass dies keine Antwort auf die Frage des OP ist, aber es könnte Leuten wie mir helfen, die auf diese Seite gestoßen sind, nach einer Lösung für das von mir beschriebene Problem zu suchen.

Lukas
quelle
Wahrscheinlich nicht relevant für die eigentliche Frage, aber das ist schrecklich ineffizient, und niemand sollte es kopieren! Geben Sie stattdessen eine einzelne Abfrage aus, z. B.:
Doin
"SELECT CAST(created_timestamp AS date) day,COUNT(*) WHERE item_id=:item_id AND (created_timestamp BETWEEN '".date("Y-m-d 00:00:00", $min_date_timestamp)."' AND '".date("Y-m-d 23:59:59", $max_date_timestamp)."') GROUP BY day ORDER BY day";
Doin