带有时区的 Postgresql date_trunc 将区域移动 1 小时

lit*_*erg 3 postgresql timezone postgresql-9.4

我们使用的是 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
Run Code Online (Sandbox Code Playgroud)

截断到例如天时没有这种行为:

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
Run Code Online (Sandbox Code Playgroud)

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

poz*_*ozs 5

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

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

2) 达到或超过day精度时,根据当前配置参数(可以用或设置)重新计算时区偏移。重新计算的偏移量始终与在相同配置参数生效的确切时刻相同。所以,例如 当参数包含 DST信息时,则相应地对齐偏移量。但是,当实际参数不包含 DST 信息(例如固定偏移量)时,结果的时区偏移量不变。TimeZoneset time zone '...'set TimeZone to '...' TimeZoneTimeZoneTimeZone

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

date_trunc('month', tstz)
Run Code Online (Sandbox Code Playgroud)

应该相当于:

date_trunc('month', tstz at time zone current_setting('TimeZone')) at time zone current_setting('TimeZone'))
Run Code Online (Sandbox Code Playgroud)

至少,我是这么想的。事实证明,有一些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
Run Code Online (Sandbox Code Playgroud)

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

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$;
Run Code Online (Sandbox Code Playgroud)

http://rextester.com/GBL17756