【问题标题】:Postgresql date_trunc with time zone shifts zone by 1 hr带有时区的 Postgresql date_trunc 将区域移动 1 小时
【发布时间】:2016-12-11 16:10:20
【问题描述】:

我们使用的是 Postgresql 9.4,我在使用 date_trunc 时发现了一个奇怪的行为。结果中的时区偏移了1小时:

select date_trunc('year','2016-08-05 04:01:58.372486-05'::timestamp with time zone);
       date_trunc
------------------------
2016-01-01 00:00:00-06

截断到例如day时没有这样的行为:

select date_trunc('day','2016-08-05 04:01:58.372486-05'::timestamp with time zone);
       date_trunc
------------------------
2016-08-05 00:00:00-05

这是预期的行为吗?如果是这样,这背后的逻辑是什么?

【问题讨论】:

  • 这很可能是因为夏令时规则:您当前的时区设置的偏移量在2016-08-052016-01-01 之间移动
  • 哦。我没有想到这一点。你说的对。此外,我注意到 date_trunk 总是截断到在 postgres 上设置的时区,它没有考虑时间戳的时区。示例:select date_trunc('year','2016-08-05 04:01:58.372486-05'::timestamp with time zone); => 2016-01-01 00:00:00+02
  • 现在,这很奇怪......但是在浏览文档之后,似乎date_trunc 应该只接受timestamps,而不是timestamp with time zones(实际上,它应该通过剪切自动转换它们时区关闭)并返回timestamps。但相反,我在pg_catalog(默认架构)中找到了date_trunc(text, timestamp with time zone) returns timestamp with time zone 定义,文档中根本没有提及。
  • 所以我的意思是,这似乎是一个未记录的功能,因此最终用户不应该依赖它。如果您想要一个面向未来的解决方案,您应该将值转换为timestamp (without time zone),或者在调用date_trunc 之前使用ts AT TIME ZONE tz 表达式。

标签: postgresql timezone postgresql-9.4


【解决方案1】:

date_trunc(text, timestamptz) 变体似乎记录不足,因此这是我的发现:

1) 在day精度(第一个参数)以下,结果的时区offset始终与第二个参数的偏移量相同。

2) 在day 或以上精度时,时区偏移量 会根据当前TimeZone 配置参数(can be setset time zone '...'set TimeZone to '...')重新计算.重新计算的偏移量始终与在 same TimeZone 配置参数生效的确切时间瞬间相同。所以,f.ex。当TimeZone 参数包含夏令时信息时,则相应地对齐偏移量。但是,当实际的TimeZone参数不包含夏令时信息(如固定偏移量)时,结果的时区偏移量是不变的。

总而言之,date_trunc(text, timestamptz) 函数可以使用date_trunc(text, timestamp) 变体和at time zone 运算符进行模拟s

date_trunc('month', tstz)

应该相当于:

date_trunc('month', tstz at time zone current_setting('TimeZone')) at time zone current_setting('TimeZone'))

至少,我是这么认为的。事实证明,有一些TimeZone 配置设置是有问题的。因为:

PostgreSQL 允许您以三种不同的形式指定时区:

  • 完整时区名称,例如America/New_York。已识别的时区名称列在pg_timezone_names 视图中(参见第 50.80 节)。 PostgreSQL 为此使用广泛使用的 IANA 时区数据,因此许多其他软件也可以识别相同的时区名称。

  • 时区缩写,例如PST。这样的规范仅定义了与UTC 的特定偏移量,而完整时区名称也可能暗示一组夏令时转换日期规则。已识别的缩写列在pg_timezone_abbrevs 视图中(请参阅第 50.79 节)。您不能将配置参数 TimeZonelog_timezone 设置为时区缩写,但您可以在日期/时间输入值和 AT TIME ZONE 运算符中使用缩写。

(第三个是修正偏移量,或者它的 POSIX 形式,但这在这里并不重要)。

如您所见,缩写不能设置为TimeZone。但也有一些缩写,也被认为是全时区名称,f.ex。 CET。因此,set time zone 'CET' 会成功,但实际上会在夏季使用CEST。但是at time zone 'CET' 总是指缩写,它是UTC 的固定偏移量(而不是CEST,因为可以使用at time zone 'CEST';但set time zone 'CEST' 无效) .

以下是时区设置的完整列表,它们在set time zone 中使用时与在at time zone 中使用时具有不兼容的含义(截至 9.6):

CET
EET
MET
WET

使用以下脚本,您可以检查您的版本:

create or replace function incompatible_tz_settings()
  returns setof text
  language plpgsql
as $func$
declare
  cur cursor for select name from pg_timezone_names;
begin
  for rec IN cur loop
    declare
      r pg_timezone_names;
    begin
      r := rec;
      execute format('set time zone %L', (r).name);
      if exists(select 1
                from   generate_series(current_timestamp - interval '12 months', current_timestamp + interval '12 months', interval '1 month') tstz
                where  date_trunc('month', tstz) <> date_trunc('month', tstz at time zone (r).name) at time zone (r).name) then
        return next (r).name;
      end if;
    end;
  end loop;
end
$func$;

http://rextester.com/GBL17756

【讨论】:

【解决方案2】:

预计date_trunc 有两种变体:一种用于timestamp,一种用于timestamptz,因为the doc 表示:

下面描述的所有函数和运算符都需要时间或 时间戳输入实际上有两种变体:一种需要时间 带时区或带时区的时间戳,以及需要时间的时间戳 没有时区或没有时区的时间戳。为简洁起见,这些 变体没有单独显示。

如果你想更好地理解timestamp和timestamptz,请先阅读the great answer here

然后关于date_trunc。根据我对各种 SO 答案(如this one)的实验和解释,一切都表现得好像在收到时间戳时,date_trunc 首先将其转换为时间戳。此转换返回本地时间的时间戳。然后执行截断:仅保留日期并删除小时/分钟/秒。

为避免这种转换(感谢 pozs),请向 date_trunc 提供时间戳(不是 timestamptz):

date_trunc('day', TIMESTAMPTZ '2001-07-16 23:38:40Z' at time zone 'UTC')

at time zone 'UTC' 部分说“将此 timestamptz 转换为 UTC 时间的时间戳”(小时不受此转换的影响)。然后 date_trunc 返回2001-07-16 00:00:00

【讨论】:

  • 好收获。我没有在文档中注意到这一点,但这是有道理的。
猜你喜欢
  • 2012-06-22
  • 2014-07-25
  • 2016-09-23
  • 1970-01-01
  • 2019-06-25
  • 1970-01-01
  • 2021-12-05
  • 1970-01-01
  • 2013-03-27
相关资源
最近更新 更多