没有时区、INTERVAL 和 DST 的时间戳

TIMESTAMP WITHOUT TIME ZONE, INTERVAL and DST extravaganza

我正在开发一个 Rails 应用程序,它以 "TIMESTAMP WITHOUT TIME ZONE" 的形式将所有日期存储到 PostgreSQL。 (Rails 处理应用程序层上的时区,对于此应用程序是 "Europe/Berlin"。)不幸的是,夏令时 (DST) 成为一个问题。

简化后的"projects"table有以下列:

started_at TIMESTAMP WITHOUT TIME ZONE
duration INTEGER

项目在 started_at 和 运行 开始 duration 天。

现在,假设只有一个项目于 2015 年 1 月 1 日 10:00 开始。由于现在是 "Europe/Berlin" 并且是一月(没有夏令时),因此数据库中的记录如下所示:

SET TimeZone = 'UTC';
SELECT started_at from projects;
# => 2015-01-01 09:00:00

它应该在 2015-06-30 10:00 (Europe/Berlin) 结束。但现在是夏天,所以 DST 适用并且 "Europe/Berlin" 中的 10:00 现在是 UTC 中的 08:00。

因此,使用以下查询查找持续时间已过的所有项目不适用于 start/end 跨 DST 边界的项目:

SELECT * FROM projects WHERE started_at + INTERVAL '1 day' * duration < NOW()

我想最好是上面的 WHERE 在时区 "Europe/Berlin" 而不是 "UTC" 中进行计算。我用 ::TIMESTAMTZAT TIME ZONE none 尝试了一些方法,其中一些有效。

附带说明:根据 PostgreSQL 文档,+ INTERVAL 处理“1 天”间隔与“24 小时”间隔对于 DST 的处理方式不同。添加天数会忽略 DST,因此 10:00 始终保持 10:00。另一方面,当增加小时数时,10:00 可能会变成 09:00 或 11:00 如果您以某种方式跨越 DST 边界。

非常感谢任何提示!

我想你有两个避免头痛的策略:

  1. 让 Rails 处理所有与时区有关的事情,这样 Postgres 就完全不需要了

  1. 让 Postgres 处理所有与时区有关的事情,所以 Rails 根本不需要

将两者混合在一起总是很痛苦,这基本上就是导致您现在出现问题的原因。我会选择策略 1(让 Rails 处理它)。为此,您的 Postgres 数据库应以 UTC 格式存储开始时间和结束时间。 duration 可能仍然在您的用户界面中,但是如果用户输入开始时间和持续时间,那么您应该计算结束时间,并将该结束时间存储在您的数据库中。用户输入的开始时间和您在应用程序中计算的结束时间,两者都是时区特定的,您只需让 Rails 在保存到数据库时处理到 UTC 的转换。

您的查询将很简单:

SELECT * FROM projects WHERE finished_at < NOW()

(顺便说一句,您也可以将持续时间存储在数据库中,但这是多余的,因为它可以根据开始时间和结束时间计算)

我创建了一个函数,它通过将 duration 天添加到 started_at 来计算 ended_at,以纪念给定时区的 DST 更改。然而,started_atended_at 都在 UTC 中,因此与 Rails.

配合得很好

它将started_at(没有时区的时间戳,Rails隐含的UTC)转换为带有时区UTC的时间戳,然后到给定的时区,添加duration和returns 没有时区的时间戳(隐式 UTC)。

# ended_at(started_at, duration, time_zone)
CREATE FUNCTION ended_at(timestamp, integer, text = 'Europe/Zurich') RETURNS timestamp AS $$
  SELECT ((::timestamp AT TIME ZONE 'UTC' AT TIME ZONE  + INTERVAL '1 day' * ) AT TIME ZONE )::timestamp
$$  LANGUAGE SQL IMMUTABLE SET search_path = public, pg_temp;

使用此功能,我可以省略必须添加 ended_at 作为必须保持同步的显式列。而且它易于使用:

SELECT ended_at(started_at, duration) FROM projects