在Rails和PostgreSQL中完全忽略时区

99m*_*les 156 postgresql timezone datetime ruby-on-rails ruby-on-rails-3

我正在处理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 
Run Code Online (Sandbox Code Playgroud)

因为数据库中的记录似乎是以UTC形式出现的,我的黑客是用当前时间,用'Date.strptime(str,"%m /%d /%Y")删除时区,然后做我的查询:

.where("time >= ?", date_start)
Run Code Online (Sandbox Code Playgroud)

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

Erw*_*ter 334

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

timestamptz是字面上日期/时间系列中的首选类型.它已经typispreferred开始了pg_type,这可能是相关的:

时代

在内部,时间戳存储为来自纪元的计数.Postgres使用UTC中2000年第一天第一时刻的纪元,即2000-01-01T00:00:00Z.八个八位字节用于存储计数.根据编译时选项,该数字是:

  • 一个8字节的整数(默认值),小数秒的0到6位数
  • 浮点数(不建议使用),小数秒的0到10位数,其中精度会在远离纪元的情况下快速降低.
    现代Postgres安装使用8字节整数.

需要注意的是Postgres并没有使用Unix时间.Postgres的时代是2000-01-01的第一时刻,而不是Unix'1970-01-01.虽然Unix时间的分辨率为整秒,但Postgres会保留几秒钟.

timestamp

如果你定义一个数据类型,你告诉Postgres:"我没有明确提供时区,而是假设当前的时区.Postgres按原样保存时间戳- 忽略时区修饰符,如果你应该添加一个!timestamp [without time zone]

当你稍后显示它时timestamp,你会得到你输入的字面意思.使用相同的时区设置一切都很好.如果会话的时区设置发生更改,则其含义也会发生变化timestamp- 保持不变.

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';
      timestamptz
------------------------
 2012-03-05 18:00:00+01

大胆强调我的.这里发生了什么?
+3为输入文字选择了一个任意时区偏移量.对于Postgres,这只是输入UTC时间戳的众多方法之一2012-03-05 17:00:00.在我的测试中显示当前时区设置维也纳/奥地利的查询结果,其+1在冬季和+2夏季时间有偏移:2012-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'
Run Code Online (Sandbox Code Playgroud)

......相当于:

SELECT timestamptz '2012-03-05 17:00:00 UTC'
Run Code Online (Sandbox Code Playgroud)

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

SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC' AT TIME ZONE 'EST'
Run Code Online (Sandbox Code Playgroud)

那是对的,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);
Run Code Online (Sandbox Code Playgroud)

返回8(或9)个相同的行,其中timestamptz列保存相同的UTC时间戳2012-03-05 17:00:00.第9排恰好在我的时区工作,但却是一个邪恶的陷阱.见下文.

① 夏威夷时间的时区名称和时区缩写的第 6 - 8行受夏令时(夏令时)的限制,可能会有所不同,但目前不同.时区名称就像'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
Run Code Online (Sandbox Code Playgroud)

根据当地时钟的时间:

SELECT * FROM tbl WHERE time_col > now()::time
Run Code Online (Sandbox Code Playgroud)

不厌倦了背景信息呢?手册中还有更多内容.

  • @ErwinBrandstetter这是一个很好的答案,除了一个严重的缺陷.正如harmic评论的那样,Postgres不会*使用Unix时间.根据[doc](http://www.postgresql.org/docs/current/interactive/datatype-datetime.html#DATATYPE-TIMEZONES):( a)时代是2001-01-01而不是Unix'1970 -01-01,和(b)虽然Unix时间的分辨率为整秒,但Postgres会保留几秒钟.小数位数取决于编译时选项:使用8字节整数存储(默认)时为0到6,使用浮点存储(不建议使用)时为0到10. (4认同)
  • 小细节,但我认为时间戳在内部存储为自2000-01-01以来的微秒数 - 请参阅[日期/时间数据类型](http://www.postgresql.org/docs/9.2/static/datatype-datetime. html)手册部分.我对源头的检查似乎证实了这一点.奇怪的是,为时代使用不同的起源! (2认同)
  • @harmic至于不同的时代......其实并不那么奇怪.这个[维基百科页面](https://en.wikipedia.org/wiki/Epoch_%28reference_date%29)列出了各种计算机系统使用的二十多个时期.虽然[Unix纪元](https://en.wikipedia.org/wiki/Unix_time)很常见,但它不是唯一的. (2认同)
  • @BasilBourque:[我知道这个不幸的错误。](http://stackoverflow.com/questions/10462059/migrate-datetime-w-timezone-in-postgresql-to-utc-timezone-to-use-django- 1-4 / 10462428#10462428)如果您不介意的话,欢迎您对其进行编辑。过去,我已经看到您的一些答案,并且您很擅长。我再进行一次编辑将迫使它进入社区Wiki-随着时间的流逝,我付出了很多努力(和编辑)以使其清晰,全面。 (2认同)
  • **更正:**在我之前的评论中,我错误地将Postgres时代引用为2001年.实际上它是******. (2认同)
  • @BasilBourque:您可能对[相关的最新政策变更](http://meta.stackexchange.com/questions/227290/stop-using-community-wiki-as-a-reputation-denial-mechanism/ 228940#228940)。现在,反直觉转换为社区Wiki的历史已经过去。 (2认同)