database - PostgreSQL应用程序中的不同时区

标签 database postgresql

我们有一个为许多配送中心服务的系统。配送中心是全国任何地方的物理空间。一个客户可能有多个分销中心。现在我们正在扩展到更多地方,我们将不得不面对不同时区的问题。同一客户也可能在不同时区设有中心。

使用我们的客户中心系统,可以创建和保存许多事件(包括日期和小时)。同一客户端中不同时区的理想行为如下:

考虑中午在时区A中发生的事件。如果时区B中的另一个配送中心的主管去检查此事件,则他应该看到该事件的日期和小时,与最初创建该事件的时区(包括DST更改,如果存在)相对。
这是因为要知道的是,该事件是否在事件时区的中午创建。对于主管来说,创建事件的时间是他所在时区的下午2点无关紧要。

我们使用PostgreSQL作为数据库,我发现存在两种不同的类型来节省时间戳。 TIMESTAMPTIMESTAMPTZ。我们所有的数据库仅使用TIMESTAMP类型。

可能(也可能不会)发生的另一种情况是配送中心的地理位置发生了变化。这可能会影响其时区的更改。

我进行了一些研究,发现更“正确”的方法(至少看起来是正确的)是将每个中心的时区保存在DISTRIBUTION_CENTER表中。将数据库中的所有类型从TIMESTAMP更改为TIMESTAMPTZ,并在每次保存时间戳的事件插入中,都应使用事件创建中心的时区来保存事件时区的偏移量(< cc>仅保存时区的偏移量,而不保存时区本身)。

我不完全相信这是处理不同时区的正确(或最佳)方法。因为我从来没有实现过这样的事情,所以我不能说。

如果必须遵循这种方法,则必须将数据库中的所有列类型从TIMESTAMPTZ更改为TIMESTAMP。还必须重新创建以某种方式依赖该列的所有视图,因为我们将更改列的类型。我还必须更改所有处理该列的查询,以使用TIMESTAMPTZ应用中心时区。

现在,数据库已设置了AT TIMEZONE时区,而我担心更改America/Sao_Paulo的列类型时最终会出错。此更改是否有可能破坏数据一致性?我应该首先更改列类型还是更改TIMESTAMPTZ的数据库时区吗?

我描述的解决方案是做到这一点的最佳方法?

该方法还可以正确处理DST更改吗?

额外信息:我们的服务器使用Java(球衣)。

最佳答案

这些问题已在Stack Overflow和姐妹站点https://dba.stackexchange.com/上多次讨论。请下次在发布前彻底搜索。因此,这里只是回顾一下。

首先,您必须了解offset-from-UTCtime zone之间的区别。偏移量仅是距UTC数小时,数分钟和数秒的偏移量。时区是特定区域的人们使用的偏移的过去,现在和将来的历史。因此,使用时区总是比单纯的偏移更可取。


存在两种不同的类型以节省时间戳。 TIMESTAMP和TIMESTAMPTZ


不完全是。 SQL标准定义的实际类型为TIMESTAMP WITH TIME ZONETIMESTAMP WITHOUT TIME ZONE。其他名称是Postgres特定的同义词。为了清楚起见,我建议您使用标准名称。日期时间处理足够令人困惑,而最后并没有模棱两可地读取/记住z的含义。

SQL规范几乎没有涉及日期时间处理的主题。因此,行为在数据库实现之间差异很大。

Postgres的工作方式确实非常简单。


对于类型为TIMESTAMP WITH TIME ZONE的列,任何通过自UTC偏移量或时区传递的输入都将自动调整为UTC。调整后,原始值的偏移量/区域信息将被丢弃。 “带时区”实际上意味着“相对于传入数据的时区”,而不是“按时区存储”。如果必须知道原始偏移量/区域,则必须将其自己存储在单独的列中。我建议使用ISO 8601标准格式作为偏移量或proper name of timezone作为文本。如果输入缺少任何区域/偏移量指示符,则应用会话的当前默认时区,然后对UTC进行调整-正如我隐约记得的那样;您不应该通过缺少区域/偏移的输入!
对于TIMESTAMP WITHOUT TIME ZONE列,将忽略带有输入(如果有)的任何区域/偏移信息。同样,检索时此值没有区域/偏移。此类型没有区域/偏移量的概念。当您打算存储时刻(时间轴上的一点)时,请勿使用此类型。这种类型是关于大约26-27小时范围内潜在时刻的模糊概念,例如“圣诞节从2018年12月25日午夜之后开始”。在添加“在日本”,“在印度”或“在法国”之前,这样的句子没有实际意义(从而在另一种类型的TIMESTAMP WITH TIME ZONE中创建值)。这种类型还用于未来数周以上的约会,这时政治家可能会更改其所在地区的偏移量(他们出人意料地经常这样做,并且几乎没有预警)。


注意:令人困惑的是,某些工具或驱动程序可能会将会话的当前默认时区应用于这两种类型的值。这包括pgAdmin。我认为这是个可怕的反特征。意图很好,但是您和Postgres之间的这种工具/驱动程序不应注入其对传输中数据的“意见”。这样做会产生一种错觉,即当正好相反时(实际上不带UTC或不带区域/偏移量),则检索到的数据将在数据库内携带该区域。如果您的工具进行了这种调整,则很可能由Postgres会话中的区域/偏移设置控制,如discussed here

日期时间处理的最佳实践是使用UTC进行思考,工作,存储,记录和交换数据。可以认为其他区域仅仅是该主题的变体。仅在业务逻辑要求或向用户展示时才调整到时区。忘记所有关于您自己的时区的信息。认真地在办公桌上设置第二个时钟为UTC。


现在数据库已设置了America / Sao_Paulo时区


服务器操作系统的默认时区应该无关紧要。绝对不要依赖程序员这样的默认设置,因为它完全不受您的控制,并且很容易更改。

在Java中,JVM具有自己的当前默认时区,与主机OS分开。 JVM的当前默认值可以在运行时随时通过该JVM中任何应用程序的任何线程中的任何代码进行更改。因此,永远不要依赖当前的默认值。始终通过传递可选参数来明确指定所需/期望的时区。


如果时区B中另一个配送中心的主管去检查此事件,他应该看到


如上所述,在数据库方面,您应该使用UTC。调整到用户期望的时区是用户界面任务,而不是数据库任务。就像国际化一样,您将在数据库中存储某种键查找值,以便在用户界面侧将其本地化为某些人类语言。


TIMESTAMPTZ仅保存时区的偏移量,而不保存时区本身


不,不正确。如上所述,TIMESTAMP WITH TIME ZONE类型在调整为UTC进行存储后会丢弃偏移量/区域信息。没有偏移,没有区域,只有UTC力矩存储在该列中-基本上是自epoch reference以来的微秒数。


将数据库中的所有类型从TIMESTAMP更改为TIMESTAMPTZ,并在每次保存时间戳的事件插入中,都应使用创建事件中心的时区来保存事件时区的偏移量


如果您说您已经将来自各个时区的日期时间值记录到TIMESTAMP WITHOUT TIME ZONE列中,那么您将感到一团糟。您无法可靠地进行清理,而不能完全准确地进行清理,因为您实际上并不知道最初打算将哪个区域/偏移量用于传递给数据库的输入。您可以猜测所存储数据的原始意图,但无法确定。

向您的老板和利益相关者解释,这不是您的事。解释说,无论谁设置了此数据库和应用程序,都相当于以各种货币(例如日元,加元,英镑和欧元)存储了货币金额,而不必费心记录每种货币所使用的货币。

如果您想猜测,则需要知道可能使用的时区名称。

在Java中,仅使用Java 8及更高版本中内置的java.time类。较早的日期时间类是一个令人毛骨悚然的烂摊子,现已遗留,由JSR 310中定义的java.time取代。

确定您可能的区域。

ZoneId zoneSaoPaulo = ZoneId.of( "America/Sao_Paulo" ) ;
ZoneId zoneLisbon = ZoneId.of( "Europe/Lisbon" ) ;
ZoneId zoneKolkata = ZoneId.of( "Asia/Kolkata" ) ;


将日期时间值提取为LocalDateTime,这是缺少任何区域/偏移量概念的日期时间值的Java类。使用JDBC 4.2及更高版本,您可以直接与数据库交换java.time对象。

LocalDateTime ldt = myResultSet.getObject( … , LocalDateTime.class ) ;


枚举可能是代表您的配送中心的合适方法。这假设列表在运行时不需要更改。

public enum DistributionCenter {
    // List the constants to be constructed automatically when this class loads.
    SAOPAULO( ZoneId.of( "America/Sao_Paulo" ) ) ,
    LISBON( ZoneId.of( "Europe/Lisbon" ) ) ,
    KOLKATA( ZoneId.of( "Asia/Kolkata" ) ) 

    final public ZoneId zoneId ;  // Make public, or add a getter method to access private member.

    // Add constructor taking the passed `ZoneId` and storing in the variable.
}


应用区域,以生成一个ZonedDateTime对象。现在我们有了一个实际时刻,即时间轴上的特定点。

DistributionCenter dc = … ;
ZonedDateTime zdt = ldt.atZone( dc.zoneId ) ;


将该值调整为UTC值。同一时刻,时间轴上的同一点,不同的挂钟时间。在您清楚了解该概念之前,请勿继续进行您的项目。

Instant类代表UTC在时间轴上的时刻,分辨率为nanoseconds(十进制小数的九(9)位)。

Instant instant = zdt.toInstant() ;


您应该能够将ZonedDateTime对象传递给JDBC驱动程序,以进行UTC调整。我只是想说一点,我们最终将在Postgres存储中使用UTC值。另外,我还是自己转换为Instant以便于调试–请记住:UTC是“真实时间”。

现在我们已经确定了一个实际时刻,我们可以将其存储在数据库中。

myPreparedStatement.setObject( … , instant ) ;


请注意,这些代码如何都不依赖于服务器主机OS,Postgres集群或JVM的当前默认时区。


我将不得不在数据库中将所有列类型从TIMESTAMP更改为TIMESTAMPTZ


是。记录实际时刻(一段历史)的数据不应该存储在TIMESTAMP WITHOUT TIME ZONE中。一些天真的程序员/ DBA希望使用此数据类型可以使他们免于处理时区问题。但这实际上是“现在付款,或以后付款”的情况。不幸的是,您是为他们的糟糕选择付出代价的人。

您可能可以在Postgres过程中完成相同的工作。 Postgres的support for date-time work确实比大多数数据库都要好。但是,没有什么比java.time类更适合日期时间处理的了。而且,就我个人而言,我更愿意在Java中调试和练习这种特殊的工作。


配送中心在地理上发生变化


那是令人困惑和不明智的。企业确实应该将新位置标识为新中心,而不是相同。如果您不能说服管理层这样做,那么我将在您的数据库和后台应用程序中这样做。



关于java.time

java.time框架内置于Java 8及更高版本中。这些类取代了麻烦的旧legacy日期时间类,例如java.util.DateCalendarSimpleDateFormat

现在位于Joda-Time中的maintenance mode项目建议迁移到java.time类。

要了解更多信息,请参见Oracle Tutorial。并在Stack Overflow中搜索许多示例和说明。规格为JSR 310

使用与JDBC driver或更高版本兼容的JDBC 4.2,您可以直接与数据库交换java.time对象。不需要字符串或java.sql。*类。

在哪里获取java.time类?


Java SE 8Java SE 9和更高版本


内置的
标准Java API的一部分,具有捆绑的实现。
Java 9添加了一些次要功能和修复。

Java SE 6Java SE 7


ThreeTen-Backport中的大部分java.time功能都已反向移植到Java 6和7。

Android


更高版本的Android捆绑了java.time类的实现。
对于早期的Android,ThreeTenABP项目改编了ThreeTen-Backport(如上所述)。请参见How to use ThreeTenABP…



ThreeTen-Extra项目使用其他类扩展了java.time。该项目为将来可能在java.time中添加内容提供了一个试验场。您可能会在这里找到一些有用的类,例如IntervalYearWeekYearQuartermore

关于database - PostgreSQL应用程序中的不同时区,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48956401/

相关文章:

sql - 执行作为值提供的语句

postgresql - 如何防止 Postgres 中表的 autovacuum

database - Exclusive Arc 和多个多对多关系数据库设计问题

python - Alembic — 在 `alembic_version` 表中存储额外信息

database - 中止 BGSAVE 已经在进行中

php - 能够使用 Laravel 在运行时更改数据库连接

mysql - 如何在本地找到我为 Rails 应用程序创建的数据库并将其加载到服务器?

database - 开源是否曾经创建过一个自动处理事务的单一文件数据库?

database - PostgreSQL:无法在规则的内部查询中引用 NEW

mysql - 数据库设计 : to EAV or not to EAV?