0

我正在开发一个 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”中进行计算,那将是最好的。我已经尝试了一些东西,::TIMESTAMTZAT TIME ZONE都没有奏效。

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

非常感谢任何提示!

4

2 回答 2

1

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

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

或者

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

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

您的查询将很简单:

SELECT * FROM projects WHERE finished_at < NOW()

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

于 2015-05-19T22:29:55.507 回答
0

我创建了一个函数,该函数ended_at通过添加duration天数来计算started_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
于 2015-05-28T12:50:52.210 回答