在Rails和PostgreSQL中完全忽略时区


164

我正在Rails和Postgres中处理日期和时间,并遇到了这个问题:

该数据库位于UTC中。

用户在Rails应用程序中设置选择的时区,但这仅在获取用户本地时间以比较时间时才使用。

用户存储时间,例如2012年3月17日晚上7点。我不希望时区转换或时区被存储。我只希望保存该日期和时间。这样,如果用户更改了时区,它将仍然显示2012年3月17日晚上7点。

我仅使用用户指定的时区来获取用户本地时区中当前时间“之前”或“之后”的记录。

我目前正在使用“没有时区的时间戳”,但是当我检索记录时,rails(?)会将它们转换为应用程序中的时区,这是我所不希望的。

Appointment.first.time
 => Fri, 02 Mar 2012 19:00:00 UTC +00:00 

因为数据库中的记录似乎是UTC格式的,所以我的建议是获取当前时间,并使用'Date.strptime(str,“%m /%d /%Y”)'删除时区,然后执行查询:

.where("time >= ?", date_start)

似乎必须有一种更简便的方法来忽略周围的时区。有任何想法吗?

Answers:


347

数据类型timestamp是的简称timestamp without time zone
另一种选择timestamptztimestamp with time zone

timestamptz从字面上看,是日期/时间系列中的首选类型。它已typispreferred设置为pg_type,可能与以下内容有关:

内部存储和时代

在内部,时间戳占用磁盘和RAM上8个字节的存储空间。它是一个整数值,表示距Postgres纪元2000-01-01 00:00:00 UTC的微秒数。

Postgres还具有从UNIX时代1970-01-01 00:00:00 UTC开始的常用UNIX计时秒数的内置知识,并将其用于函数to_timestamp(double precision)或中EXTRACT(EPOCH FROM timestamptz)

源代码:

*时间戳以及间隔的h / m / s字段存储为
* int64值,以微秒为单位。(曾几何时  
*以秒为单位的双精度值。)

和:

/ *在Unix和Postgres估算中,第0天的儒略日期等效* /  
#定义UNIX_EPOCH_JDATE 2440588 / * == date2j(1970,1,1)* /  
#定义POSTGRES_EPOCH_JDATE 2451545 / * == date2j(2000,1,1)* /  

微秒的分辨率最多可转换为6个小数位,持续几秒。

timestamp

键入为的值告诉Postgres没有明确提供时区。假设当前时区。Postgres会忽略错误添加的任何时区修饰符!timestamp [without time zone]

没有时间转移显示。使用相同的时区设置,一切都很好。对于不同的时区设置,含义会发生变化,但显示保持不变。

timestamptz

的处理timestamp with time zone略有不同。我在这里引用手册

对于timestamp with time zone,内部存储的值始终以UTC(全球标准时间...)表示

大胆强调我的。该时区本身永远不会保存。它是一个输入修饰符,用于计算相应的UTC时间戳,该时间戳被存储-或与输出修饰符一起用于计算要显示的本地时间-带有附加的时区偏移量。如果您没有timestamptz在输入上附加偏移量,则假定会话的当前时区设置。所有计算都是使用UTC时间戳值完成的。如果您必须(或可能必须)处理多个时区,请使用timestamptz

客户端(如psql或pgAdmin或通过libpq进行通信的任何应用程序(如带有pg gem的Ruby)会显示当前时区或根据请求的时区的时间戳加上偏移量(请参见下文)。它始终是同一时间点,只有显示格式有所不同。或者,如手册所述

所有可识别时区的日期和时间都内部存储在UTC中。在将 它们显示给客户端之前,它们会在TimeZone配置参数指定的区域中转换为本地时间。

考虑以下简单示例(在psql中):

db =#SELECT timestamptz'2012-03-05 20:00 +03 ';
      时间戳
------------------------
 2012-03-05 18:00:00 +01

大胆强调我的。这里发生了什么?
+3为输入文字选择了任意时区偏移量。对于Postgres来说,这只是输入UTC时间戳的多种方式之一2012-03-05 17:00:00。在我的测试中,查询的结果针对当前时区设置Vienna / Austria 显示,该位置在冬季和夏季时间具有偏移量:,因为它属于冬季时间。+1+22012-03-05 18:00:00+01

Postgres已经忘记了如何输入该值。它只记住值和数据类型。就像十进制数字一样。numeric '003.4'numeric '3.40'numeric '+3.4'-所有结果都具有完全相同的内部值。

AT TIME ZONE

一旦掌握了这种逻辑,就可以做任何您想做的事情。现在缺少的只是根据特定时区解释或表示时间戳文字的工具。这就是AT TIME ZONE构造的来源。有两种不同的用例。timestamptz转换为timestamp,反之亦然。

输入UTC timestamptz 2012-03-05 17:00:00+0

SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC'

...相当于:

SELECT timestamptz '2012-03-05 17:00:00 UTC'

要显示与EST timestamp(东部标准时间)相同的时间点:

SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC' AT TIME ZONE 'EST'

是的,AT TIME ZONE 'UTC' 两次。第一个将timestamp值解释为返回类型的(给定)UTC时间戳timestamptz。第二个转换timestamptztimestamp在给定的时间区“EST” -什么在这个时间点独特的时区EST显示时钟。

例子

SELECT ts AT TIME ZONE 'UTC'
FROM  (
   VALUES
      (1, timestamptz '2012-03-05 17:00:00+0')
    , (2, timestamptz '2012-03-05 18:00:00+1')
    , (3, timestamptz '2012-03-05 17:00:00 UTC')
    , (4, timestamp   '2012-03-05 11:00:00'  AT TIME ZONE '+6') 
    , (5, timestamp   '2012-03-05 17:00:00'  AT TIME ZONE 'UTC') 
    , (6, timestamp   '2012-03-05 07:00:00'  AT TIME ZONE 'US/Hawaii')  -- 
    , (7, timestamptz '2012-03-05 07:00:00 US/Hawaii')                  -- 
    , (8, timestamp   '2012-03-05 07:00:00'  AT TIME ZONE 'HST')        -- 
    , (9, timestamp   '2012-03-05 18:00:00+1')  --  loaded footgun!
      ) t(id, ts);

返回8(或9)个相同的行,其中的timestamptz列具有相同的UTC timestamp 2012-03-05 17:00:00。第9行碰巧在我所在的时区起作用,但这是一个陷阱。见下文。

①具有时区名称和时区缩写(夏威夷时间)的第6-8行受DST(夏令时)的影响,并且可能会有所不同,尽管当前没有变化。像这样的时区名称'US/Hawaii'会自动识别DST规则和所有历史性班次,而像的缩写HST则只是一个固定偏移量的哑码。您可能需要为夏季/标准时间附加一个不同的缩写。该名称可以正确解释给定时区的任何时间戳。缩写很便宜,但是对于给定的时间戳,它必须是正确的缩写

夏令时并不是人类提出过的最聪明的想法。

②第9行,标记为加载footgun的作品对我来说,只是巧合。如果将文字显式转换为timestamp [without time zone],则任何时区偏移都将被忽略!仅使用裸时间戳。然后,该值将timestamptz在示例中自动强制为与列类型匹配的值。对于此步骤,timezone假定当前会话的设置,+1在我的情况下(欧洲/维也纳)碰巧是同一时区。但对于您而言,可能并非如此-这将带来不同的价值。简而言之:不要将timestamptz文字转换为,timestamp否则会丢失时区偏移。

你的问题

用户存储时间,例如2012年3月17日晚上7点。我不希望时区转换或时区被存储。

时区本身从不存储。使用上述方法之一输入UTC时间戳。

我仅使用用户指定的时区来获取用户本地时区中当前时间“之前”或“之后”的记录。

您可以对不同时区的所有客户端使用一个查询。
对于绝对的全球时间:

SELECT * FROM tbl WHERE time_col > (now() AT TIME ZONE 'UTC')::time

对于根据本地时钟的时间:

SELECT * FROM tbl WHERE time_col > now()::time

还不厌倦背景信息吗?手册中还有更多内容。


2
次要细节,但我认为时间戳记是自2000年1 月1日以来在内部存储的微秒数-请参见手册的日期/时间数据类型部分。我自己对消息来源的检查似乎证实了这一点。奇怪地为时代使用了不同的起源!
2013年

2
@harmic至于不同的时代……其实并不奇怪。该Wikipedia页面列出了各种计算机系统使用的二十个纪元。尽管Unix时代很普遍,但它不是唯一的时代
罗勒·布尔克

4
@ErwinBrandstetter这是一个很好的答案,除了一个严重的缺陷。作为HARMIC评论,Postgres并没有使用Unix时间。根据文档:(a)纪元是2001-01-01,而不是Unix'1970-01-01,并且(b)虽然Unix时间的分辨率为整秒,但Postgres保留的时间只有几分之一秒。小数位数取决于编译时间选项:使用八字节整数存储(默认)时为0到6;使用浮点存储(不推荐使用)时为0到10。
罗勒·布尔克

2
@BasilBourque:我知道这个不幸的错误。如果您不介意,欢迎您对其进行编辑。过去,我已经看到了您的一些答案,并且您很擅长。我又进行了一次编辑,将其强制添加到社区Wiki-随着时间的推移,我付出了很多努力(和编辑)以使其清晰,全面。
Erwin Brandstetter 2014年

2
更正:在我之前的评论中,我错误地将Postgres时代称为2001。实际上是2000
罗勒·布尔克

1

如果您想默认使用UTC进行交易:

在中config/application.rb,添加:

config.time_zone = 'UTC'

然后,如果您存储的是当前用户的时区名称,则current_user.timezone可以说。

post.created_at.in_time_zone(current_user.timezone)

current_user.timezone应该是有效的时区名称,否则您将获得ArgumentError: Invalid Timezone,请参阅完整列表

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.