4

we are using Postgresql 9.4 and i noticed a strange behavior when using date_trunc. The time zone in result is shifted by 1hr:

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

There is no such behavior when truncating to for example 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

Is this expected behavior? If so what is the logic behind that?

literg
  • 482
  • 5
  • 13
  • 1
    That's most probably because daylight saving time rules: your current timezone setting's offset is shifted between `2016-08-05` and `2016-01-01` – pozs Aug 05 '16 at 09:45
  • 1
    Oh. I didn't thought about that. You are right. Additionally I noticed that date_trunk is always truncating to the time zone which is set on postgres it doesn't take into account the time zone of timestamp. Example: `select date_trunc('year','2016-08-05 04:01:58.372486-05'::timestamp with time zone); => 2016-01-01 00:00:00+02` – literg Aug 05 '16 at 09:58
  • 1
    Now, that is stange ... but after browsing the docs, it seems `date_trunc` should only accept `timestamp`s, not `timestamp with time zone`s (actually, it should convert those automatically by cutting the time zone off) and return `timestamp`s. But instead, I found a `date_trunc(text, timestamp with time zone) returns timestamp with time zone` definition in the `pg_catalog` (the default schema), which is not mentioned in the docs at all. – pozs Aug 05 '16 at 10:05
  • 2
    So my point is, this seems like it is an undocumented feature, so end-users shouldn't rely on it. If you want a solution, which is future-proof, you should either cast values to `timestamp (without time zone)`, or use the `ts AT TIME ZONE tz` expression before calling `date_trunc`. – pozs Aug 05 '16 at 10:07
  • Also found [this related thread on Postgres' mailing list](https://www.postgresql.org/message-id/20041101220039.0E16B3A46CD%40svr1.postgresql.org) – pozs Aug 05 '16 at 10:11
  • Yes, the documentation is misleading. I guess it means "type `timestamp`, be it with or without time zone". In `psql` you can say `\df date_trunc` to see the truth. – Laurenz Albe Aug 05 '16 at 12:02

2 Answers2

6

The date_trunc(text, timestamptz) variant seems a bit under-documented, so here are my findings:

1) below the day precision (first parameter) the time zone offset of the result is always the same as the second parameters' offset.

2) at or above day precision, the time zone offset is recalculated, according to the current TimeZone configuration parameter (which can be set with set time zone '...' or set TimeZone to '...'). The recalculated offset is always the same as it would be on that exact time-instant with the same TimeZone configuration parameter in effect. So, f.ex. when the TimeZone parameter contains DST information, then the offset is aligned accordingly. However, when the actual TimeZone parameter does not contain DST informations (such as a fix offset), the result's time zone offset is untouched.

All-in-all, the date_trunc(text, timestamptz) function can be simulated with the date_trunc(text, timestamp) variant and the at time zone operators:

date_trunc('month', tstz)

should be equivalent to:

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

At least, that's what I thought. As it turned out, there are some TimeZone configuration settings, which are problematic. Because:

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.

(The third is the fix offset, or its POSIX form, but that's not important here).

As you can see, abbreviations cannot be set to TimeZone. But there are some abbreviations, which is also considered a full time zone name, f.ex. CET. Thus, set time zone 'CET' will succeed, but will actually use CEST in the summer time. But at time zone 'CET' will always refer to the abbreviation, which is a fixed offset from UTC (and never CEST, for that one can use at time zone 'CEST'; but set time zone 'CEST' is invalid).

Here is a full list of time zone settings, which has incompatible meanings when they are used in set time zone vs. when they are used in at time zone (as of 9.6):

CET
EET
MET
WET

With the following script, you can check for your version:

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

pozs
  • 34,608
  • 5
  • 57
  • 63
  • 1
    There is indeed a special case for `timestamptz_trunc` (and not for `timestamp_trunc`) and finer than day precision: look for `redotz` in https://github.com/postgres/postgres/blob/master/src/backend/utils/adt/timestamp.c – jrouquie Apr 27 '17 at 15:37
4

It is expected to have two variants of date_trunc: one for timestamp and one for timestamptz, because the doc says:

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.

Should you want to better understand timestamp and timestamptz, read first the great answer here.

Then about date_trunc. According to my experiments and interpretation of various SO answers (like this one), everything behaves as if, when receiving a timestamptz, date_trunc first converts it to a timestamp. This conversion returns a timestamp in local time. Then truncation is performed: keep only the date and drop the hours/min/seconds.

To avoid this conversion (thanks pozs), provide a timestamp (not timestamptz) to date_trunc:

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

the part at time zone 'UTC' says "convert this timestamptz to a timestamp in UTC time" (the hour isn't affected by this conversion). Then date_trunc returns 2001-07-16 00:00:00.

Community
  • 1
  • 1
jrouquie
  • 4,315
  • 4
  • 27
  • 43