我正在开发一个Rails应用程序,它将PostgreSQL的所有日期存储为“TIMESTAMP WITHOUT TIME ZONE”。 (Rails处理应用程序层的时区,对于此应用程序来说是“欧洲/柏林”。)不幸的是,夏令时(DST)成为一个问题。
简化的“项目”表格包含以下列:
started_at TIMESTAMP WITHOUT TIME ZONE
duration INTEGER
项目从started_at
开始,运行duration
天。
现在,假设只有一个项目于2015年1月1日10点开始。由于这是“欧洲/柏林”而且它是1月(没有DST),因此数据库中的记录如下:
SET TimeZone = 'UTC';
SELECT started_at from projects;
# => 2015-01-01 09:00:00
应于2015-06-30 10:00(欧洲/柏林)结束。但现在是夏天了,所以夏令时适用,“欧洲/柏林”的10:00现在是UTC时间的08:00。
因此,使用以下查询查找持续时间已过的所有项目对于跨DST边界开始/结束的项目不起作用:
SELECT * FROM projects WHERE started_at + INTERVAL '1 day' * duration < NOW()
我想最好是上面的WHERE在时区“Europe / Berlin”而不是“UTC”进行计算。我已尝试使用::TIMESTAMTZ
和AT TIME ZONE
进行了一些操作。
作为旁注:根据PostgreSQL文档,+ INTERVAL
应该处理“1天”的时间间隔与“夏令时”的“24小时”时间间隔不同。添加天数会忽略DST,因此10:00始终保持10:00。另一方面,如果你越过DST边界,10:00可能会在09:00或11:00增加时间。
非常感谢任何提示!
答案 0 :(得分:1)
我认为你有两种避免头痛的策略:
或
将两者混合在一起总是很痛苦,而且现在基本上是造成问题的原因。我会选择策略1(让Rails处理它)。为此,Postgres数据库应以UTC格式存储开始时间和结束时间。 duration
可能仍然是您用户界面中的内容,但如果用户输入了开始时间和持续时间,那么您应该计算完成时间,并将完成时间存储在数据库中。用户输入的开始时间以及您在应用中计算的结束时间,两者都是特定于时区的,您只需让Rails在保存到数据库时处理转换为UTC。
您的查询将是简单的:
SELECT * FROM projects WHERE finished_at < NOW()
(顺便说一句,你也可以在数据库中存储持续时间,但它是多余的,因为它可以从开始时间和结束时间计算)
答案 1 :(得分:0)
我创建了一个函数,通过将ended_at
天添加到duration
来计算给定时区的DST更改来计算started_at
。但是,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