一尘不染

完全忽略Rails和PostgreSQL中的时区

Rails

我正在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)

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


阅读 436

收藏
2021-04-15

共1个答案

一尘不染

数据类型timestamptz是的简称timestamp with time zone。
另一个选择timestamp是的缩写timestamp without time zone。

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

在PostgreSQL中生成两个日期之间的时间序列
内部存储和时代
在内部,时间戳占用磁盘和RAM上8个字节的存储空间。它是一个整数值,表示距Postgres纪元2000-01-01 00:00:00 UTC的微秒数。

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

源代码:

* Timestamps, as well as the h/m/s fields of intervals, are stored as
* int64 values with units of microseconds.  (Once upon a time they were  
* double values with units of seconds.)

和:

/* Julian-date equivalents of Day 0 in Unix and Postgres reckoning */  
#define UNIX_EPOCH_JDATE        2440588 /* == date2j(1970, 1, 1) */  
#define POSTGRES_EPOCH_JDATE    2451545 /* == date2j(2000, 1, 1) */  

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

timestamp

键入为的值timestamp告诉Postgres没有显式提供任何时区。假设当前时区。Postgres会错误地忽略添加到输入文字中的任何时区修饰符!

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

timestamptz

的处理timestamptz方式略有不同。我在这里引用手册:

对于timestamp with time zone,内部存储的值始终以UTC(Universal Coordinated Time …)为单位。

大胆强调我的。该时区本身永远不会保存。它是一个输入修饰符,用于计算相应的UTC时间戳,该修饰符被存储-或输出修饰符用于计算显示的本地时间-带有附加的时区偏移量。如果您没有timestamptz在输入上附加偏移量,则假定会话的当前时区设置。所有计算都是使用UTC时间戳值完成的。如果您必须(或可能必须)处理多个时区,请使用timestamptz。换句话说:如果对假设的时区可能有任何疑问或误解,请选择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。在我的测试中,查询结果显示了当前时区设置的Vienna / Austria,+1在冬季和+2夏季都有偏移(“夏令时”,DST)。因此2012-03-05 18:00:00+01,DST仅在以后才生效。

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。第二个转换timestamptz到timestamp在给定的时间区“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
2021-04-15