当数据依赖于日期时间时,在数据库中保存日期时间和时区信息的最佳实践

Best practices with saving datetime & timezone info in database when data is dependant on datetime

关于将datetime&timezones信息保存在数据库中有很多问题,但总体上有更多问题。在这里,我想处理一个具体的案件。

系统规格

  • 我们有订单系统数据库
  • 它是一个多租户系统,租户可以使用任意时区(它是任意的,但每个租户只有一个时区,保存在租户表中一次,从不更改)

数据库中需要包含的业务规则

  • 当租户向系统下订单时,订单号将根据其本地日期时间(它不是字面上的数字,而是某种标识符,如ORDR-13432-Year-Month-Day)进行计算。精确的计算目前并不重要,重要的是它依赖于租户的本地日期时间。
  • 我们还希望能够在系统级别上选择在某些UTC日期时间之间放置的所有订单,而不考虑租户(用于一般系统统计/报告)

我们最初的想法

  • 我们最初的想法是在整个数据库中保存UTC日期时间,当然,保持租户时区相对于UTC的偏移量,并让使用数据库的应用程序始终将日期时间转换为UTC,以便数据库本身始终与UTC一起运行。

方法1

  • 保存本地租户的日期时间对于每个租户来说是一个不错的选择,但是我们在以下查询方面遇到了问题:

    1
    SELECT * FROM ORDERS WHERE OrderDateTime BETWEEN UTCDateTime1 AND UTCDateTime2

这是有问题的,因为这个查询中的OrderDateTime表示基于租户的不同时间点。当然,这个查询可能包括连接到Tenants表以获取本地日期时间偏移量,然后动态计算OrderDateTime以进行调整。这是可能的,但不确定这是否是一个好的方法?

方法2

  • 另一方面,在保存UTC日期时间时,当我们计算订单号时,由于UTC中的日/月/年可能与本地日期时间中的不同

让我们举个极端的例子:假设租户比UTC早6小时,而他的本地日期时间是2017-01-01 02:00。UTC将是2016-12-31 20:00。此时下的订单应获得订单号'ORDR-13432-2017-1-1',但如果保存UTC,将获得ORDR-13432-2016-12-31

在这种情况下,在数据库中创建订单时,我们应该获取UTC日期时间、租户偏移量,并根据重新计算的租户本地时间编译订单号,但仍将日期时间列保存为UTC。

问题

  • 处理这种情况的首选方法是什么?
  • 是否有一个保存UTC日期时间的好解决方案,因为由于系统级别的报告,该解决方案对我们来说非常好?
  • 如果要保存UTC,方法2)是处理这些情况的好方法还是有更好/推荐的方法?
  • [更新]

    根据杰拉德·阿什顿和雨果的评论:

    最初的问题在细节上还不清楚,承租人是否可以改变时区,如果政治权威改变了时区属性或某个极端时区,会发生什么。当然,这是一个非常重要的问题,但它不在这个问题的中心。我们可以在另一个问题中解决这个问题。

    为了这个问题,我们假设租户不会改变位置。该位置的时区属性或时区本身可能会更改,这些更改将在系统中与此问题分开处理。


    雨果的回答基本上是正确的,但我要补充一些要点:好的。

    • 在存储客户时区时,不要存储数字偏移量。正如其他人所指出的,与UTC的偏差仅限于一个时间点,并且可以很容易地因DST和其他原因而改变。相反,您应该将时区标识符(最好是IANA时区标识符)存储为字符串,如"America/Los_Angeles"。在时区标签wiki中阅读更多信息。好的。

    • 您的OrderDateTime字段应该绝对代表UTC中的时间。但是,根据数据库平台的不同,对于如何存储它,您有几个选择。好的。

      • 例如,如果使用Microsoft SQL Server,一种好的方法是将本地时间存储在datetimeoffset列中,该列保留了与UTC的偏移量。请注意,您在该列上创建的任何索引都将基于等效的UTC,因此在进行范围查询时,您将获得良好的查询性能。好的。

      • 如果使用其他数据库平台,您可能希望将UTC值存储在timestamp字段中。有些数据库也有timestamp with time zone,但要明白,它并不意味着它存储时区或偏移量,它只是意味着它可以在存储和检索值时隐式地为您进行转换。如果您打算始终代表UTC,那么通常timestamp(无时区)或仅datetime更合适。好的。

    • 由于上述任何一种方法都将存储UTC时间,因此您还需要考虑如何执行需要本地时间值索引的操作。例如,您可能需要根据用户时区的日期创建每日报告。为此,您需要按本地日期分组。如果您试图在查询时从您的UTC值计算它,那么您将最终扫描整个表。好的。

      处理这一问题的一个好方法是为本地date创建一个单独的列(甚至可能是本地datetime,这取决于您的需要,但不是datetimeoffsettimestamp。这可能是一个完全独立的列,可以单独填充,也可以是基于其他列的计算/计算列。在索引中使用此列,以便可以按本地日期筛选或分组。好的。

    • 如果使用计算列方法,则需要知道如何在数据库中的时区之间进行转换。有些数据库内置了一个convert_tz功能,可以理解IANA时区标识符。好的。

      如果您使用的是Microsoft SQL Server,则可以在SQL 2016和Azure SQL DB中使用新的AT TIME ZONE函数,但这只适用于Microsoft时区标识符。要使用IANA时区标识符,您需要第三方解决方案,如我的SQL Server时区支持项目。好的。

    • 在查询时,避免使用BETWEEN语句。它是完全包容性的。它对整个日期都适用,但是当您有时间参与时,最好进行半开放范围的查询,例如:好的。

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

      例如,如果@t1是今天的开始,那么@t2将是明天的开始。好的。

    关于用户时区已更改的注释中讨论的方案:好的。

    • 如果您选择计算数据库中的本地日期,那么您需要担心的唯一情况是,位置或业务是否切换时区而不发生"区域分割"。区域分割是指引入一个新的时区标识符,该标识符覆盖已更改的区域,包括其旧规则和新规则。好的。

      例如,在撰写本文时添加到IANA TZDB中的最新区域是America/Punta_Arenas,当智利南部决定留在UTC-3时,智利其余部分(America/Santiago)在DST结束时返回到UTC-4,这是一个区域分割。好的。

      但是,如果两个时区边界上的一个次要区域决定更改它们所遵循的边,并且不保证分区分割,那么您可能会使用它们的新时区规则来对抗它们的旧数据。好的。

    • 如果单独存储本地日期(在应用程序中计算,而不是数据库),那么就不会有任何问题。用户将其时区更改为新时区,所有旧数据仍保持不变,新数据与新时区一起存储。好的。

    好啊。


    我建议在内部始终使用UTC,只有在向用户显示日期时才转换为时区。所以我倾向于使用方法2。

    如果有业务规则规定租户的本地日期/时间必须是标识符的一部分,那么就这样做吧。但在内部,您将订单日期保留为UTC。

    使用您的示例:时区在UTC+06:00中的租户,因此租户的本地时间是2017-01-01 02:00,相当于UTC中的2016-12-31 20:00

    订单标识符为ORDR-13432-2017-1-1,订单日期为utc 2016-12-31 20:00Z

    要获取2个日期之间的所有订单,此查询将很紧张:

    1
    SELECT * FROM ORDERS WHERE OrderDateTime BETWEEN UTCDateTime1 AND UTCDateTime2

    因为OrderDateTime是用UTC表示的。

    如果要查找特定的租户,那么可以获取相应的时区,相应地转换日期并搜索它。使用上面的相同示例(租户的时区在UTC+06:00中),获取2017-01-01中的所有订单(在租户的本地时间中):

    1
    2
    3
    4
    --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

    这将使ORDR-13432-2017-1-1正确。

    为了在不同的时区对多个租户进行查询,这两种方法都需要一个联接,因此对于这种情况,没有一种方法是"更好的"。

    除非使用租户的本地日期/时间(UTC OrderDateTime转换为租户的时区)创建额外的列。它将是多余的,但它可以帮助您处理在多个时区中搜索的查询。如果这是一个合理的权衡,那将取决于这些查询的频率。