有很多关于保存日期和时间的问题。数据库中的时区信息,但总体水平更多。在这里,我想谈谈具体案例。
系统规格
需要在DB
中涵盖业务规则ORDR-13432-Year-Month-Day
)。目前,精确计算并不重要,依赖租户本地日期时间非常重要我们最初的想法
方法1
保存本地租户的日期时间对每个租户来说都不错,但我们遇到的问题包括:
SELECT * FROM ORDERS WHERE OrderDateTime BETWEEN UTCDateTime1 AND UTCDateTime2
这是有问题的,因为此查询中的OrderDateTime
表示基于租户的不同时刻。当然,此查询可能包括连接到Tenants
表以获取本地日期时间偏移量,然后在运行中计算OrderDateTime
以进行调整。这是可能的,但不确定这是否是一个好方法呢?
方法2
让我们举一个极端的例子;假设租户比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
。
在这种情况下,在DB中创建Order时,我们应该根据重新计算的租户本地时间获取UTC日期时间,租户偏移量和编译OrderNumber,但仍然以UTC格式保存DateTime列。
问题
[UPDATE]
基于Gerard Ashton和Hugo的评论:
如果租户可以改变时区,那么最初的问题就不清楚,如果政治当局改变了时区属性或某个地区的时区会发生什么。 当然,这是非常重要的,但它不是这个问题的核心。我们可以在一个单独的问题中解决这个问题。
为了这个问题,我们假设租户不会改变位置。该位置的时区属性或时区本身可能会发生变化,这些更改将在系统中与此问题分开处理。
答案 0 :(得分:20)
雨果的回答大多是正确的,但我会补充一些要点:
当您存储客户的时区时,请勿存储数字偏移。正如其他人所指出的那样,与UTC的偏移仅适用于单个时间点,并且可以很容易地因DST和其他原因而改变。相反,您应该存储时区标识符,最好是IANA时区标识符作为字符串,例如"America/Los_Angeles"
。请阅读the timezone tag wiki。
您的OrderDateTime
字段绝对应代表UTC的时间。但是,根据您的数据库平台,您可以选择如何存储它。
例如,如果使用Microsoft SQL Server,一种好方法是将本地时间存储在datetimeoffset
列中,该列保留与UTC的偏移量。请注意,您在该列上创建的任何索引都将基于UTC等效项,因此在进行范围查询时,您将获得良好的查询性能。
如果使用其他数据库平台,您可能希望将UTC值存储在timestamp
字段中。有些数据库也有timestamp with time zone
,但是要知道它并不意味着存储时区或偏移量,它只是意味着它可以在您存储和检索值时隐式地为您进行转换。如果您打算始终代表UTC,那么通常timestamp
(没有时区)或仅datetime
更合适。
由于上述任何一种方法都会存储UTC时间,因此您还需要考虑如何执行需要本地时间值索引的操作。例如,您可能需要根据用户时区的日期创建每日报告。为此,您需要按本地日期分组。如果您尝试在查询时根据UTC值计算,则最终会扫描整个表格。
处理此问题的一个好方法是根据您的需要为本地date
(或者甚至是本地datetime
创建单独的列,但不 a datetimeoffset
或timestamp
)。这可以是单独填充的完全隔离的列,也可以是基于其他列的计算/计算列。在索引中使用此列,以便按本地日期过滤或分组。
如果您选择计算列方法,则需要知道如何在数据库中的时区之间进行转换。某些数据库具有内置的convert_tz
功能,可以理解IANA时区标识符。
如果您使用的是Microsoft SQL Server,则可以在SQL 2016和Azure SQL DB中使用新的AT TIME ZONE
功能,但这仅适用于Microsoft时区标识符。要使用IANA时区标识符,您需要第三方解决方案,例如我的SQL Server Time Zone Support项目。
在查询时,请避免使用BETWEEN
语句。它是完全包容的。它适用于整个日期,但是当你有时间参与时,你最好做一个半开放范围的查询,例如:
... WHERE OrderDateTime >= @t1 AND OrderDateTime < @t2
例如,如果@t1
是今天的开始,@t2
将是明天的开始。
关于用户时区发生变化的评论中讨论的场景:
如果您选择计算数据库中的本地日期,则唯一需要担心的情况是位置或业务切换时区而不会发生“区域拆分”。区域拆分是指引入新的时区标识符,其中包含更改的区域,包括旧规则和新规则。
例如,在撰写本文时添加到IANA tzdb的最新区域为America/Punta_Arenas
,这是智利南部决定在智利其他地区停留在UTC-3时的区域分割(America/Santiago
)在夏令时结束时回到了UTC-4。
但是,如果两个时区边界上的次要地点决定改变他们遵循的哪一方,并且没有保证区域拆分,那么您可能会使用他们的新时区的规则来对抗他们的旧时区数据
如果您单独存储本地日期(在应用程序中计算,而不是数据库),那么您将没有任何问题。用户将时区更改为新时区,所有旧数据仍然完好无损,新数据将与新时区一起存储。
答案 1 :(得分:9)
我建议始终在内部使用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个日期之间的所有订单,此查询是直截了当的:
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
。
要对不同时区的多个租户进行查询,两种方法都需要加入,因此对于这种情况,没有一种方法“更好”。
除非您使用租户的本地日期/时间创建额外的列( UTC OrderDateTime
转换为租户的时区)。这将是多余的,但它可以帮助您查询在多个时区搜索。如果这是一个合理的权衡,那将取决于这些查询的频率。