没有时区、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" 中进行计算。我用 ::TIMESTAMTZ
和 AT TIME ZONE
none 尝试了一些方法,其中一些有效。
附带说明:根据 PostgreSQL 文档,+ INTERVAL
处理“1 天”间隔与“24 小时”间隔对于 DST 的处理方式不同。添加天数会忽略 DST,因此 10:00 始终保持 10:00。另一方面,当增加小时数时,10:00 可能会变成 09:00 或 11:00 如果您以某种方式跨越 DST 边界。
非常感谢任何提示!
我想你有两个避免头痛的策略:
- 让 Rails 处理所有与时区有关的事情,这样 Postgres 就完全不需要了
或
- 让 Postgres 处理所有与时区有关的事情,所以 Rails 根本不需要
将两者混合在一起总是很痛苦,这基本上就是导致您现在出现问题的原因。我会选择策略 1(让 Rails 处理它)。为此,您的 Postgres 数据库应以 UTC 格式存储开始时间和结束时间。 duration
可能仍然在您的用户界面中,但是如果用户输入开始时间和持续时间,那么您应该计算结束时间,并将该结束时间存储在您的数据库中。用户输入的开始时间和您在应用程序中计算的结束时间,两者都是时区特定的,您只需让 Rails 在保存到数据库时处理到 UTC 的转换。
您的查询将很简单:
SELECT * FROM projects WHERE finished_at < NOW()
(顺便说一句,您也可以将持续时间存储在数据库中,但这是多余的,因为它可以根据开始时间和结束时间计算)
我创建了一个函数,它通过将 duration
天添加到 started_at
来计算 ended_at
,以纪念给定时区的 DST 更改。然而,started_at
和 ended_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
我正在开发一个 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" 中进行计算。我用 ::TIMESTAMTZ
和 AT TIME ZONE
none 尝试了一些方法,其中一些有效。
附带说明:根据 PostgreSQL 文档,+ INTERVAL
处理“1 天”间隔与“24 小时”间隔对于 DST 的处理方式不同。添加天数会忽略 DST,因此 10:00 始终保持 10:00。另一方面,当增加小时数时,10:00 可能会变成 09:00 或 11:00 如果您以某种方式跨越 DST 边界。
非常感谢任何提示!
我想你有两个避免头痛的策略:
- 让 Rails 处理所有与时区有关的事情,这样 Postgres 就完全不需要了
或
- 让 Postgres 处理所有与时区有关的事情,所以 Rails 根本不需要
将两者混合在一起总是很痛苦,这基本上就是导致您现在出现问题的原因。我会选择策略 1(让 Rails 处理它)。为此,您的 Postgres 数据库应以 UTC 格式存储开始时间和结束时间。 duration
可能仍然在您的用户界面中,但是如果用户输入开始时间和持续时间,那么您应该计算结束时间,并将该结束时间存储在您的数据库中。用户输入的开始时间和您在应用程序中计算的结束时间,两者都是时区特定的,您只需让 Rails 在保存到数据库时处理到 UTC 的转换。
您的查询将很简单:
SELECT * FROM projects WHERE finished_at < NOW()
(顺便说一句,您也可以将持续时间存储在数据库中,但这是多余的,因为它可以根据开始时间和结束时间计算)
我创建了一个函数,它通过将 duration
天添加到 started_at
来计算 ended_at
,以纪念给定时区的 DST 更改。然而,started_at
和 ended_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