从铸造值中提取纪元时,PostgreSQL 9.3.5 与 8.3.6 的区别

PostgreSQL 9.3.5 difference from 8.3.6 when extracting epoch from casted value

我们有一个 table,其中包含作为字符串的时间戳,并且一直在使用 extract 在 PostgreSQL 8.3.6 服务器上检索它的纪元:

select '2015/01/07 14:00:00' as the_timestamp, 
extract(epoch from cast('2015/01/07 14:00:00' as timestamp)) as the_epoch;

    the_timestamp    | the_epoch
---------------------+------------
 2015/01/07 14:00:00 | 1420668000
(1 row)

我们终于升级了,有一台运行 PostgreSQL 9.3.5 的服务器,现在得到了不同的结果:

select '2015/01/07 14:00:00' as the_timestamp, 
extract(epoch from cast('2015/01/07 14:00:00' as timestamp)) as the_epoch;

    the_timestamp    | the_epoch
---------------------+------------
 2015/01/07 14:00:00 | 1420639200         <<=== this is 8 hours earlier
(1 row)

这两个示例都使用 psql 作为客户端,它们都使用相同的时区:

show timezone;

      TimeZone
---------------------
 America/Los_Angeles
(1 row)

在 PostgreSQL 9.3 documentation 中,我发现了这个:

NOTE: The SQL standard requires that writing just timestamp be equivalent to timestamp without time zone, and PostgreSQL honors that behavior. (Releases prior to 7.3 treated it as timestamp with time zone.) timestamptz is accepted as an abbreviation for timestamp with time zone; this is a PostgreSQL extension.

我发现如果我将查询更改为在 9.3 服务器上使用 timestamptz,它会给出与 8.3 相同的结果:

select '2015/01/07 14:00:00' as the_timestamp, 
extract(epoch from cast('2015/01/07 14:00:00' as timestamp)) as the_epoch;

    the_timestamp    | the_epoch
---------------------+------------
 2015/01/07 14:00:00 | 1420668000
(1 row)

请注意,timestamptimestamptz 在 8.3 上给出相同的结果:

select extract(epoch from cast('2015/01/07 14:00:00' as timestamptz));
 date_part
------------
 1420668000
(1 row)

select extract(epoch from cast('2015/01/07 14:00:00' as timestamp));
 date_part
------------
 1420668000
(1 row)

我们似乎在 9.3 中发现了一个错误?似乎以这种方式提取是错误地假设 with time zone 而它不应该。

TIMESTAMP WITH TIME ZONE并不是你想的那样。不幸的是,它并不意味着 "take this timestamp and store it, along with the associated time zone, as two separate values in a field"。相反,它被 PostgreSQL 视为 "take this timestamp, which you should assume is in local time unless it has a timezone specifier, and convert it to UTC, then store it as UTC. Convert it back to local time for display."

时区信息实际使用然后在导入时丢弃,使 TIMESTAMP WITH TIME ZONE 成为一个可怕的用词不当。

您遇到的问题是 timestamp with time zone 的纪元是 UTC,而不是当地时间,纪元。因为除非您指定时区说明符,否则假定时间戳为当地时间,这意味着时区会影响输入的解释。

详情

当你写:

cast('2015/01/07 14:00:00' as timestamp)

或文字的等价物:

TIMESTAMP '2015/01/07 14:00:00'

你是说 "the timestamp '2015/01/07 14:00:00' as a point in wall-clock time with no time zone defined." 本地时区不影响它。假定纪元与时间戳位于同一时区,无论它是什么。这就是设置 TimeZone 对其没有影响的原因:

regress=# SET TimeZone = 'Australia/Perth';
SET
regress=# SELECT extract(epoch from cast('2015/01/07 14:00:00' as timestamp));
 date_part  
------------
 1420639200
(1 row)

regress=# SET TimeZone = UTC;
SET
regress=# SELECT extract(epoch from cast('2015/01/07 14:00:00' as timestamp));
 date_part  
------------
 1420639200
(1 row)

现在,当您改为使用 timestamp with time zone 时,您是在说时间戳是当地时间,除非另有说明。它将被导入并转换为 UTC 以供内部存储。然后它被转换回本地时间,由 TimeZone 定义,display/output.

纪元是 UTC,不是本地时间。

这就是为什么会发生这种情况:

regress=# SET TimeZone = 'Australia/Perth';
SET
regress=# SELECT extract(epoch from cast('2015/01/07 14:00:00' as timestamp with time zone));
 date_part  
------------
 1420610400
(1 row)

regress=# SET TimeZone = UTC;
SET
regress=# SELECT extract(epoch from cast('2015/01/07 14:00:00' as timestamp with time zone));
 date_part  
------------
 1420639200
(1 row)

extract结果不同的原因是输入的时间戳值不同。它是相同的值,但在读取和加载值时会考虑 TimeZone。如果您在 table:

中查看它会更有意义
CREATE TABLE myts (ts timestamp without time zone, tstz timestamp with time zone);

SET TimeZone = UTC;
INSERT INTO myts(ts,tstz) VALUES ('2015/01/07 14:00:00','2015/01/07 14:00:00');
SET TimeZone = 'Australia/Perth';
INSERT INTO myts(ts,tstz) VALUES ('2015/01/07 14:00:00','2015/01/07 14:00:00');

现在看内容:

regress=# Set TimeZone = UTC;
SET
regress=# SELECT * FROM myts;
         ts          |          tstz          
---------------------+------------------------
 2015-01-07 14:00:00 | 2015-01-07 14:00:00+00
 2015-01-07 14:00:00 | 2015-01-07 06:00:00+00
(2 rows)

和时代:

regress=# SELECT extract(epoch from ts) as ets, extract(epoch from tstz) as etstz FROM myts;
    ets     |   etstz    
------------+------------
 1420639200 | 1420639200
 1420639200 | 1420610400
(2 rows)

如您所见,影响事物的是输入,而不是输出。

明确时区呢?

现在,如果我们在输入中设置明确的时区会怎样?

SET TimeZone = UTC;

INSERT INTO myts(ts,tstz) VALUES ('2015/01/07 14:00:00 +8','2015/01/07 14:00:00 +8');

您会看到效果与将 TimeZone 设置为 Australia/Perth 相同,即输入时忽略本地 TimeZone 设置,因为时间戳包含明确的时区。

虽然有和没有时区,这仍然会为时间戳产生不同的纪元。时区限定符从 timestamp 字段 discarded,而它用于 convert timstamptz 字段。

(是的,timestamp 上的时区被丢弃的事实很可怕。关于 SQL 次有很多可怕的事情。)

那么如何得到想要的结果呢?

如果您需要本地时间而不是世界时点,请使用 timestamp

或者,告诉 PostgreSQL 您想要 timestamptz 的纪元而不转换回本地时间,即在 UTC 中,通过使用 AT TIME ZONE 运算符将其重新解释为 UTC 中的时间戳:

SELECT extract(epoch from cast('2015/01/07 14:00:00' as timestamp) AT TIME ZONE 'UTC');

或者 运行 您的服务器,时区设置为 UTC。坦率地说,这是大多数人所做的,因为 TimeZonetimestamptimestamptz 的语义在大多数时候都不是很有用。

为什么 8.3 不同?

不知道,我必须比我有时间挖掘更多的发行说明和提交日志。看起来 timstamptz 输入已更改为尊重 TimeZone,但我不知道当时的确切理由是什么时候或什么。