Champs datetime MySQL et heure d'été - comment faire référence à l'heure «supplémentaire»?

88

J'utilise le fuseau horaire Amérique / New York. À l'automne, nous «retombons» d'une heure - effectivement «gagnant» une heure à 2 heures du matin. Au point de transition, ce qui suit se produit:

il est 01:59:00 -04: 00
puis 1 minute plus tard, il devient:
01:00:00 -05: 00

Donc, si vous dites simplement "1h30 du matin", il est ambigu de savoir si vous faites référence à la première fois à 1h30 ou à la seconde. J'essaie d'enregistrer les données de planification dans une base de données MySQL et je ne peux pas déterminer comment enregistrer correctement les temps.

Voici le problème:
"2009-11-01 00:30:00" est stocké en interne sous le nom 2009-11-01 00:30:00 -04: 00
"2009-11-01 01:30:00" est stocké en interne sous 01/11/2009 01:30:00 -05: 00

C'est bien et assez attendu. Mais comment puis-je enregistrer quoi que ce soit à 01:30:00 -04: 00 ? La documentation ne montre aucun support pour spécifier le décalage et, par conséquent, lorsque j'ai essayé de spécifier le décalage, il a été dûment ignoré.

Les seules solutions auxquelles j'ai pensé impliquent de configurer le serveur sur un fuseau horaire qui n'utilise pas l'heure d'été et de faire les transformations nécessaires dans mes scripts (j'utilise PHP pour cela). Mais cela ne semble pas nécessaire.

Merci beaucoup pour vos suggestions.

Aaron
la source
Je ne connais pas assez MySQL ou PHP pour former une réponse cohérente, mais je parie que cela a quelque chose à voir avec la conversion vers et depuis UTC.
Mark Ransom
2
En interne, ils sont tous stockés au format UTC, non?
Eli
4
J'ai trouvé web.ivy.net/~carton/rant/MySQL-timezones.txt une lecture intéressante sur le sujet.
micahwittman
Bon lien, micahwittman - très utile.
Aaron
grandes questions. un problème commun.
Vardumper

Réponses:

47

Les types de date de MySQL sont, franchement, cassés et ne peuvent pas stocker correctement toutes les heures à moins que votre système ne soit réglé sur un fuseau horaire à décalage constant, comme UTC ou GMT-5. (J'utilise MySQL 5.0.45)

En effet, vous ne pouvez enregistrer aucune heure pendant l'heure précédant la fin de l'heure d'été . Quelle que soit la manière dont vous saisissez les dates, chaque fonction de date traitera ces heures comme si elles se situaient dans l'heure suivant le changement.

Le fuseau horaire de mon système est America/New_York. Essayons de stocker 1257051600 (dim, 01 nov 2009 06:00:00 +0100).

Voici en utilisant la syntaxe propriétaire INTERVAL:

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

Même FROM_UNIXTIME()ne retournera pas l'heure exacte.

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

Curieusement, DATETIME sera toujours stocker et retour (sous forme de chaîne uniquement!) Fois dans le « perdu » l' heure où commence l' heure d' été (par exemple 2009-03-08 02:59:59). Mais utiliser ces dates dans n'importe quelle fonction MySQL est risqué:

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

À retenir: Si vous avez besoin de stocker et de récupérer à chaque fois dans l'année, vous avez quelques options indésirables:

  1. Réglez le fuseau horaire du système sur GMT + un décalage constant. Par exemple UTC
  2. Stocker les dates sous forme de INT (comme Aaron l'a découvert, TIMESTAMP n'est même pas fiable)

  3. Prétendre que le type DATETIME a un fuseau horaire à décalage constant. Par exemple, si vous y êtes America/New_York, convertissez votre date en GMT-5 en dehors de MySQL , puis stockez-la en DATETIME (cela s'avère essentiel: voir la réponse d'Aaron). Ensuite, vous devez faire très attention en utilisant les fonctions date / heure de MySQL, car certaines supposent que vos valeurs sont celles du fuseau horaire du système, d'autres (en particulier les fonctions arithmétiques de l'heure) sont "indépendantes du fuseau horaire" (elles peuvent se comporter comme si les heures étaient UTC).

Aaron et moi soupçonnons que les colonnes TIMESTAMP générées automatiquement sont également cassées. Les deux 2009-11-01 01:30 -0400et 2009-11-01 01:30 -0500seront stockés comme ambigus 2009-11-01 01:30.

Steve Clay
la source
Merci pour toute votre aide sur ce mrclay. Vous avez décrit la situation ici très précisément.
Aaron
Il semble que l'option 3 est en fait plus sûre pour l'arithmétique du temps car (il semble que) les fonctions ont été implémentées avant l'ajout de la fonctionnalité DST. Par exemple, TIMEDIFF ('2009-11-01 02:30:00', '2009-11-01 00:30:00') renvoie 2:00, ce qui est correct pour UTC, mais en Amérique / New_York, les heures sont de 3 heures une part.
Steve Clay
1
-1: Vous avez commis l'erreur que les fonctions de date / heure de MySQL fonctionnent sur un type DATETIME, qui est indépendant du fuseau horaire. L'argument que vous passez à UNIX_TIMSTAMP est donc select '2009-11-01 00:00:00' + INTERVAL 3600 SECOND;qui est '2009-11-01 01:00:00'. UNIX_TIMESTAMP tente alors simplement de convertir ceci en UTC dans le contexte du fuseau horaire de la session - il n'essaye pas d'effectuer l'ajout dans le contexte des règles d'heure d'été de ce fuseau horaire.
kbro
@kbro OK, mais le problème demeure. Si la session tz est America/New_York, je ne vois aucun moyen de stocker 1257051600. Avez-vous?
Steve Clay
75

Je l'ai compris pour mes besoins. Je vais résumer ce que j'ai appris (désolé, ces notes sont verbeuses; elles sont autant pour ma future référence que pour toute autre chose).

Contrairement à ce que j'ai dit dans l'un de mes commentaires précédents, les champs DATETIME et TIMESTAMP font se comportent différemment. Les champs TIMESTAMP (comme l'indiquent les documents) prennent tout ce que vous leur envoyez au format "AAAA-MM-JJ hh: mm: ss" et le convertit de votre fuseau horaire actuel en heure UTC. L'inverse se produit de manière transparente chaque fois que vous récupérez les données. Les champs DATETIME n'effectuent pas cette conversion. Ils prennent tout ce que vous leur envoyez et le stockent directement.

Ni les types de champ DATETIME ni TIMESTAMP ne peuvent stocker avec précision les données dans un fuseau horaire qui respecte l'heure d'été . Si vous stockez "2009-11-01 01:30:00", les champs n'ont aucun moyen de distinguer la version de 1h30 que vous vouliez - la version -04: 00 ou -05: 00.

Ok, donc nous devons stocker nos données dans un fuseau horaire non DST (comme UTC). Les champs TIMESTAMP ne peuvent pas gérer ces données avec précision pour des raisons que je vais vous expliquer: si votre système est réglé sur un fuseau horaire DST, ce que vous mettez dans TIMESTAMP peut ne pas être ce que vous récupérez. Même si vous lui envoyez des données que vous avez déjà converties en UTC, il assumera toujours les données dans votre fuseau horaire local et effectuera une autre conversion en UTC. Cet aller-retour local-à-UTC-retour-local imposé par TIMESTAMP est avec perte lorsque votre fuseau horaire local observe l'heure d'été (depuis "2009-11-01 01:30:00" correspond à 2 heures possibles différentes).

Avec DATETIME, vous pouvez stocker vos données dans n'importe quel fuseau horaire de votre choix et être sûr que vous récupérerez tout ce que vous envoyez (vous ne serez pas obligé de participer aux conversions aller-retour avec perte que les champs TIMESTAMP vous imposent). La solution consiste donc à utiliser un champ DATETIME et avant d'enregistrer sur le terrain, convertir le fuseau horaire de votre système dans la zone non-DST dans laquelle vous souhaitez l'enregistrer (je pense que UTC est probablement la meilleure option). Cela vous permet de créer la logique de conversion dans votre langage de script afin de pouvoir enregistrer explicitement l'équivalent UTC de "2009-11-01 01:30:00 -04: 00" ou "" 2009-11-01 01:30: 00 -05: 00 ".

Une autre chose importante à noter est que les fonctions mathématiques date / heure de MySQL ne fonctionnent pas correctement autour des limites de l'heure d'été si vous stockez vos dates dans un TZ DST. Raison de plus pour économiser en UTC.

En un mot, je fais maintenant ceci:

Lors de la récupération des données de la base de données:

Interprétez explicitement les données de la base de données comme UTC en dehors de MySQL afin d'obtenir un horodatage Unix précis. J'utilise la fonction strtotime () de PHP ou sa classe DateTime pour cela. Cela ne peut pas être fait de manière fiable à l'intérieur de MySQL en utilisant les fonctions CONVERT_TZ () ou UNIX_TIMESTAMP () de MySQL car CONVERT_TZ affichera uniquement une valeur 'YYYY-MM-DD hh: mm: ss' qui souffre de problèmes d'ambiguïté, et UNIX_TIMESTAMP () suppose son l'entrée est dans le fuseau horaire du système, et non dans le fuseau horaire dans lequel les données ont été réellement stockées (UTC).

Lors du stockage des données dans la base de données:

Convertissez votre date en heure UTC précise que vous désirez en dehors de MySQL. Par exemple: avec la classe DateTime de PHP, vous pouvez spécifier "2009-11-01 1:30:00 EST" distinctement de "2009-11-01 1:30:00 EDT", puis le convertir en UTC et enregistrer l'heure UTC correcte dans votre champ DATETIME.

Phew. Merci beaucoup pour la contribution et l'aide de tous. Espérons que cela évite à quelqu'un d'autre des maux de tête sur la route.

BTW, je vois cela sur MySQL 5.0.22 et 5.0.27

Aaron
la source
13

Je pense que le lien de micahwittman a la meilleure solution pratique à ces limitations de MySQL: réglez le fuseau horaire de la session sur UTC lorsque vous vous connectez:

SET SESSION time_zone = '+0:00'

Ensuite, envoyez-lui simplement les horodatages Unix et tout devrait bien se passer.

Steve Clay
la source
Ce conseil fonctionne très bien. Le problème est résolu après avoir initié toutes les connexions dans mon pool avec l'instruction donnée.
snowindy
4

Ce fil m'a fait paniquer car nous utilisons des TIMESTAMPcolonnes avec On UPDATE CURRENT_TIMESTAMP(ie:) recordTimestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMPpour suivre les enregistrements modifiés et ETL vers un entrepôt de données.

Au cas où quelqu'un se demanderait, dans ce cas, TIMESTAMPse comporter correctement et vous pouvez différencier les deux dates similaires en convertissant l' TIMESTAMPhorodatage en unix:

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
la source
3

Mais comment puis-je enregistrer quoi que ce soit à 01:30:00 -04: 00?

Vous pouvez convertir en UTC comme:

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


Mieux encore, enregistrez les dates dans un champ TIMESTAMP . Cela est toujours stocké en UTC et UTC ne connaît pas l'heure d'été / d'hiver.

Vous pouvez convertir UTC en heure locale en utilisant CONVERT_TZ :

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

Où «+00: 00» est UTC, le fuseau horaire de, et «SYSTEM» est le fuseau horaire local du système d'exploitation sur lequel MySQL s'exécute.

Andomar
la source
Merci pour la réponse. Pour autant que je sache, malgré ce que disent les documents, les champs TIMESTAMP et Datetime se comportent de la même manière: ils stockent leurs données en UTC, mais ils s'attendent à ce que leur entrée soit en heure locale et ils les convertissent automatiquement en UTC - si je convertis en UTC d'abord, la base de données n'a aucune idée de ce que j'ai fait et elle ajoute 4 (ou 5, selon que nous sommes ou non l'heure d'été) à l'heure. Le problème demeure donc: comment spécifier 2009-11-01 01:30:00 -04: 00 comme entrée?
Aaron
Eh bien, j'ai découvert que la source de la plupart de ma confusion est le fait que la fonction UNIX_TIMESTAMP () interprète toujours son paramètre de date par rapport au fuseau horaire actuel, que vous tiriez ou non les données d'un champ TIMESTAMP ou DATETIME . Cela a du sens maintenant que j'y pense. Je mettrai à jour plus tard.
Aaron
2

Mysql résout intrinsèquement ce problème en utilisant la table time_zone_name de mysql db. Utilisez CONVERT_TZ pendant CRUD pour mettre à jour la date et l'heure sans vous soucier de l'heure d'été.

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
la source
1

Je travaillais sur la journalisation des comptes de visites de pages et l'affichage des comptes dans le graphique (en utilisant le plugin Flot jQuery). J'ai rempli le tableau avec des données de test et tout avait l'air bien, mais j'ai remarqué qu'à la fin du graphique, les points étaient un jour de congé selon les étiquettes sur l'axe des x. Après examen, j'ai remarqué que le nombre de vues pour le jour 2015-10-25 a été récupéré deux fois de la base de données et transmis à Flot, donc chaque jour après cette date a été déplacé d'un jour vers la droite.
Après avoir cherché un bogue dans mon code pendant un moment, j'ai réalisé que cette date correspond à celle de l'heure d'été. Puis je suis arrivé à cette page SO ...
... mais les solutions proposées étaient exagérées pour ce dont j'avais besoin ou elles avaient d'autres inconvénients. Je ne suis pas très inquiet de ne pas pouvoir distinguer les horodatages ambigus.J'ai juste besoin de compter et d'afficher les enregistrements par jour.

Tout d'abord, je récupère la plage de dates:

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

Ensuite, dans une boucle for, commençant par min_date, se terminant par max_date, par pas d'un jour ( 60*60*24), je récupère les décomptes:

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
}

Ma solution finale et rapide à mon problème était la suivante:

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

Je ne regarde donc pas la boucle en début de journée, mais deux heures plus tard . Le jour est toujours le même et je récupère toujours les décomptes corrects, car je demande explicitement à la base de données les enregistrements entre 00:00:00 et 23:59:59 du jour, quelle que soit l'heure réelle de l'horodatage. Et quand le temps passe d'une heure, je suis toujours dans le bon jour.

Remarque: je sais que c'est un fil de 5 ans, et je sais que ce n'est pas une réponse à la question des OP, mais cela pourrait aider des gens comme moi qui ont rencontré cette page à chercher une solution au problème que j'ai décrit.

Lukas
la source
Probablement pas pertinent par rapport à la question réelle, mais c'est horriblement inefficace, et personne ne devrait le copier! Au lieu de cela, émettez une seule requête telle que:
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";
Fait