【问题标题】:TIMESTAMP WITHOUT TIME ZONE, INTERVAL and DST extravaganza没有时区、间隔和夏令时的时间戳
【发布时间】:2015-07-31 21:01:28
【问题描述】:

我正在开发一个 Rails 应用程序,它将所有日期存储到 PostgreSQL 为“TIMESTAMP WITHOUT TIME ZONE”。 (Rails 处理应用程序层的时区,对于这个应用程序来说是“欧洲/柏林”。)不幸的是,夏令时 (DST) 成为一个问题。

简化的“项目”表包含以下列:

started_at TIMESTAMP WITHOUT TIME ZONE
duration INTEGER

项目从started_at 开始并运行duration 天。

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

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

应于 2015 年 6 月 30 日 10:00(欧洲/柏林)结束。但现在是夏天,所以 DST 适用,“欧洲/柏林”的 10:00 现在是 UTC 的 08:00。

因此,对于跨 DST 边界开始/结束的项目,使用以下查询查找所有持续时间已过的项目不起作用:

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

我想如果上面的 WHERE 在时区“欧洲/柏林”而不是“UTC”中进行计算,那将是最好的。我用::TIMESTAMTZ 和AT TIME ZONE 尝试了一些方法,但都没有奏效。

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

非常感谢任何提示!

【问题讨论】:

    标签: ruby-on-rails postgresql intervals dst postgresql-9.4


    【解决方案1】:

    我认为您有两种避免头痛的策略:

    1. 让 Rails 处理与时区有关的所有事情,因此 Postgres 根本不必这样做

    或

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

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

    您的查询将很简单:

    SELECT * FROM projects WHERE finished_at < NOW()
    

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

    【讨论】:

    • 这当然是我的计划 B。但是在数据库层上,是否有可能采用 WITHOUT TIME ZONE 时间,分配隐式时区 UTC,将其转换为“Europe/Berlin” ,添加间隔,然后将其转换回UTC?
    • TBH 我不确定这是否可能。但我的经验是,这些类型的计算在 SQL 之外更容易处理,即使它们是可能的。
    • 我终于找到了一种在数据库层进行数学运算的方法。但是,无论如何,我都会支持你的答案,因为对于一个新应用程序,它肯定是存储 started_at 和 ended_at 而不是 duration 然后在应用层上进行所有数学运算的有效替代方案。
    【解决方案2】:

    我创建了一个函数,它通过将 duration 天数添加到 started_at 来计算 ended_at,以遵守给定时区的 DST 变化。但是,started_at 和 ended_at 都是 UTC,因此可以很好地与 Rails 配合使用。

    它将started_at(没有时区的时间戳,Rails 的隐式 UTC)转换为具有时区 UTC 的时间戳,然后到给定的时区,添加 duration 并返回没有时区的时间戳(隐式 UTC) .

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

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

    SELECT ended_at(started_at, duration) FROM projects
    

    【讨论】:

      猜你喜欢
      • 2019-09-06
      • 2011-10-22
      • 1970-01-01
      • 2013-01-31
      • 2018-03-04
      • 1970-01-01
      • 2012-04-07
      • 2021-10-18
      • 2011-12-14
      相关资源
      最近更新 更多