web-dev-qa-db-fra.com

Meilleures pratiques pour enregistrer les informations de date / heure et de fuseau horaire dans la base de données lorsque les données dépendent de date / heure

Il y avait pas mal de questions sur la sauvegarde des informations de date/heure et de fuseaux horaires dans la base de données, mais davantage au niveau global. Ici, je voudrais aborder un cas spécifique.

Spécifications du système

  • Nous avons une base de données système de commandes
  • Il s’agit d’un système multi-locataire dans lequel les locataires peuvent utiliser un fuseau horaire arbitraire (il s’agit d’un fuseau horaire arbitraire mais unique par locataire, enregistré une fois dans la table des locataires et ne change jamais).

La règle métier doit être couverte dans la base de données

  • Lorsque le locataire passe une commande dans le système, le numéro de commande est calculé en fonction de la date/heure locale (ce n'est pas littéralement un nombre mais une sorte d'identificateur comme ORDR-13432-Year-Month-Day). Le calcul précis n'a pas d'importance pour le moment, il est simplement important qu'il soit dépendant de la date et de l'heure locales des locataires
  • Nous souhaitons également pouvoir sélectionner toutes les commandes, au niveau système, placées entre certaines dates UTC, quel que soit le locataire (statistiques système générales/rapports).

Notre idée initiale

  • Notre idée initiale était de sauvegarder la date et l'heure UTC dans l'ensemble de la base de données et, bien sûr, de conserver le décalage du fuseau horaire des locataires par rapport à l'heure UTC et de laisser une application consommant une base de données convertir toujours les dates/heures en temps UTC, de sorte que la base de données elle-même fonctionne toujours avec UTC.

approche 1

  • L'enregistrement des locataires locaux en datetime correspond à Nice par locataire, mais nous avons un problème avec des requêtes telles que:

    SELECT * FROM ORDERS WHERE OrderDateTime BETWEEN UTCDateTime1 AND UTCDateTime2
    

C'est problématique parce que OrderDateTime dans cette requête signifie un moment différent dans le temps, en fonction du locataire. Bien entendu, cette requête peut inclure la jointure à la table Tenants afin d’obtenir un décalage de date/heure local, qui calculerait ensuite OrderDateTime à la volée pour effectuer des ajustements. C'est possible, mais vous ne savez pas si c'est un bon moyen de le faire?

approche 2

  • Par ailleurs, lors de la sauvegarde de la date et de l'heure UTC, le calcul du numéro de commande depuis le jour/mois/année en UTC peut différer de celui de la date et de l'heure locales.

Prenons l'exemple extrême; Supposons que le locataire a 6 heures d'avance sur UTC et que son heure de référence locale est 2017-01-01 02:00. UTC serait 2016-12-31 20:00. La commande passée à ce moment devrait obtenir OrderNumber 'ORDR-13432-2017-1-1' mais si vous sauvegardiez UTC, vous obtiendrez ORDR-13432-2016-12-31.

Dans ce cas, au moment de la création de Order in DB, nous devrions obtenir la date et l'heure UTC, les locataires doivent être compensés et compiler OrderNumber sur la base des locataires recalculés, heure locale, tout en conservant la colonne DateTime au format UTC.

Questions

  1. Quel est le moyen privilégié de gérer ce genre de situation?
  2. Existe-t-il une solution intéressante avec la sauvegarde des heures UTC, car celle-ci serait très agréable pour nous en raison des rapports au niveau du système?
  3. L’approche 2) est-elle un bon moyen de traiter ces cas ou existe-t-il un moyen meilleur/recommandé?

[UPDATE]

D'après les commentaires de Gerard Ashton et Hugo:

La question initiale n'était pas claire en ce qui concerne les détails si le locataire peut modifier le fuseau horaire ou non et ce qui se passe si l'autorité politique modifie les propriétés du fuseau horaire ou le fuseau horaire d'un territoire. Bien sûr, cela revêt une extrême importance, mais ce n’est pas au centre de cette question. Nous pourrions aborder cette question dans une question distincte.

Dans l'intérêt de cette question, supposons que le locataire ne change pas d'emplacement. Les propriétés de fuseau horaire ou le fuseau horaire lui-même pour cet emplacement peuvent changer et ces modifications seront traitées dans le système séparément de cette question.

27
daneejela

La réponse de Hugo est généralement correcte, mais j'ajouterai quelques points essentiels:

  • Lorsque vous enregistrez le fuseau horaire du client, NE stockez PAS un décalage numérique. Comme d'autres l'ont souligné, le décalage par rapport au temps UTC ne concerne qu'un seul point dans le temps et peut facilement changer pour l'heure d'été ou pour d'autres raisons. Au lieu de cela, vous devez stocker un identifiant de fuseau horaire, de préférence un identifiant de fuseau horaire IANA sous forme de chaîne, telle que "America/Los_Angeles". Lisez la suite dans le wiki de la balise de fuseau horaire .

  • Votre champ OrderDateTime devrait absolument représenter l'heure en UTC. Cependant, en fonction de votre plate-forme de base de données, vous avez plusieurs choix pour le stocker.

    • Par exemple, si vous utilisez Microsoft SQL Server, une bonne approche consiste à stocker l'heure locale dans une colonne datetimeoffset, qui conserve le décalage par rapport à l'UTC. Notez que tout index que vous créez sur cette colonne sera basé sur l'équivalent UTC. Vous obtiendrez ainsi de bonnes performances lors de l'interrogation de plage.

    • Si vous utilisez d'autres plates-formes de base de données, vous souhaiterez peut-être stocker la valeur UTC dans un champ timestamp. Certaines bases de données ont également timestamp with time zone, Mais comprenez que cela ne veut pas dire que stocke le fuseau horaire ou le décalage, cela signifie simplement que peut effectuer des conversions pour vous de manière implicite lorsque vous stockez et récupérez des valeurs. Si vous avez l’intention de toujours représenter l’UTC, alors timestamp (sans fuseau horaire) ou simplement datetime est plus approprié.

  • Étant donné que l'une ou l'autre des méthodes ci-dessus stocke une heure UTC, vous devez également déterminer comment effectuer les opérations nécessitant un index des valeurs d'heure locale. Par exemple, vous devrez peut-être créer un rapport quotidien, basé sur le jour du fuseau horaire de l'utilisateur. Pour cela, vous devez regrouper selon la date locale. Si vous essayez de calculer cela au moment de la requête à partir de votre valeur UTC, vous allez analyser l'ensemble de la table.

    Une bonne solution consiste à créer une colonne distincte pour le date local (ou peut-être même le datetime local selon vos besoins, mais non a datetimeoffset ou timestamp). Il peut s'agir d'une colonne complètement isolée que vous remplissez séparément ou d'une colonne calculée/calculée en fonction de votre autre colonne. Utilisez cette colonne dans un index pour pouvoir filtrer ou grouper par date locale.

  • Si vous optez pour l'approche par colonne calculée, vous devez savoir comment convertir des fuseaux horaires dans la base de données. Certaines bases de données ont une fonction intégrée convert_tz Qui comprend les identifiants de fuseau horaire IANA.

    Si vous utilisez Microsoft SQL Server, vous pouvez utiliser la nouvelle fonction AT TIME ZONE dans SQL 2016 et Azure SQL DB, mais cela ne fonctionne qu'avec les identificateurs de fuseau horaire Microsoft. Pour utiliser les identifiants de fuseau horaire IANA, vous avez besoin d'une solution tierce, telle que mon Support du fuseau horaire SQL Server .

  • Au moment de la requête, évitez d'utiliser l'instruction BETWEEN. C'est totalement inclusif. Cela fonctionne bien pour des dates entières, mais lorsque vous avez du temps à faire, vous feriez mieux de faire une requête à demi ouverte, telle que:

    ... WHERE OrderDateTime >= @t1 AND OrderDateTime < @t2
    

    Par exemple, si @t1 Était le début d'aujourd'hui, @t2 Serait le début de demain.

En ce qui concerne le scénario décrit dans les commentaires où le fuseau horaire de l'utilisateur a été modifié:

  • Si vous choisissez de calculer la date locale dans la base de données, vous ne devez vous inquiéter que si un emplacement ou une entreprise change de fuseau horaire sans "division de zone". Une division de zone se produit lorsqu'un nouvel identifiant de fuseau horaire est introduit et couvre la zone qui a été modifiée, y compris leurs anciennes et nouvelles règles.

    Par exemple, la dernière zone ajoutée à la tzdb IANA au moment de la rédaction est America/Punta_Arenas, Une zone scindée lorsque la partie sud du Chili a décidé de rester à UTC-3 alors que le reste du Chili (America/Santiago) Est retourné à UTC-4 à la fin de l'heure d'été.

    Toutefois, si une localité mineure à la frontière de deux fuseaux horaires décide de changer de côté et qu'elle ne se divise pas en zones, vous utiliseriez potentiellement les règles de leur nouveau fuseau horaire contre leurs anciennes données.

  • Si vous stockez la date locale séparément (calculée dans l'application, pas dans la base de données), aucun problème ne se produira. L'utilisateur modifie son nouveau fuseau horaire, toutes les anciennes données sont toujours intactes et les nouvelles données sont stockées avec le nouveau fuseau horaire.

25
Matt Johnson-Pint

Je recommanderais de toujours utiliser UTC en interne et de convertir en fuseau horaire uniquement lors de l'affichage de la date à l'utilisateur. J'ai donc tendance à préférer l'approche 2.

S'il existe une règle de gestion indiquant que la date/heure locale du locataire doit faire partie de l'identifiant, qu'il en soit ainsi. Mais en interne, vous conservez la date de commande en UTC.

En utilisant votre exemple: un locataire dont le fuseau horaire est dans UTC+06:00, l'heure locale du locataire est donc 2017-01-01 02:00, ce qui équivaut à 2016-12-31 20:00 en UTC.

L'ordre identifiant serait ORDR-13432-2017-1-1 et la commande date serait UTC 2016-12-31 20:00Z.

Pour obtenir toutes les commandes entre 2 dates, cette requête est simple:

SELECT * FROM ORDERS WHERE OrderDateTime BETWEEN UTCDateTime1 AND UTCDateTime2

Parce que OrderDateTime est en UTC.

Si vous recherchez un locataire spécifique, vous pouvez obtenir le fuseau horaire correspondant, convertir la date en conséquence et le rechercher. En utilisant le même exemple ci-dessus (le fuseau horaire du locataire est dans UTC+06:00), pour obtenir toutes les commandes passées en 2017-01-01 (heure locale du locataire):

--get tenant timezone
--startUTC=tenant's local 2017-01-01 00:00 converted to UTC (2016-12-31T18:00Z)
--endUTC=tenant's local 2017-01-01 23:59:59.999 converted to UTC (2017-01-01T17:59:59.999)
SELECT * FROM ORDERS WHERE OrderDateTime between startUTC and endUTC

Cela va obtenir ORDR-13432-2017-1-1 correctement.


Pour effectuer des requêtes sur plusieurs clients hébergés dans des fuseaux horaires différents, les deux approches nécessitent une jointure. Aucune n'est donc "meilleure" dans ce cas.

Sauf si vous créez une colonne supplémentaire avec la date/heure locale du locataire (le UTC OrderDateTime converti en fuseau horaire du locataire). Ce sera redondant, mais cela peut vous aider pour les requêtes qui effectuent des recherches dans plusieurs fuseaux horaires. Si cela constitue un compromis raisonnable, cela dépendra de la fréquence à laquelle ces requêtes seront effectuées.

14
user7605325