Postgresql date_trunc with time zone shifts zone by 1 hr

Postgresql date_trunc with time zone shifts zone by 1 hr

我们正在使用 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

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

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

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

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

All the functions and operators described below that take time or timestamp inputs actually come in two variants: one that takes time with time zone or timestamp with time zone, and one that takes time without time zone or timestamp without time zone. For brevity, these variants are not shown separately.

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

然后大约date_trunc。根据我的实验和对各种 SO 答案(如 )的解释,一切都表现得好像在收到时间戳时,date_trunc 首先将其转换为时间戳。此转换 returns 本地时间的时间戳。然后执行截断:仅保留日期并删除 hours/min/seconds.

为避免这种转换(感谢 pozs),请向 date_trunc:

提供一个时间戳(不是 timestamptz)
date_trunc('day', TIMESTAMPTZ '2001-07-16 23:38:40Z' at time zone 'UTC')

at time zone 'UTC' 部分表示 "convert this timestamptz to a timestamp in UTC time"(小时不受此转换影响)。然后date_truncreturns2001-07-16 00:00:00.

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

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

2) 达到或超过 day 精度,时区 offset 根据当前 TimeZone 配置参数(其中 can be setset time zone '...'set TimeZone to '...')。重新计算的偏移量始终与在 same TimeZone 配置参数生效时的确切时间相同。所以,f.ex。当 TimeZone 参数 包含 DST 信息时,则相应地对齐偏移量。但是,当实际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 allows you to specify time zones in three different forms:

  • A full time zone name, for example America/New_York. The recognized time zone names are listed in the pg_timezone_names view (see Section 50.80). PostgreSQL uses the widely-used IANA time zone data for this purpose, so the same time zone names are also recognized by much other software.

  • A time zone abbreviation, for example PST. Such a specification merely defines a particular offset from UTC, in contrast to full time zone names which can imply a set of daylight savings transition-date rules as well. The recognized abbreviations are listed in the pg_timezone_abbrevs view (see Section 50.79). You cannot set the configuration parameters TimeZone or log_timezone to a time zone abbreviation, but you can use abbreviations in date/time input values and with the AT TIME ZONE operator.

(第三个是固定偏移量,或者它的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