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

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

有很多关于在数据库中保存日期时间和时区信息的问题,但更多的是关于整体层面的问题。这里我想说一个具体的案例。

系统规格

需要在数据库中涵盖业务规则

我们的初步想法

方法一

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

方法二

举个极端的例子;假设租户比 UTC 早 6 小时,他的本地日期时间是 2017-01-01 02:00。 UTC 将是 2016-12-31 20:00。当时下的订单应该得到 OrderNumber 'ORDR-13432-2017-1-1' 但如果保存 UTC 它将得到 ORDR-13432-2016-12-31.

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

问题

  1. 处理这种情况的首选方法是什么?
  2. 是否有保存 UTC 日期时间的好解决方案,因为系统级报告对我们来说非常好?
  3. 如果要保存 UTC,方法 2) 是处理这些情况的好方法还是有一些 better/recommended 方法?

[更新]

根据 Gerard Ashton 和 Hugo 的评论:

关于承租人是否可以更改时区的细节,以及如果政治当局更改时区属性或某些地区的时区会发生什么,最初的问题并不清楚。 当然,这是极其重要的,但它不是这个问题的核心。我们可能会在一个单独的问题中解决这个问题。

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

我建议始终在内部使用 UTC,并且仅在向用户显示日期时才转换为时区。所以我倾向于更喜欢方法 2.

如果有业务规则规定租户的本地 date/time 必须是标识符的一部分,那就这样吧。但在内部,您将订单日期保留为 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 个日期之间的所有订单,此查询很简单:

SELECT * FROM ORDERS WHERE OrderDateTime BETWEEN UTCDateTime1 AND UTCDateTime2

因为 OrderDateTime 是 UTC。

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

--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 正确。


要对不同时区的多个租户进行查询,两种方法都需要连接,因此对于这种情况 none 是 "better"。

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

雨果的回答大部分是正确的,但我要补充几个要点:

  • 当您存储客户的时区时,请勿存储数字偏移量。正如其他人指出的那样,与 UTC 的偏移量仅适用于单个时间点,并且可以很容易地因 DST 和其他原因而改变。相反,您应该存储时区标识符,最好是 IANA 时区标识符作为字符串,例如 "America/Los_Angeles"。在 the timezone tag wiki.

  • 中阅读更多内容
  • 您的 OrderDateTime 字段应该绝对代表 UTC 时间。但是,根据您的数据库平台,您有多种存储方式的选择。

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

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

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

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

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

    如果您使用的是 Microsoft SQL 服务器,则可以使用新的 AT TIME ZONE function in SQL 2016 and Azure SQL DB, but that only works with Microsoft time zone identifiers. To use IANA time zone identifiers, you'll need a third party solution, such as my SQL Server Time Zone Support 项目。

  • 查询时,避免使用BETWEEN语句。它是完全包容的。它适用于整个日期,但是当你有时间参与时,你最好做一个半开放范围查询,例如:

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

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

关于评论中讨论的用户时区发生变化的场景:

  • 如果您选择计算数据库中的本地日期,您唯一需要担心的情​​况是位置或企业切换时区时没有 "zone split" 发生。时区拆分是指引入新的时区标识符,该标识符涵盖发生变化的区域,包括新旧规则。

    例如,在撰写本文时添加到 IANA tzdb 的最新区域是 America/Punta_Arenas,这是智利南部决定留在 UTC-3 时的区域拆分,而其余部分智利 (America/Santiago) 在夏令时结束时回到 UTC-4。

    但是,如果两个时区边界上的一个小地方决定改变他们所遵循的那一边,并且没有必要进行时区分割,那么您可能会使用他们新时区的规则来反对他们的旧数据。

  • 如果您单独存储本地日期(在应用程序中计算,而不是在数据库中计算),那么您将没有问题。用户将他们的时区更改为新时区,所有旧数据仍然完好无损,新数据以新时区存储。