为什么 Postgres 中的 now() 和 now()::timestamp 对于 CET 和 CEST 时区如此“错误”?

iti*_*nce 3 postgresql

我尝试理解PostgresTIMESTAMP WITH TIMEZONE中使用的一些查询示例。

\n

给出了一些将now结果转换为时区“CET”和“CEST”的时间戳的查询。“CEST”比 CET(夏令时间)早 1 小时

\n

例子:

\n
SELECT 1, now()\nUNION\nSELECT 2, now()::timestamp\nUNION\nSELECT 3, now() AT TIME ZONE \'CET\'\nUNION\nSELECT 4, now()::timestamp AT TIME ZONE \'CET\'\nUNION\nSELECT 5, now() AT TIME ZONE \'CEST\'\nUNION\nSELECT 6, now()::timestamp AT TIME ZONE \'CEST\'\n;\n
Run Code Online (Sandbox Code Playgroud)\n

结果:

\n
    1, 2023-09-10 17:07:10.524389 +00:00\n    2, 2023-09-10 17:07:10.524389 +00:00\n    3, 2023-09-10 18:07:10.524389 +00:00\n    4, 2023-09-10 16:07:10.524389 +00:00\n    5, 2023-09-10 19:07:10.524389 +00:00\n    6, 2023-09-10 15:07:10.524389 +00:00\n
Run Code Online (Sandbox Code Playgroud)\n

所以,之间的区别

\n
SELECT 5, now() AT TIME ZONE \'CEST\'\n
Run Code Online (Sandbox Code Playgroud)\n

\xe2\x80\xa6 和:

\n
SELECT 6, now()::timestamp AT TIME ZONE \'CEST\'\n
Run Code Online (Sandbox Code Playgroud)\n

\xe2\x80\xa6 是 4 小时。为什么?

\n

和...之间的不同

\n
SELECT 3, now() AT TIME ZONE \'CET\'\n
Run Code Online (Sandbox Code Playgroud)\n

\xe2\x80\xa6 和:

\n
SELECT 4, now()::timestamp AT TIME ZONE \'CET\'\n
Run Code Online (Sandbox Code Playgroud)\n

\xe2\x80\xa6 是 2 小时。我也不明白。

\n

我的 Postgres-Container 的当前时区是 UTC:

\n
SELECT current_setting(\'TIMEZONE\');\n\n> UTC\n
Run Code Online (Sandbox Code Playgroud)\n

有人可以解释一下吗?

\n

Bas*_*que 5

太长了;博士

\n
    \n
  • 避开UNION这里。由于TIMESTAMP WITH TIME ZONE 和类型的混合,会导致从第二个类型到第一个类型的隐式转换。TIMESTAMP WITHOUT TIME ZONEUNION
  • \n
  • 您使用了错误的类型 ,TIMESTAMP WITHOUT TIME ZONE而您应该只使用TIMESTAMP WITH TIME ZONE
  • \n
  • 调用在和类型AT TIME ZONE之间来回翻转。TIMESTAMP WITH TIME ZONETIMESTAMP WITHOUT TIME ZONE
  • \n
  • 您使用的伪时区 ( CET/ CEST) 仅指示夏令时是否有效。这些伪时区可以指多个实时时区中的任何一个。
  • \n
  • 不幸的是, psql控制台应用程序将默认时区应用于TIMESTAMP WITH TIME ZONE实际上始终采用 UTC 格式的值(偏移量为零)。
  • \n
\n

错误类型:TIMESTAMP WITHOUT TIME ZONE

\n

您使用了错误的数据类型。TIMESTAMP是 per the SQL standard 的缩写TIMESTAMP WITHOUT TIME ZONE

\n

该类型没有时区或与 UTC 的偏移量的概念。所以它不能代表一个时刻,时间轴上的一个特定点。该类型仅代表一个含糊不清的日期和时间,仅此而已。如果你告诉我“2024 年 1 月 23 日中午”,我怎么知道你指的是东京的中午、图卢兹的中午,还是俄亥俄州托莱多的中午 \xe2\x80\x94\xc2\xa0 三个截然不同的时刻,相隔几个小时。

\n

请参阅Postgres 文档

\n

正确类型:TIMESTAMP WITH TIME ZONE

\n

您应该使用另一种类型, TIMESTAMP WITH TIME ZONE. 这种类型确实具有偏移量的概念,始终以 UTC 格式存储提交的值(距 UTC 时间子午线的零时-分-秒偏移量)。

\n

提交值附带的任何偏移量或区域信息都用于调整为 UTC,然后被丢弃。

\n

如果您想保留有关该原始区域的信息,则需要将其记录为第二列中的文本。

\n

您可以缩写TIMESTAMP WITH TIME ZONETIMESTAMPTZ, 作为 Postgres 特定的功能。就我个人而言,我总是用全名拼写这两种类型,以避免误读造成的混乱。

\n

请注意,这种存储在 UTC 中的行为是 Postgres 团队的策略选择。其他一些数据库引擎也以同样的方式工作;其他人则不然。始终研究文档。遗憾的是,SQL 标准缺乏对日期时间处理行为的指定。

\n

小心工具

\n

不幸的是,一些数据库访问工具和中间件可能会选择 \xe2\x9a\xa0\xef\xb8\x8f 将一些默认时区注入到从 Postgres 检索的 UTC 值上。虽然本意是好的,但这种反功能会造成该区域已被存储的错误错觉。pgAdmin就是这样的工具之一。

\n

我认为数据访问工具应该始终\xe2\x80\x9c 说出真相\xe2\x80\x9d,而不是篡改检索结果。但那些工具的创造者并没有问我。

\n

所以请理解 Postgres 中的:任何和所有TIMESTAMP WITH TIME ZONE值都以零偏移量存储,并以零偏移量检索。您看到的任何其他区域或偏移都被干扰工具覆盖。

\n

实时时区

\n

CETCEST不是实时时区。

\n

此类伪区仅用于向用户呈现,而绝不能用于数据存储或数据交换。

\n

实时区域的名称格式为Continent/Region. 举几个例子:

\n
    \n
  • Europe/Paris
  • \n
  • Europe/Berlin
  • \n
  • Europe/Stockholm
  • \n
  • Europe/Warsaw
  • \n
\n

你的例子

\n

让我们看看您的示例代码。

\n
SELECT 1, now()\nUNION\nSELECT 2, now()::timestamp\nUNION\nSELECT 3, now() AT TIME ZONE \'CET\'\nUNION\nSELECT 4, now()::timestamp AT TIME ZONE \'CET\'\nUNION\nSELECT 5, now() AT TIME ZONE \'CEST\'\nUNION\nSELECT 6, now()::timestamp AT TIME ZONE \'CEST\'\n;\n
Run Code Online (Sandbox Code Playgroud)\n

根据 Postres 15.4日期时间函数日期时间类型的文档\xe2\x80\xa6

\n

第一个,now()返回一个timestamp with time zone值。我们可以通过将会话设置为 UTC 偏移量 0 来尝试。

\n
set timezone=\'UTC\' ;\n
Run Code Online (Sandbox Code Playgroud)\n

验证:

\n
show timezone ;\n
Run Code Online (Sandbox Code Playgroud)\n

调用该now函数。

\n
             now              \n------------------------------\n 2023-09-11 01:13:03.64721+00\n(1 row)\n
Run Code Online (Sandbox Code Playgroud)\n

我们看到偏移量为+00,这意味着与 UTC 的偏移量为零时-分-秒。这是格林威治皇家天文台牧羊人门时钟上看到的时间(好吧,在一秒钟左右的时间内,我们不会在这里讨论 GMT 与 UTC,这与实际业务环境无关)。

\n

将我们会话的默认时区设置为柏林时间。

\n
set timezone=\'Europe/Berlin\' ;\n
Run Code Online (Sandbox Code Playgroud)\n

SELECT now() ;

\n
2023-09-11 03:19:46.171143+02\n
Run Code Online (Sandbox Code Playgroud)\n

我们看到现在的时间0301上面看到的早了两个小时。这符合+02. 这两个结果代表同一时刻,时间轴上的同一点(或者如果我打字速度更快的话就会有)。他们的挂钟时间不同,就像冰岛的某人给柏林的某人打电话一样,两人同时抬头看墙上各自的时钟\xe2\x80\x94,一个人看到凌晨 1 点,另一个人看到凌晨 3 点。

\n

注意:我们现在看到psql具有在生成要在控制台上显示的文本时应用默认时区的反功能。Postgres 中的值TIMESTAMP WITHOUT TIME ZONE始终采用 UTC 格式。因此,我们不能完全信任psql的输出,因为该工具向我们显示的内容并不完全是数据库引擎生成的内容。

\n

相反,如果您在 Java 中通过 JDBC 使用类似 的调用检索该值myResultSet.getObject( \xe2\x80\xa6 , OffsetDateTime.class ),您将始终获得偏移量为零的值。

\n

你的下一行:

\n
SELECT 2, now()::timestamp \n
Run Code Online (Sandbox Code Playgroud)\n

\xe2\x80\xa6 使用了错误的类型,TIMESTAMP WITHOUT TIME ZONE。您正在丢弃有价值的信息,即与 UTC 的偏移量,而没有获得任何回报。该行应该是:

\n
SELECT 2, now()::TIMESTAMP WITH TIME ZONE  \n
Run Code Online (Sandbox Code Playgroud)\n

但这很愚蠢,因为你TIMESTAMP WITHOUT TIME ZONE手上已经有了一个值。演员阵容毫无意义,没有增加任何价值。

\n

你的第三行:

\n
SELECT 3, now() AT TIME ZONE \'CET\'\n
Run Code Online (Sandbox Code Playgroud)\n

\xe2\x80\xa6 使用伪时区。那应该使用实时时区名称。像这样的东西:

\n
SELECT 3, now() AT TIME ZONE \'Europe/Berlin\'\n
Run Code Online (Sandbox Code Playgroud)\n

但你必须小心操作员AT TIME ZONEtimestamp with time zone该运算符在和类型之间来回翻转输入的类型timestamp without time zone

\n
    \n
  • 如果你对一个timestamp without time zone值进行操作,你就会得到一个timestamp with time zone值。
  • \n
  • 反之亦然,对 a 进行操作timestamp with time zone会返回 a timestamp without time zone
  • \n
\n

添加或删除偏移量时,会根据您指定的时区对时间和日期进行调整。

\n

所以这一行:

\n
SELECT now() AT TIME ZONE \'Europe/Berlin\' ;\n
Run Code Online (Sandbox Code Playgroud)\n

\xe2\x80\xa6 为您提供柏林地区的日期和时间,而不考虑我们数据库会话的默认时区。

\n

因此,在我们的 UTC 凌晨 1 点示例场景中,上面的行将始终返回下面带有凌晨 3 点时间的文本,无论默认时区是 UTC、欧洲/柏林还是亚洲/东京。

\n
         timezone          \n---------------------------\n 2023-09-11 03:39:19.67042\n(1 row)\n
Run Code Online (Sandbox Code Playgroud)\n

但请注意,在上面的行中,我们丢失了偏移指示。这是因为我们调用AT TIME ZONE对 a 进行操作timestamp with time zone,并返回 a timestamp without time zone,在进行日期时间调整后丢失了偏移量内容。

\n

您的第四行SELECT 4, now()::timestamp AT TIME ZONE \'CET\'重复了前面几行的两个错误:(a)将 a 转换timestamp with time zone为 atimestamp without time zone从而丢失偏移信息,以及(b)使用伪时区。所以我们可以删除这一行,因为它不会加深我们的理解。

\n

您的第五行,SELECT 5, now() AT TIME ZONE \'CEST\',使用不同的伪时区,CEST而不是CET。它们之间的区别在于S指示“夏令时”的方式,即遵守夏令时(DST) 。这是这些伪区域的问题之一:人们经常混淆 DST 和非 DST 对应区域并使用错误的区域。再次强调,我们不应该在编程中使用这些伪区域。所以我们可以放弃你的这一行。

\n

您的第六行SELECT 6, now()::timestamp AT TIME ZONE \'CEST\'也可以忽略。同样,这一行进行了不恰当的转换,并且不恰当地使用了伪区域。

\n

通过使用实时时区名称(例如 )Europe/Berlin,而不是伪时区(例如CET/ CEST),我们让软件确定 DST 在给定时刻是否有效。Postgres 有自己的嵌入的tzdata副本。只要该文件是最新的,那么我们就可以依靠 Postgres 来确定Europe/Berlin.

\n

因此,我们可以将示例代码简化为示例代码中唯一有用/合理的行(第一行和第三行)的修改版本:

\n
SET timezone=\'UTC\' \n;\nSELECT 1 AS RowNo , now() \n;\nSELECT 3 as RowNo, now() AT TIME ZONE \'Europe/Berlin\' \n;\n
Run Code Online (Sandbox Code Playgroud)\n

结果:

\n
SET\n rowno |              now              \n-------+-------------------------------\n     1 | 2023-09-11 02:05:53.188829+00\n(1 row)\n\n rowno |          timezone          \n-------+----------------------------\n     3 | 2023-09-11 04:05:53.189836\n(1 row)\n
Run Code Online (Sandbox Code Playgroud)\n

混合类型的隐式铸造UNION

\n

我们要考虑的另一个问题是jjanes 在评论中提出的。使用UNION组合 SELECT 查询会改变结果

\n

请注意,在上面直接显示的结果中,#3 没有偏移指示器。这意味着结果是一个TIMESTAMP WITHOUT TIME ZONE值。但 #1确实有一个偏移指示符,这意味着该项目是一个TIMESTAMP WITH TIME ZONE值。因此,我们有两个不同的单独SELECT语句,其结果具有不同的数据类型。

\n

让我们尝试一下问题中看到的方法UNION。我们将分号替换为UNION.

\n
SET timezone=\'UTC\' \n;\nSELECT 1 AS RowNo , now() \nUNION\nSELECT 3 as RowNo, now() AT TIME ZONE \'Europe/Berlin\' \n;\n
Run Code Online (Sandbox Code Playgroud)\n

哇!这些结果与上面的结果不匹配。在这里我们看到 #1 和 #3上都有一个偏移指示器。所以现在 #3 是一个TIMESTAMP WITH TIME ZONE值。为什么?行间列的类型必须相同。如何?Postgres 隐式地将TIMESTAMP WITHOUT TIME ZONE值转换为一个 TIMESTAMP WITH TIME ZONE值,在上一个示例中没有应用偏移量。

\n
SET\n rowno |              now              \n-------+-------------------------------\n     1 | 2023-09-11 02:10:13.934363+00\n     3 | 2023-09-11 04:10:13.934363+00\n(2 rows)\n
Run Code Online (Sandbox Code Playgroud)\n

这里要理解的关键是日期时间函数返回日期时间值,而不仅仅是字符串。(然后psql应用程序根据这些键入的值生成文本以供显示。)

\n

为了解决类型混合问题,Postgres 尝试通过强制转换来提供帮助。因此,最终,UNION此代码的版本会产生与非UNION版本在语义上不同的结果。

\n

这意味着问题的代码示例对于尝试了解 Postgres 的日期时间处理来说更加没有用处。对于我们的研究,我们必须将这些示例语句分开,不带UNION.

\n
\n

顺便说一句,时区与 UTC 偏移量:

\n
    \n
  • 偏移量只是 UTC 时间子午线之前或之后的小时数、分钟数、秒数。
  • \n
  • 时区的意义要大得多。时区是特定地区人民所使用的偏移量的过去、现在和未来变化的命名历史,由其政治家决定。
  • \n
\n