web-dev-qa-db-fra.com

Champs date-heure et heure avancée MySQL - comment référencer l'heure "supplémentaire"

J'utilise le fuseau horaire Amérique/New York. À l’automne, nous "retombons" une heure - nous gagnons effectivement une heure à 2 heures du matin. Au point de transition, il se passe ce qui suit: 

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 de sauvegarder les données de planification dans une base de données MySQL et je ne peux pas déterminer comment enregistrer les temps correctement.

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

C'est bien et assez attendu. Mais comment puis-je sauvegarder quelque chose à 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 que j'ai envisagées impliquent de définir le serveur sur un fuseau horaire qui n'utilise pas l'heure d'été et d'effectuer les transformations nécessaires dans mes scripts (j'utilise PHP pour cela). Mais cela ne semble pas être nécessaire.

Merci beaucoup pour vos suggestions.

76
Aaron

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

En effet, vous ne pouvez pas mémoriser d’heure au cours de l’heure qui précède la fin de l’heure Quelle que soit la manière dont vous saisissez les dates, chaque fonction de date traitera ces heures comme si elles étaient pendant l'heure qui a suivi 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 conservera toujours et retournera (uniquement sous forme de chaîne!) Au cours de l'heure "perdue" au début de l'heure d'été (par exemple, 2009-03-08 02:59:59). Mais utiliser ces dates dans n'importe quelle fonction de 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

La livraison: Si vous avez besoin de stocker et de récupérer à chaque période de l'année, vous avez quelques options indésirables:

  1. Définissez le fuseau horaire du système sur GMT + un décalage constant. Par exemple. UTC
  2. Conservez les dates au format INT (comme Aaron l'a découvert, TIMESTAMP n'est même pas fiable)

  3. Imaginez que le type DATETIME ait un fuseau horaire décalé constant. Par exemple. Si vous êtes dans America/New_York, convertissez votre date au format GMT-5 en dehors de MySQL , puis enregistrez-la en tant que DATETIME (le résultat est: être 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 temps) sont "agnostiques selon le fuseau horaire" (elles peuvent se comporter comme si les heures étaient en UTC).

Aaron et moi soupçonnons que les colonnes TIMESTAMP auto-générées sont également cassées. 2009-11-01 01:30 -0400 et 2009-11-01 01:30 -0500 seront tous deux stockés sous le nom ambigu 2009-11-01 01:30.

42
Steve Clay

Je l'ai compris pour mes besoins. Je vais résumer ce que j'ai appris (désolé, ces notes sont verbeuses; elles sont aussi utiles à mon renvoi futur).

Contrairement à ce que j'ai dit dans l'un de mes précédents commentaires, les champs DATETIME et TIMESTAMP do se comportent différemment. Les champs TIMESTAMP (comme indiqué par la documentation) prennent tout ce que vous leur envoyez au format "AAAA-MM-JJ hh: mm: ss" et le convertissent 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 ne font pas cette conversion. Ils prennent ce que vous leur envoyez et stockez-le directement.

Ni les types de champ DATETIME ni TIMESTAMP ne peuvent stocker avec précision des données dans un fuseau horaire qui observe DST . Si vous enregistrez "2009-11-01 01:30:00", les champs ne permettent pas de distinguer la version de 1h30 que vous souhaitiez - la version -04: 00 ou -05: 00.

Ok, nous devons donc stocker nos données dans un fuseau horaire autre que DST (comme UTC). Les champs TIMESTAMP ne peuvent pas traiter ces données avec précision pour des raisons que je vais expliquer: si votre système est configuré sur un fuseau horaire DST, ce que vous avez mis 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 au format UTC, les données seront toujours dans votre fuseau horaire local et seront encore converties au format UTC. Ce transfert aller-retour de local à local UTC imposé par TIMESTAMP entraîne des pertes lorsque votre fuseau horaire local observe l'heure d'été (depuis le "2009-11-01 01:30:00" de correspond à deux heures possibles différentes).

Avec DATETIME, vous pouvez stocker vos données dans n’importe quel fuseau horaire et être sûr que vous recevrez ce que vous avez envoyé (les conversions aller-retour avec perte que les champs TIMESTAMP vous imposent ne vous sont pas imposées). La solution consiste donc à utiliser un champ DATETIME et avant de sauvegarder sur le terrain convertir le fuseau horaire de votre système dans le fuseau horaire autre que DST (je pense que le format UTC est probablement la meilleure option). Cela vous permet de construire 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 enregistrer 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 au format 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. Il ne peut pas être effectué de manière fiable dans MySQL avec les fonctions CONVERT_TZ () ou UNIX_TIMESTAMP () de MySQL, car CONVERT_TZ ne produira qu'un "AAAA-MM-JJ hh: mm: ss" qui souffre de problèmes d'ambiguïté, et UNIX_TIMESTAMP () assume sa l’entrée se trouve dans le fuseau horaire du système et non dans le fuseau horaire dans lequel les données ont été réellement stockées (au format 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 convertissez-le au format UTC et enregistrez l'heure UTC correcte. à 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 ceci sur MySQL 5.0.22 et 5.0.27

62
Aaron

Je pense que link de micahwittman offre 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, il vous suffit d’envoyer des timestamps Unix et tout devrait bien se passer.

10
Steve Clay

Ce fil de discussion m'a fait paniquer car nous utilisons des colonnes TIMESTAMP avec On UPDATE CURRENT_TIMESTAMP (c'est-à-dire: recordTimestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) pour suivre les enregistrements modifiés et ETL dans un entrepôt de données.

Au cas où quelqu'un se demanderait, dans ce cas, TIMESTAMP se comportera correctement et vous pourrez différencier les deux dates similaires en convertissant la TIMESTAMP en timestamp 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
3
Manuel Darveau

Mais comment puis-je sauvegarder quelque chose à 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 . C'est toujours stocké en UTC, et UTC ne sait pas l'heure d'été/hiver.

Vous pouvez convertir UTC en heure locale à l’aide de CONVERT_TZ :

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

Où "+00: 00" est l'heure UTC, le fuseau horaire de départ et "SYSTEM" est le fuseau horaire local du système d'exploitation sur lequel tourne MySQL.

3
Andomar

Mysql résout ce problème de manière inhérente en utilisant la table time_zone_name de la base de données mysql. Utilisez CONVERT_TZ en mode 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;
2
Arpan Jain

Je travaillais sur la journalisation des comptes de visites de pages et l’affichage graphique du nombre (en utilisant le plugin Flot jQuery). J'ai rempli le tableau avec les données de test et tout semblait aller 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 constaté que le nombre de vues du jour 2015-10-25 avait été extrait deux fois de la base de données et transmis à Flot. Ainsi, chaque jour suivant cette date était décalé 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 à l'heure à laquelle l'heure d'été a lieu. Puis je suis arrivé à cette page SO ...
... mais les solutions suggérées étaient excessives pour ce dont j'avais besoin ou elles présentaient d'autres inconvénients. _ {Je ne m'inquiète pas beaucoup de ne pas être en mesure de faire la distinction entre des horodatages ambigus.J'ai juste besoin de compter et d'afficher des 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

Puis, dans une boucle for commençant par min_date et se terminant par max_date, par incréments d'un jour (60*60*24), je récupère les 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
}

Mon solution finale et rapide _ à mon problème était le suivant:

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

Donc, je suis je ne regarde pas la boucle au début de la 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 des enregistrements entre 00:00:00 et 23:59:59 du jour, quelle que soit l'heure de l'horodatage. Et quand le temps passe d'une heure, je suis toujours dans le bon jour.

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

0
Lukas